From d843b67d2d47c1c86980a3582d959abf2f8e1bcb Mon Sep 17 00:00:00 2001 From: ridiculousfish Date: Sat, 14 Jan 2023 14:56:24 -0800 Subject: [PATCH] Initial Rust commit --- .github/workflows/main.yml | 25 +- .gitignore | 11 + CMakeLists.txt | 23 +- CONTRIBUTING.rst | 4 +- README.rst | 1 + cmake/Rust.cmake | 48 + cmake/Tests.cmake | 16 + doc_internal/fish-riir-plan.md | 77 ++ doc_internal/rust-devel.md | 162 ++++ fish-rust/Cargo.lock | 1021 +++++++++++++++++++++ fish-rust/Cargo.toml | 40 + fish-rust/build.rs | 45 + fish-rust/src/fd_readable_set.rs | 239 +++++ fish-rust/src/fds.rs | 88 ++ fish-rust/src/ffi.rs | 57 ++ fish-rust/src/ffi_init.rs | 26 + fish-rust/src/flog.rs | 198 ++++ fish-rust/src/lib.rs | 20 + fish-rust/src/signal.rs | 40 + fish-rust/src/smoke.rs | 21 + fish-rust/src/topic_monitor.rs | 640 +++++++++++++ fish-rust/src/wchar.rs | 34 + fish-rust/src/wchar_ext.rs | 137 +++ fish-rust/src/wchar_ffi.rs | 131 +++ fish-rust/src/wgetopt.rs | 610 ++++++++++++ fish-rust/widestring-suffix/Cargo.lock | 47 + fish-rust/widestring-suffix/Cargo.toml | 12 + fish-rust/widestring-suffix/src/lib.rs | 51 + fish-rust/widestring-suffix/tests/test.rs | 24 + src/builtins/function.cpp | 2 +- src/builtins/wait.cpp | 2 +- src/common.cpp | 10 +- src/common.h | 9 +- src/env_universal_common.cpp | 5 +- src/event.cpp | 2 +- src/fd_monitor.cpp | 8 +- src/fd_monitor.h | 4 +- src/fds.cpp | 112 +-- src/fds.h | 67 +- src/fish.cpp | 6 +- src/fish_key_reader.cpp | 5 +- src/fish_test_helper.cpp | 1 + src/fish_tests.cpp | 30 +- src/flog.cpp | 2 + src/flog.h | 4 + src/function.cpp | 2 +- src/input.cpp | 4 +- src/input_common.cpp | 8 +- src/io.cpp | 4 + src/io.h | 11 +- src/iothread.cpp | 3 +- src/parser.cpp | 8 +- src/parser.h | 9 +- src/postfork.cpp | 2 +- src/proc.cpp | 16 +- src/proc.h | 30 +- src/reader.cpp | 5 +- src/rustffi.cpp | 21 + src/{signal.cpp => signals.cpp} | 16 +- src/{signal.h => signals.h} | 0 src/topic_monitor.cpp | 283 ------ src/topic_monitor.h | 258 +----- src/wutil.cpp | 6 +- src/wutil.h | 16 +- 64 files changed, 4052 insertions(+), 767 deletions(-) create mode 100644 cmake/Rust.cmake create mode 100644 doc_internal/fish-riir-plan.md create mode 100644 doc_internal/rust-devel.md create mode 100644 fish-rust/Cargo.lock create mode 100644 fish-rust/Cargo.toml create mode 100644 fish-rust/build.rs create mode 100644 fish-rust/src/fd_readable_set.rs create mode 100644 fish-rust/src/fds.rs create mode 100644 fish-rust/src/ffi.rs create mode 100644 fish-rust/src/ffi_init.rs create mode 100644 fish-rust/src/flog.rs create mode 100644 fish-rust/src/lib.rs create mode 100644 fish-rust/src/signal.rs create mode 100644 fish-rust/src/smoke.rs create mode 100644 fish-rust/src/topic_monitor.rs create mode 100644 fish-rust/src/wchar.rs create mode 100644 fish-rust/src/wchar_ext.rs create mode 100644 fish-rust/src/wchar_ffi.rs create mode 100644 fish-rust/src/wgetopt.rs create mode 100644 fish-rust/widestring-suffix/Cargo.lock create mode 100644 fish-rust/widestring-suffix/Cargo.toml create mode 100644 fish-rust/widestring-suffix/src/lib.rs create mode 100644 fish-rust/widestring-suffix/tests/test.rs create mode 100644 src/rustffi.cpp rename src/{signal.cpp => signals.cpp} (96%) rename src/{signal.h => signals.h} (100%) delete mode 100644 src/topic_monitor.cpp diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c39741e20..47b888e52 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: SetupRust + uses: ATiltedTree/setup-rust@v1 + with: + rust-version: beta - name: Install deps run: | sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux @@ -42,6 +46,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: SetupRust + uses: ATiltedTree/setup-rust@v1 + with: + rust-version: beta + targets: "i686-unknown-linux-gnu" # setup-rust wants this space-separated - name: Install deps run: | sudo apt update @@ -53,10 +62,10 @@ jobs: CFLAGS: "-m32" run: | mkdir build && cd build - cmake -DFISH_USE_SYSTEM_PCRE2=OFF .. + cmake -DFISH_USE_SYSTEM_PCRE2=OFF -DRust_CARGO_TARGET=i686-unknown-linux-gnu .. - name: make run: | - make + make VERBOSE=1 - name: make test run: | make test @@ -67,6 +76,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: SetupRust + uses: ATiltedTree/setup-rust@v1 + with: + rust-version: beta - name: Install deps run: | sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux @@ -101,6 +114,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: SetupRust + uses: ATiltedTree/setup-rust@v1 + with: + rust-version: beta - name: Install deps run: | sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux @@ -127,6 +144,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: SetupRust + uses: ATiltedTree/setup-rust@v1 + with: + rust-version: beta - name: Install deps run: | sudo pip3 install pexpect diff --git a/.gitignore b/.gitignore index 917c3ac4d..52bf88e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,14 @@ __pycache__ /tags xcuserdata/ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + diff --git a/CMakeLists.txt b/CMakeLists.txt index 08d7c54e3..971dad866 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,8 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}") endif() +include(cmake/Rust.cmake) + # Error out when linking statically, it doesn't work. if (CMAKE_EXE_LINKER_FLAGS MATCHES ".*-static.*") message(FATAL_ERROR "Fish does not support static linking") @@ -43,6 +45,9 @@ endif() # - address, because that occurs for our mkostemp check (weak-linking requires us to compare `&mkostemp == nullptr`). add_compile_options(-Wall -Wextra -Wno-comment -Wno-address) +# Get extra C++ files from Rust. +get_property(FISH_EXTRA_SOURCES TARGET fish-rust PROPERTY fish_extra_cpp_files) + if ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")) add_compile_options(-Wunused-template -Wunused-local-typedef -Wunused-macros) endif() @@ -53,6 +58,9 @@ add_compile_options(-fno-exceptions) # Undefine NDEBUG to keep assert() in release builds. add_definitions(-UNDEBUG) +# Allow including Rust headers in normal (not bindgen) builds. +add_definitions(-DINCLUDE_RUST_HEADERS) + # Enable large files on GNU. add_definitions(-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE @@ -117,10 +125,10 @@ set(FISH_SRCS src/pager.cpp src/parse_execution.cpp src/parse_tree.cpp src/parse_util.cpp src/parser.cpp src/parser_keywords.cpp src/path.cpp src/postfork.cpp src/proc.cpp src/re.cpp src/reader.cpp src/redirection.cpp src/screen.cpp - src/signal.cpp src/termsize.cpp src/timer.cpp src/tinyexpr.cpp - src/tokenizer.cpp src/topic_monitor.cpp src/trace.cpp src/utf8.cpp src/util.cpp + src/signals.cpp src/termsize.cpp src/timer.cpp src/tinyexpr.cpp + src/tokenizer.cpp src/trace.cpp src/utf8.cpp src/util.cpp src/wait_handle.cpp src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp - src/wutil.cpp src/fds.cpp + src/wutil.cpp src/fds.cpp src/rustffi.cpp ) # Header files are just globbed. @@ -133,6 +141,11 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config_cmake.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) +# Pull in our src directory for headers searches, but only quoted ones. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -iquote ${CMAKE_CURRENT_SOURCE_DIR}/src") + + + # Set up standard directories. include(GNUInstallDirs) add_definitions(-D_UNICODE=1 @@ -175,8 +188,10 @@ endfunction(FISH_LINK_DEPS_AND_SIGN) add_library(fishlib STATIC ${FISH_SRCS} ${FISH_BUILTIN_SRCS}) target_sources(fishlib PRIVATE ${FISH_HEADERS}) target_link_libraries(fishlib + fish-rust ${CURSES_LIBRARY} ${CURSES_EXTRA_LIBRARY} Threads::Threads ${CMAKE_DL_LIBS} - ${PCRE2_LIB} ${Intl_LIBRARIES} ${ATOMIC_LIBRARY}) + ${PCRE2_LIB} ${Intl_LIBRARIES} ${ATOMIC_LIBRARY} + "fish-rust") target_include_directories(fishlib PRIVATE ${CURSES_INCLUDE_DIRS}) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6bfc24ba5..f19d2afce 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -420,8 +420,8 @@ Include What You Use You should not depend on symbols being visible to a ``*.cpp`` module from ``#include`` statements inside another header file. In other words if your module does ``#include "common.h"`` and that header does -``#include "signal.h"`` your module should not assume the sub-include is -present. It should instead directly ``#include "signal.h"`` if it needs +``#include "signals.h"`` your module should not assume the sub-include is +present. It should instead directly ``#include "signals.h"`` if it needs any symbol from that header. That makes the actual dependencies much clearer. It also makes it easy to modify the headers included by a specific header file without having to worry that will break any module diff --git a/README.rst b/README.rst index d13e9e5f0..3286fa055 100644 --- a/README.rst +++ b/README.rst @@ -148,6 +148,7 @@ Dependencies Compiling fish requires: +- Rust (version 1.67 or later) - a C++11 compiler (g++ 4.8 or later, or clang 3.3 or later) - CMake (version 3.5 or later) - a curses implementation such as ncurses (headers and libraries) diff --git a/cmake/Rust.cmake b/cmake/Rust.cmake new file mode 100644 index 000000000..79c5c8372 --- /dev/null +++ b/cmake/Rust.cmake @@ -0,0 +1,48 @@ +include(FetchContent) + +# Don't let Corrosion's tests interfere with ours. +set(CORROSION_TESTS OFF CACHE BOOL "" FORCE) + +FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/ridiculousfish/corrosion + GIT_TAG fish +) + +FetchContent_MakeAvailable(Corrosion) + +set(fish_rust_target "fish-rust") + +set(fish_autocxx_gen_dir "${CMAKE_BINARY_DIR}/fish-autocxx-gen/") + +corrosion_import_crate( + MANIFEST_PATH "${CMAKE_SOURCE_DIR}/fish-rust/Cargo.toml" +) + +# We need the build dir because cxx puts our headers in there. +# Corrosion doesn't expose the build dir, so poke where we shouldn't. +if (Rust_CARGO_TARGET) + set(rust_target_dir "${CMAKE_BINARY_DIR}/cargo/build/${_CORROSION_RUST_CARGO_TARGET}") +else() + set(rust_target_dir "${CMAKE_BINARY_DIR}/cargo/build/${_CORROSION_RUST_CARGO_HOST_TARGET}") + corrosion_set_hostbuild(${fish_rust_target}) +endif() + +# Tell Cargo where our build directory is so it can find config.h. +corrosion_set_env_vars(${fish_rust_target} "FISH_BUILD_DIR=${CMAKE_BINARY_DIR}" "FISH_AUTOCXX_GEN_DIR=${fish_autocxx_gen_dir}" "FISH_RUST_TARGET_DIR=${rust_target_dir}") + +target_include_directories(${fish_rust_target} INTERFACE + "${rust_target_dir}/cxxbridge/${fish_rust_target}/src/" + "${fish_autocxx_gen_dir}/include/" +) + +# Tell fish what extra C++ files to compile. +define_property( + TARGET PROPERTY fish_extra_cpp_files + BRIEF_DOCS "Extra C++ files to compile for fish." + FULL_DOCS "Extra C++ files to compile for fish." +) + +set_property(TARGET ${fish_rust_target} PROPERTY fish_extra_cpp_files + "${fish_autocxx_gen_dir}/cxx/gen0.cxx" +) diff --git a/cmake/Tests.cmake b/cmake/Tests.cmake index ed745b4ce..b8b511ded 100644 --- a/cmake/Tests.cmake +++ b/cmake/Tests.cmake @@ -175,3 +175,19 @@ foreach(PEXPECT ${PEXPECTS}) set_tests_properties(${PEXPECT} PROPERTIES ENVIRONMENT FISH_FORCE_COLOR=1) add_test_target("${PEXPECT}") endforeach(PEXPECT) + +# Rust stuff. +add_test( + NAME "cargo-test" + COMMAND cargo test + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/fish-rust" +) +set_tests_properties("cargo-test" PROPERTIES SKIP_RETURN_CODE ${SKIP_RETURN_CODE}) +add_test_target("cargo-test") + +add_test( + NAME "cargo-test-widestring" + COMMAND cargo test + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/fish-rust/widestring-suffix/" +) +add_test_target("cargo-test-widestring") diff --git a/doc_internal/fish-riir-plan.md b/doc_internal/fish-riir-plan.md new file mode 100644 index 000000000..c2df77616 --- /dev/null +++ b/doc_internal/fish-riir-plan.md @@ -0,0 +1,77 @@ +These is a proposed port of fish-shell from C++ to Rust, and from CMake to cargo or related. This document is high level - see the Development Guide for more details. + +## Why Port + +- Gain access to more contributors and enable easier contributions. C++ is becoming a legacy language. +- Free us from the annoyances of C++/CMake, and old toolchains. +- Ensure fish continues to be perceived as modern and relevant. +- Unlock concurrent mode (see below). + +## Why Rust + +- Rust is a systems programming language with broad platform support, a large community, and a relatively high probability of still being relevant in a decade. +- Rust has a unique strength in its thread safety features, which is the missing piece to enable concurrent mode - see below. +- Other languages considered: + - Java, Python and the scripting family are ruled out for startup latency and memory usage reasons. + - Go would be an awkward fit. fork is [quite the problem](https://stackoverflow.com/questions/28370646/how-do-i-fork-a-go-process/28371586#28371586) in Go. + - Other system languages (D, Nim, Zig...) are too niche: fewer contributors, higher risk of the language becoming irrelevant. + +## Risks + +- Large amount of work with possible introduction of new bugs. +- Long period of complicated builds. +- Existing contributors will have to learn Rust. +- As of yet unknown compatibility story for Tier 2+ platforms (Cygwin, etc). + +## Approach + +We will do an **incremental port** in the span of one release. We will have a period of using both C++ and Rust, and both cargo and CMake, leveraging FFI tools (see below). + +The work will **proceed on master**: no long-lived branches. Tests and CI continue to pass at every commit for recent Linux and Mac. Centos7, \*BSD, etc may be temporarily disabled if they prove problematic. + +The Rust code will initially resemble the replaced C++. Fidelity to existing code is more important than Rust idiomaticity, to aid review and bisecting. But don't take this to extremes - use judgement. + +The port will proceed "outside in." We'll start with leaf components (e.g. builtins) and proceed towards the core. Some components will have both a Rust and C++ implementation (e.g. FLOG), in other cases we'll change the existing C++ to invoke the new Rust implementations (builtins). + +After porting the C++, we'll replace CMake. + +We will continue to use wide chars, locales, gettext, printf format strings, and PCRE2. We will not change the fish scripting language at all. We will _not_ use this as an opportunity to fix existing design flaws, with a few carefully chosen exceptions. See [Strings](#strings). + +We will not use tokio, serde, async, or other fancy Rust frameworks initially. + +### FFI + +Rust/C++ interop will use [autocxx](https://github.com/google/autocxx), [Cxx](https://cxx.rs), and possibly [bindgen](https://rust-lang.github.io/rust-bindgen/). I've forked these for fish (see the Development Guide). Once the port is done, we will stop using them, except perhaps bindgen for PCRE2. + +We will use [corrosion](https://github.com/corrosion-rs/corrosion) for CMake integration. + +Inefficiencies (e.g. extra string copying) at the FFI layer are fine, since it will all get thrown away. + +Tests can stay in fish_tests.cpp or be moved into Rust .rs files; either is fine. + +### Strings + +Rust's `String` / `&str` types cannot represent non-UTF8 filenames or data using the default encoding scheme. That's why all string conversions must go through fish's encoding scheme (using the private-use area to encode invalid sequences). For example, fish cannot use `File::open` with a `&str` because the decoding will be incorrect. + +So instead of `String`, fish will use its own string type, and manage encoding and decoding as it does today. However we will make some specific changes: + +1. Drop the nul-terminated requirement. When passing `const wchar_t*` back to C++, we will allocate and copy into a nul-terminated buffer. +2. Drop support for 16-bit wchar. fish will use UTF32 on all platforms, and manage conversions itself. + +After the port we can consider moving to UTF-8, for memory usage reasons. + +See the Rust Development Guide for more on strings. + +### Thread Safety + +Allowing [background functions](https://github.com/fish-shell/fish-shell/issues/238) and concurrent functions has been a goal for many years. I have been nursing [a long-lived branch](https://github.com/ridiculousfish/fish-shell/tree/concurrent_even_simpler) which allows full threaded execution. But though the changes are small, I have been reluctant to propose them, because they will make reasoning about the shell internals too complex: it is difficult in C++ to check and enforce what crosses thread boundaries. + +This is Rust's bread and butter: we will encode thread requirements into our types, making it explicit and compiler-checked, via Send and Sync. Rust will allow turning on concurrent mode in a safe way, with a manageable increase in complexity, finally enabling this feature. + +## Timeline + +Handwaving, 6 months? Frankly unknown - there's 102 remaining .cpp files of various lengths. It'll go faster as we get better at it. Peter (ridiculous_fish) is motivated to work on this, other current contributors have some Rust as well, and we may also get new contributors from the Rust community. Part of the point is to make contribution easier. + +## Links + +- [Packaging Rust projects](https://wiki.archlinux.org/title/Rust_package_guidelines) from Arch Linux diff --git a/doc_internal/rust-devel.md b/doc_internal/rust-devel.md new file mode 100644 index 000000000..83edc9870 --- /dev/null +++ b/doc_internal/rust-devel.md @@ -0,0 +1,162 @@ +# fish-shell Rust Development Guide + +This describes how to get started building fish-shell in its partial Rust state, and how to contribute to the port. + +## Overview + +fish is in the process of transitioning from C++ to Rust. The fish project has a Rust crate embedded at path `fish-rust`. This crate builds a Rust library `libfish_rust.a` which is linked with the C++ `libfish.a`. Existing C++ code will be incrementally migrated to this crate; then CMake will be replaced with cargo and other Rust-native tooling. + +Important tools used during this transition: + +1. [Corrosion](https://github.com/corrosion-rs/corrosion) to invoke cargo from CMake. +2. [cxx](http://cxx.rs) for basic C++ <-> Rust interop. +3. [autocxx](https://google.github.io/autocxx/) for using C++ types in Rust. + +We use forks of the last two - see the FFI section below. No special action is required to obtain these packages. They're downloaded by cargo. + +## Building + +### Build Dependencies + +fish-shell currently depends on Rust 1.67 or later. To install Rust, follow https://rustup.rs. + +### Build via CMake + +It is recommended to build inside `fish-shell/build`. This will make it easier for Rust to find the `config.h` file. + +Build via CMake as normal (use any generator, here we use Ninja): + +```shell +$ cd fish-shell +$ mkdir build && cd build +$ cmake -G Ninja .. +$ ninja +``` + +This will create the usual fish executables. + +### Build just libfish_rust.a with Cargo + +The directory `fish-rust` contains the Rust sources. These require that CMake has been run to produce `config.h` which is necessary for autocxx to succeed. + +Follow the "Build from CMake" steps above, and then: + +```shell +$ cd fish-shell/fish-rust +$ cargo build +``` + +This will build only the library, not a full working fish, but it allows faster iteration for Rust development. That is, after running `cmake` you can open the `fish-rust` as the root of a Rust crate, and tools like rust-analyzer will work. + +## Development + +The basic development loop for this port: + +1. Pick a .cpp (or in some cases .h) file to port, say `util.cpp`. +2. Add the corresponding `util.rs` file to `fish-rust/`. +3. Reimplement it in Rust, along with its dependencies as needed. Match the existing C++ code where practical, including propagating any relevant comments. + - Do this even if it results in less idiomatic Rust, but avoid being super-dogmatic either way. + - One technique is to paste the C++ into the Rust code, commented out, and go line by line. +4. Decide whether any existing C++ callers should invoke the Rust implementation, or whether we should keep the C++ one. + - Utility functions may have both a Rust and C++ implementation. An example is `FLOG` where interop is too hard. + - Major components (e.g. builtin implementations) should _not_ be duplicated; instead the Rust should call C++ or vice-versa. + +You will likely run into limitations of [`autocxx`](https://google.github.io/autocxx/) and to a lesser extent [`cxx`](https://cxx.rs/). See the FFI sections below. + +## Type Mapping + +### Strings + +Fish will mostly _not_ use Rust's `String/&str` types as these cannot represent non-UTF8 data using the default encoding. + +fish's primary string types will come from the [`widestring` crate](https://docs.rs/widestring). The two main string types are `WString` and `&wstr`, which are renamed [Utf32String](https://docs.rs/widestring/latest/widestring/utfstring/struct.Utf32String.html) and [Utf32Str](https://docs.rs/widestring/latest/widestring/utfstr/struct.Utf32Str.html). `WString` is an owned, heap-allocated UTF32 string, `&wstr` a borrowed UTF32 slice. + +In general, follow this mapping when porting from C++: + +- `wcstring` -> `WString` +- `const wcstring &` -> `&wstr` +- `const wchar_t *` -> `&wstr` + +None of the Rust string types are nul-terminated. We're taking this opportunity to drop the nul-terminated aspect of wide string handling. + +#### Creating strings + +One may create a `&wstr` from a string literal using the `wchar::L!` macro: + +```rust +use crate::wchar::{wstr, L!} + +fn get_shell_name() -> &'static wstr { + L!("fish") +} +``` + +There is also a `widestrs` proc-macro which enables L as a _suffix_, to reduce the noise. This can be applied to any block, including modules and individual functions: + +```rust +use crate::wchar::{wstr, widestrs} + +[#widestrs] +fn get_shell_name() -> &'static wstr { + "fish"L // equivalent to L!("fish") +} +``` + +### Strings for FFI + +`WString` and `&wstr` are the common strings used by Rust components. At the FII boundary there are some additional strings for interop. _All of these are temporary for the duration of the port._ + +- `CxxWString` is the Rust binding of `std::wstring`. It is the wide-string analog to [`CxxString`](https://cxx.rs/binding/cxxstring.html) and is [added in our fork of cxx](https://github.com/ridiculousfish/cxx/blob/fish/src/cxx_wstring.rs). This is useful for functions which return e.g. `const wcstring &`. +- `W0String` is renamed [U32CString](https://docs.rs/widestring/latest/widestring/ucstring/struct.U32CString.html). This is basically `WString` except it _is_ nul-terminated. This is useful for getting a nul-terminated `const wchar_t *` to pass to C++ implementations. +- `wcharz_t` is an annoying C++ struct which merely wraps a `const wchar_t *`, used for passing these pointers from C++ to Rust. We would prefer to use `const wchar_t *` directly but `autocxx` refuses to generate bindings for types such as `std::vector` so we wrap it in this silly struct. + +Note C++ `wchar_t`, Rust `char`, and `u32` are effectively interchangeable: you can cast pointers to them back and forth (except we check upon u32->char conversion). However be aware of which types are nul-terminated. + +These types should be confined to the FFI modules, in particular `wchar_ffi`. They should not "leak" into other modules. See the `wchar_ffi` module. + +### Format strings + +Rust's builtin `std::fmt` modules do not accept runtime-provided format strings, so we mostly won't use them, except perhaps for FLOG / other non-translated text. + +Instead we'll continue to use printf-style strings, with a Rust printf implementation. + +### Vectors + +See [`Vec`](https://cxx.rs/binding/vec.html) and [`CxxVector`](https://cxx.rs/binding/cxxvector.html). + +In many cases, `autocxx` refuses to allow vectors of certain types. For example, autocxx supports `std::vector` and `std::shared_ptr` but NOT `std::vector>`. To work around this one can create a helper (pointer, length) struct. Example: + +```cpp +struct RustFFIJobList { + std::shared_ptr *jobs; + size_t count; +}; +``` + +This is just a POD (plain old data) so autocxx can generate bindings for it. Then it is trivial to convert it to a Rust slice: + +``` +pub fn get_jobs(ffi_jobs: &ffi::RustFFIJobList) -> &[SharedPtr] { + unsafe { slice::from_raw_parts(ffi_jobs.jobs, ffi_jobs.count) } +} +``` + +## Development Tooling + +The [autocxx guidance](https://google.github.io/autocxx/workflow.html#how-can-i-see-what-bindings-autocxx-has-generated) is helpful: + +1. Install cargo expand (`cargo install cargo-expand`). Then you can use `cargo expand` to see the generated Rust bindings for C++. In particular this is useful for seeing failed expansions for C++ types that autocxx cannot handle. +2. In rust-analyzer, enable Proc Macro and Proc Macro Attributes. + +## FFI + +The boundary between Rust and C++ is referred to as the FII. + +`autocxx` and `cxx` both are designed for long-term interop: C++ and Rust coexisting for years. To this end, both emphasize safety: requiring lots of `unsafe`, `Pin`, etc. + +fish plans to use them only temporarily, with a focus on getting things working. To this end, both cxx and autocxx have been forked to support fish: + +1. Relax the requirement that all functions taking pointers are `unsafe` (this just added noise). +2. Add support for `wchar_t` as a recognized type, and `CxxWString` analogous to `CxxString`. + +See the `Cargo.toml` file for the locations of the forks. diff --git a/fish-rust/Cargo.lock b/fish-rust/Cargo.lock new file mode 100644 index 000000000..5dfdb98d3 --- /dev/null +++ b/fish-rust/Cargo.lock @@ -0,0 +1,1021 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aquamarine" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" +dependencies = [ + "itertools 0.9.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "autocxx" +version = "0.23.1" +source = "git+https://github.com/ridiculousfish/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99" +dependencies = [ + "aquamarine", + "autocxx-macro", + "cxx", + "moveit", +] + +[[package]] +name = "autocxx-bindgen" +version = "0.62.0" +source = "git+https://github.com/ridiculousfish/autocxx-bindgen?branch=fish#a229d3473bd90d2d10fc61a244408cfc1958934a" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "autocxx-build" +version = "0.23.1" +source = "git+https://github.com/ridiculousfish/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99" +dependencies = [ + "autocxx-engine", + "env_logger", + "indexmap", + "syn", +] + +[[package]] +name = "autocxx-engine" +version = "0.23.1" +source = "git+https://github.com/ridiculousfish/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99" +dependencies = [ + "aquamarine", + "autocxx-bindgen", + "autocxx-parser", + "cc", + "cxx-gen", + "indexmap", + "indoc", + "itertools 0.10.5", + "log", + "miette", + "once_cell", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustversion", + "serde_json", + "strum_macros", + "syn", + "tempfile", + "thiserror", + "version_check", +] + +[[package]] +name = "autocxx-macro" +version = "0.23.1" +source = "git+https://github.com/ridiculousfish/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99" +dependencies = [ + "autocxx-parser", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocxx-parser" +version = "0.23.1" +source = "git+https://github.com/ridiculousfish/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99" +dependencies = [ + "indexmap", + "itertools 0.10.5", + "log", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "thiserror", +] + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.81" +source = "git+https://github.com/ridiculousfish/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", + "widestring", +] + +[[package]] +name = "cxx-build" +version = "1.0.81" +source = "git+https://github.com/ridiculousfish/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxx-gen" +version = "0.7.81" +source = "git+https://github.com/ridiculousfish/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63" +dependencies = [ + "codespan-reporting", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.81" +source = "git+https://github.com/ridiculousfish/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.81" +source = "git+https://github.com/ridiculousfish/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fish-rust" +version = "0.1.0" +dependencies = [ + "autocxx", + "autocxx-build", + "cxx", + "cxx-build", + "cxx-gen", + "errno", + "lazy_static", + "libc", + "miette", + "nix", + "num-traits", + "unixstring", + "widestring", + "widestring-suffix", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "indoc" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2d6f23ffea9d7e76c53eee25dfb67bcd8fde7f1198b0855350698c9f07c780" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd9b301defa984bbdbe112b4763e093ed191750a0d914a78c1106b2d0fe703" +dependencies = [ + "atty", + "backtrace", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c2401ab7ac5282ca5c8b518a87635b1a93762b0b90b9990c509888eeccba29" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "moveit" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d756ffe4e38013507d35bf726a93fcdae2cae043ab5ce477f13857a335030d" +dependencies = [ + "cxx", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "prettyplease" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "supports-color" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" +dependencies = [ + "atty", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406" +dependencies = [ + "atty", +] + +[[package]] +name = "supports-unicode" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2" +dependencies = [ + "atty", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unixstring" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366c5c5657cbe7a684b3476acc7b96d4087e953bf750b1eab4dfbffeda32b2f3" +dependencies = [ + "libc", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "widestring-suffix" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/fish-rust/Cargo.toml b/fish-rust/Cargo.toml new file mode 100644 index 000000000..a79abc3d1 --- /dev/null +++ b/fish-rust/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "fish-rust" +version = "0.1.0" +edition = "2021" + + +[dependencies] +widestring-suffix = { path = "./widestring-suffix/" } + +autocxx = "0.23.1" +cxx = "1.0" +errno = "0.2.8" +lazy_static = "1.4.0" +libc = "0.2.137" +nix = "0.25.0" +num-traits = "0.2.15" +unixstring = "0.2.7" +widestring = "1.0.2" + +[build-dependencies] +autocxx-build = "0.23.1" +cxx-build = { git = "https://github.com/ridiculousfish/cxx", branch = "fish" } +cxx-gen = { git = "https://github.com/ridiculousfish/cxx", branch = "fish" } +miette = { version = "5", features = ["fancy"] } + +[lib] +crate-type=["staticlib"] + +[patch.crates-io] +cxx = { git = "https://github.com/ridiculousfish/cxx", branch = "fish" } +cxx-gen = { git = "https://github.com/ridiculousfish/cxx", branch = "fish" } +autocxx = { git = "https://github.com/ridiculousfish/autocxx", branch = "fish" } +autocxx-build = { git = "https://github.com/ridiculousfish/autocxx", branch = "fish" } +autocxx-bindgen = { git = "https://github.com/ridiculousfish/autocxx-bindgen", branch = "fish" } + +#cxx = { path = "../../cxx" } +#cxx-gen = { path="../../cxx/gen/lib" } +#autocxx = { path = "../../autocxx" } +#autocxx-build = { path = "../../autocxx/gen/build" } +#autocxx-bindgen = { path = "../../autocxx-bindgen" } diff --git a/fish-rust/build.rs b/fish-rust/build.rs new file mode 100644 index 000000000..a805721e6 --- /dev/null +++ b/fish-rust/build.rs @@ -0,0 +1,45 @@ +fn main() -> miette::Result<()> { + let rust_dir = std::env::var("CARGO_MANIFEST_DIR").expect("Env var CARGO_MANIFEST_DIR missing"); + let target_dir = + std::env::var("FISH_RUST_TARGET_DIR").unwrap_or(format!("{}/{}", rust_dir, "target/")); + let fish_src_dir = format!("{}/{}", rust_dir, "../src/"); + + // Where cxx emits its header. + let cxx_include_dir = format!("{}/{}", target_dir, "cxxbridge/rust/"); + + // If FISH_BUILD_DIR is given by CMake, then use it; otherwise assume it's at ../build. + let fish_build_dir = + std::env::var("FISH_BUILD_DIR").unwrap_or(format!("{}/{}", rust_dir, "../build/")); + + // Where autocxx should put its stuff. + let autocxx_gen_dir = std::env::var("FISH_AUTOCXX_GEN_DIR") + .unwrap_or(format!("{}/{}", fish_build_dir, "fish-autocxx-gen/")); + + // Emit cxx junk. + // This allows "Rust to be used from C++" + // This must come before autocxx so that cxx can emit its cxx.h header. + let source_files = vec![ + "src/fd_readable_set.rs", + "src/ffi_init.rs", + "src/smoke.rs", + "src/topic_monitor.rs", + ]; + cxx_build::bridges(source_files) + .flag_if_supported("-std=c++11") + .include(&fish_src_dir) + .include(&fish_build_dir) // For config.h + .include(&cxx_include_dir) // For cxx.h + .compile("fish-rust"); + + // Emit autocxx junk. + // This allows "C++ to be used from Rust." + let include_paths = [&fish_src_dir, &fish_build_dir, &cxx_include_dir]; + let mut b = autocxx_build::Builder::new("src/ffi.rs", &include_paths) + .custom_gendir(autocxx_gen_dir.into()) + .build()?; + b.flag_if_supported("-std=c++11") + .compile("fish-rust-autocxx"); + println!("cargo:rerun-if-changed=src/ffi.rs"); + + Ok(()) +} diff --git a/fish-rust/src/fd_readable_set.rs b/fish-rust/src/fd_readable_set.rs new file mode 100644 index 000000000..315360759 --- /dev/null +++ b/fish-rust/src/fd_readable_set.rs @@ -0,0 +1,239 @@ +use libc::c_int; +use std::os::unix::io::RawFd; + +#[cxx::bridge] +mod fd_readable_set_ffi { + extern "Rust" { + type fd_readable_set_t; + fn new_fd_readable_set() -> Box; + fn clear(&mut self); + fn add(&mut self, fd: i32); + fn test(&self, fd: i32) -> bool; + fn check_readable(&mut self, timeout_usec: u64) -> i32; + fn is_fd_readable(fd: i32, timeout_usec: u64) -> bool; + fn poll_fd_readable(fd: i32) -> bool; + } +} + +/// Create a new fd_readable_set_t. +pub fn new_fd_readable_set() -> Box { + Box::new(fd_readable_set_t::new()) +} + +/// \return true if the fd is or becomes readable within the given timeout. +/// This returns false if the waiting is interrupted by a signal. +pub fn is_fd_readable(fd: i32, timeout_usec: u64) -> bool { + fd_readable_set_t::is_fd_readable(fd, timeout_usec) +} + +/// \return whether an fd is readable. +pub fn poll_fd_readable(fd: i32) -> bool { + fd_readable_set_t::poll_fd_readable(fd) +} + +/// A modest wrapper around select() or poll(). +/// This allows accumulating a set of fds and then seeing if they are readable. +/// This only handles readability. +/// Apple's `man poll`: "The poll() system call currently does not support devices." +#[cfg(target_os = "macos")] +pub struct fd_readable_set_t { + // The underlying fdset and nfds value to pass to select(). + fdset_: libc::fd_set, + nfds_: c_int, +} + +const kUsecPerMsec: u64 = 1000; +const kUsecPerSec: u64 = 1000 * kUsecPerMsec; + +#[cfg(target_os = "macos")] +impl fd_readable_set_t { + /// Construct an empty set. + pub fn new() -> fd_readable_set_t { + fd_readable_set_t { + fdset_: unsafe { std::mem::zeroed() }, + nfds_: 0, + } + } + + /// Reset back to an empty set. + pub fn clear(&mut self) { + self.nfds_ = 0; + unsafe { + libc::FD_ZERO(&mut self.fdset_); + } + } + + /// Add an fd to the set. The fd is ignored if negative (for convenience). + pub fn add(&mut self, fd: RawFd) { + if fd >= (libc::FD_SETSIZE as RawFd) { + //FLOGF(error, "fd %d too large for select()", fd); + return; + } + if fd >= 0 { + unsafe { libc::FD_SET(fd, &mut self.fdset_) }; + self.nfds_ = std::cmp::max(self.nfds_, fd + 1); + } + } + + /// \return true if the given fd is marked as set, in our set. \returns false if negative. + pub fn test(&self, fd: RawFd) -> bool { + fd >= 0 && unsafe { libc::FD_ISSET(fd, &self.fdset_) } + } + + /// Call select() or poll(), according to FISH_READABLE_SET_USE_POLL. Note this destructively + /// modifies the set. \return the result of select() or poll(). + pub fn check_readable(&mut self, timeout_usec: u64) -> c_int { + let null = std::ptr::null_mut(); + if timeout_usec == Self::kNoTimeout { + unsafe { + return libc::select( + self.nfds_, + &mut self.fdset_, + null, + null, + std::ptr::null_mut(), + ); + } + } else { + let mut tvs = libc::timeval { + tv_sec: (timeout_usec / kUsecPerSec) as libc::time_t, + tv_usec: (timeout_usec % kUsecPerSec) as libc::suseconds_t, + }; + unsafe { + return libc::select(self.nfds_, &mut self.fdset_, null, null, &mut tvs); + } + } + } + + /// Check if a single fd is readable, with a given timeout. + /// \return true if readable, false if not. + pub fn is_fd_readable(fd: RawFd, timeout_usec: u64) -> bool { + if fd < 0 { + return false; + } + let mut s = Self::new(); + s.add(fd); + let res = s.check_readable(timeout_usec); + return res > 0 && s.test(fd); + } + + /// Check if a single fd is readable, without blocking. + /// \return true if readable, false if not. + pub fn poll_fd_readable(fd: RawFd) -> bool { + return Self::is_fd_readable(fd, 0); + } + + /// A special timeout value which may be passed to indicate no timeout. + pub const kNoTimeout: u64 = u64::MAX; +} + +#[cfg(not(target_os = "macos"))] +pub struct fd_readable_set_t { + pollfds_: Vec, +} + +#[cfg(not(target_os = "macos"))] +impl fd_readable_set_t { + /// Construct an empty set. + pub fn new() -> fd_readable_set_t { + fd_readable_set_t { + pollfds_: Vec::new(), + } + } + + /// Reset back to an empty set. + pub fn clear(&mut self) { + self.pollfds_.clear(); + } + + #[inline] + fn pollfd_get_fd(pollfd: &libc::pollfd) -> RawFd { + pollfd.fd + } + + /// Add an fd to the set. The fd is ignored if negative (for convenience). + pub fn add(&mut self, fd: RawFd) { + if fd >= 0 { + if let Err(pos) = self.pollfds_.binary_search_by_key(&fd, Self::pollfd_get_fd) { + self.pollfds_.insert( + pos, + libc::pollfd { + fd: fd, + events: libc::POLLIN, + revents: 0, + }, + ); + } + } + } + + /// \return true if the given fd is marked as set, in our set. \returns false if negative. + pub fn test(&self, fd: RawFd) -> bool { + // If a pipe is widowed with no data, Linux sets POLLHUP but not POLLIN, so test for both. + if let Ok(pos) = self.pollfds_.binary_search_by_key(&fd, Self::pollfd_get_fd) { + let pollfd = &self.pollfds_[pos]; + debug_assert_eq!(pollfd.fd, fd); + return pollfd.revents & (libc::POLLIN | libc::POLLHUP) != 0; + } + return false; + } + + // Convert from a usec to a poll-friendly msec. + fn usec_to_poll_msec(timeout_usec: u64) -> c_int { + let mut timeout_msec: u64 = timeout_usec / kUsecPerMsec; + // Round to nearest, down for halfway. + if (timeout_usec % kUsecPerMsec) > kUsecPerMsec / 2 { + timeout_msec += 1; + } + if timeout_usec == fd_readable_set_t::kNoTimeout || timeout_msec > c_int::MAX as u64 { + // Negative values mean wait forever in poll-speak. + return -1; + } + return timeout_msec as c_int; + } + + fn do_poll(fds: &mut [libc::pollfd], timeout_usec: u64) -> c_int { + let count = fds.len(); + assert!(count <= libc::nfds_t::MAX as usize, "count too big"); + return unsafe { + libc::poll( + fds.as_mut_ptr(), + count as libc::nfds_t, + Self::usec_to_poll_msec(timeout_usec), + ) + }; + } + + /// Call select() or poll(), according to FISH_READABLE_SET_USE_POLL. Note this destructively + /// modifies the set. \return the result of select() or poll(). + pub fn check_readable(&mut self, timeout_usec: u64) -> c_int { + if self.pollfds_.is_empty() { + return 0; + } + return Self::do_poll(&mut self.pollfds_, timeout_usec); + } + + /// Check if a single fd is readable, with a given timeout. + /// \return true if readable, false if not. + pub fn is_fd_readable(fd: RawFd, timeout_usec: u64) -> bool { + if fd < 0 { + return false; + } + let mut pfd = libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }; + let ret = Self::do_poll(std::slice::from_mut(&mut pfd), timeout_usec); + return ret > 0 && (pfd.revents & libc::POLLIN) != 0; + } + + /// Check if a single fd is readable, without blocking. + /// \return true if readable, false if not. + pub fn poll_fd_readable(fd: RawFd) -> bool { + return Self::is_fd_readable(fd, 0); + } + + /// A special timeout value which may be passed to indicate no timeout. + pub const kNoTimeout: u64 = u64::MAX; +} diff --git a/fish-rust/src/fds.rs b/fish-rust/src/fds.rs new file mode 100644 index 000000000..a7092c644 --- /dev/null +++ b/fish-rust/src/fds.rs @@ -0,0 +1,88 @@ +use crate::ffi; +use nix::unistd; +use std::os::unix::io::RawFd; + +/// A helper type for managing and automatically closing a file descriptor +pub struct autoclose_fd_t { + fd_: RawFd, +} + +impl autoclose_fd_t { + // Closes the fd if not already closed. + pub fn close(&mut self) { + if self.fd_ != -1 { + _ = unistd::close(self.fd_); + self.fd_ = -1; + } + } + + // Returns the fd. + pub fn fd(&self) -> RawFd { + self.fd_ + } + + // Returns the fd, transferring ownership to the caller. + pub fn acquire(&mut self) -> RawFd { + let temp = self.fd_; + self.fd_ = -1; + temp + } + + // Resets to a new fd, taking ownership. + pub fn reset(&mut self, fd: RawFd) { + if fd == self.fd_ { + return; + } + self.close(); + self.fd_ = fd; + } + + // \return if this has a valid fd. + pub fn valid(&self) -> bool { + self.fd_ >= 0 + } + + // Construct, taking ownership of an fd. + pub fn new(fd: RawFd) -> autoclose_fd_t { + autoclose_fd_t { fd_: fd } + } +} + +impl Default for autoclose_fd_t { + fn default() -> autoclose_fd_t { + autoclose_fd_t { fd_: -1 } + } +} + +impl Drop for autoclose_fd_t { + fn drop(&mut self) { + self.close() + } +} + +/// Helper type returned from make_autoclose_pipes. +#[derive(Default)] +pub struct autoclose_pipes_t { + /// Read end of the pipe. + pub read: autoclose_fd_t, + + /// Write end of the pipe. + pub write: autoclose_fd_t, +} + +/// Construct a pair of connected pipes, set to close-on-exec. +/// \return None on fd exhaustion. +pub fn make_autoclose_pipes() -> Option { + let pipes = ffi::make_pipes_ffi(); + + let readp = autoclose_fd_t::new(pipes.read); + let writep = autoclose_fd_t::new(pipes.write); + if !readp.valid() || !writep.valid() { + None + } else { + Some(autoclose_pipes_t { + read: readp, + write: writep, + }) + } +} diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs new file mode 100644 index 000000000..5323d00af --- /dev/null +++ b/fish-rust/src/ffi.rs @@ -0,0 +1,57 @@ +use crate::wchar::{self}; +use ::std::slice; +use autocxx::prelude::*; + +// autocxx has been hacked up to know about this. +pub type wchar_t = u32; + +include_cpp! { + #include "fds.h" + #include "wutil.h" + #include "flog.h" + #include "io.h" + #include "parse_util.h" + #include "wildcard.h" + #include "parser.h" + #include "proc.h" + #include "common.h" + #include "builtin.h" + + safety!(unsafe_ffi) + + generate_pod!("wcharz_t") + generate!("make_fd_nonblocking") + generate!("wperror") + + generate_pod!("pipes_ffi_t") + generate!("make_pipes_ffi") + + generate!("get_flog_file_fd") + + generate!("parse_util_unescape_wildcards") + + generate!("wildcard_match") + +} + +/// Allow wcharz_t to be "into" wstr. +impl From for &wchar::wstr { + fn from(w: wcharz_t) -> Self { + let len = w.length(); + let v = unsafe { slice::from_raw_parts(w.str_ as *const u32, len) }; + wchar::wstr::from_slice(v).expect("Invalid UTF-32") + } +} + +/// Allow wcharz_t to be "into" WString. +impl From for wchar::WString { + fn from(w: wcharz_t) -> Self { + let len = w.length(); + let v = unsafe { slice::from_raw_parts(w.str_ as *const u32, len).to_vec() }; + Self::from_vec(v).expect("Invalid UTF-32") + } +} + +pub use autocxx::c_int; +pub use ffi::*; +pub use libc::c_char; diff --git a/fish-rust/src/ffi_init.rs b/fish-rust/src/ffi_init.rs new file mode 100644 index 000000000..018d722b4 --- /dev/null +++ b/fish-rust/src/ffi_init.rs @@ -0,0 +1,26 @@ +/// Bridged functions concerned with initialization. +use crate::ffi::wcharz_t; + +#[cxx::bridge] +mod ffi2 { + + extern "C++" { + include!("wutil.h"); + type wcharz_t = super::wcharz_t; + } + + extern "Rust" { + fn rust_init(); + fn rust_activate_flog_categories_by_pattern(wc_ptr: wcharz_t); + } +} + +/// Entry point for Rust-specific initialization. +fn rust_init() { + crate::topic_monitor::topic_monitor_init(); +} + +/// FFI bridge for activate_flog_categories_by_pattern(). +fn rust_activate_flog_categories_by_pattern(wc_ptr: wcharz_t) { + crate::flog::activate_flog_categories_by_pattern(wc_ptr.into()); +} diff --git a/fish-rust/src/flog.rs b/fish-rust/src/flog.rs new file mode 100644 index 000000000..50989fc22 --- /dev/null +++ b/fish-rust/src/flog.rs @@ -0,0 +1,198 @@ +use crate::ffi::{get_flog_file_fd, parse_util_unescape_wildcards, wildcard_match}; +use crate::wchar::{widestrs, wstr, WString}; +use crate::wchar_ffi::WCharToFFI; +use std::io::Write; +use std::os::unix::io::{FromRawFd, IntoRawFd, RawFd}; +use std::sync::atomic::Ordering; + +#[rustfmt::skip::macros(category)] +#[widestrs] +pub mod categories { + use super::wstr; + use std::sync::atomic::AtomicBool; + + pub struct category_t { + pub name: &'static wstr, + pub description: &'static wstr, + pub enabled: AtomicBool, + } + + /// Macro to declare a static variable identified by $var, + /// with the given name and description, and optionally enabled by default. + macro_rules! declare_category { + ( + ($var:ident, $name:expr, $description:expr, $enabled:expr) + ) => { + pub static $var: category_t = category_t { + name: $name, + description: $description, + enabled: AtomicBool::new($enabled), + }; + }; + ( + ($var:ident, $name:expr, $description:expr) + ) => { + declare_category!(($var, $name, $description, false)); + }; + } + + /// Macro to extract the variable name for a category. + macro_rules! category_name { + (($var:ident, $name:expr, $description:expr, $enabled:expr)) => { + $var + }; + (($var:ident, $name:expr, $description:expr)) => { + $var + }; + } + + macro_rules! categories { + ( + // A repetition of categories, separated by semicolons. + $($cats:tt);* + + // Allow trailing semicolon. + $(;)? + ) => { + // Declare each category. + $( + declare_category!($cats); + )* + + // Define a function which gives you a Vector of all categories. + pub fn all_categories() -> Vec<&'static category_t> { + vec![ + $( + & category_name!($cats), + )* + ] + } + }; + } + + categories!( + (error, "error"L, "Serious unexpected errors (on by default)"L, true); + + (debug, "debug"L, "Debugging aid (on by default)"L, true); + + (warning, "warning"L, "Warnings (on by default)"L, true); + + (warning_path, "warning-path"L, "Warnings about unusable paths for config/history (on by default)"L, true); + + (config, "config"L, "Finding and reading configuration"L); + + (event, "event"L, "Firing events"L); + + (exec, "exec"L, "Errors reported by exec (on by default)"L, true); + + (exec_job_status, "exec-job-status"L, "Jobs changing status"L); + + (exec_job_exec, "exec-job-exec"L, "Jobs being executed"L); + + (exec_fork, "exec-fork"L, "Calls to fork()"L); + + (output_invalid, "output-invalid"L, "Trying to print invalid output"L); + (ast_construction, "ast-construction"L, "Parsing fish AST"L); + + (proc_job_run, "proc-job-run"L, "Jobs getting started or continued"L); + + (proc_termowner, "proc-termowner"L, "Terminal ownership events"L); + + (proc_internal_proc, "proc-internal-proc"L, "Internal (non-forked) process events"L); + + (proc_reap_internal, "proc-reap-internal"L, "Reaping internal (non-forked) processes"L); + + (proc_reap_external, "proc-reap-external"L, "Reaping external (forked) processes"L); + (proc_pgroup, "proc-pgroup"L, "Process groups"L); + + (env_locale, "env-locale"L, "Changes to locale variables"L); + + (env_export, "env-export"L, "Changes to exported variables"L); + + (env_dispatch, "env-dispatch"L, "Reacting to variables"L); + + (uvar_file, "uvar-file"L, "Writing/reading the universal variable store"L); + (uvar_notifier, "uvar-notifier"L, "Notifications about universal variable changes"L); + + (topic_monitor, "topic-monitor"L, "Internal details of the topic monitor"L); + (char_encoding, "char-encoding"L, "Character encoding issues"L); + + (history, "history"L, "Command history events"L); + (history_file, "history-file"L, "Reading/Writing the history file"L); + + (profile_history, "profile-history"L, "History performance measurements"L); + + (iothread, "iothread"L, "Background IO thread events"L); + (fd_monitor, "fd-monitor"L, "FD monitor events"L); + + (term_support, "term-support"L, "Terminal feature detection"L); + + (reader, "reader"L, "The interactive reader/input system"L); + (reader_render, "reader-render"L, "Rendering the command line"L); + (complete, "complete"L, "The completion system"L); + (path, "path"L, "Searching/using paths"L); + + (screen, "screen"L, "Screen repaints"L); + ); +} + +/// Write to our FLOG file. +pub fn flog_impl(s: &str) { + let fd = get_flog_file_fd().0 as RawFd; + if fd < 0 { + return; + } + let mut file = unsafe { std::fs::File::from_raw_fd(fd) }; + let _ = file.write(s.as_bytes()); + // Ensure the file is not closed. + file.into_raw_fd(); +} + +macro_rules! FLOG { + ($category:ident, $($elem:expr),+) => { + if crate::flog::categories::$category.enabled.load(Ordering::Relaxed) { + let mut vs = Vec::new(); + $( + vs.push(format!("{:?}", $elem)); + )+ + // We don't use locking here so we have to append our own newline to avoid multiple writes. + let mut v = vs.join(" "); + v.push('\n'); + crate::flog::flog_impl(&v); + } + }; +} +pub(crate) use FLOG; + +/// For each category, if its name matches the wildcard, set its enabled to the given sense. +fn apply_one_wildcard(wc_esc: &wstr, sense: bool) { + let wc = parse_util_unescape_wildcards(&wc_esc.to_ffi()); + let mut match_found = false; + for cat in categories::all_categories() { + if wildcard_match(&cat.name.to_ffi(), &*wc, false) { + cat.enabled.store(sense, Ordering::Relaxed); + match_found = true; + } + } + if !match_found { + eprintln!("Failed to match debug category: {}\n", wc_esc); + } +} + +/// Set the active flog categories according to the given wildcard \p wc. +pub fn activate_flog_categories_by_pattern(wc_ptr: &wstr) { + let mut wc: WString = wc_ptr.into(); + // Normalize underscores to dashes, allowing the user to be sloppy. + for c in wc.as_char_slice_mut() { + if *c == '_' { + *c = '-'; + } + } + for s in wc.as_char_slice().split(|c| *c == ',') { + if s.starts_with(&['-']) { + apply_one_wildcard(wstr::from_char_slice(&s[1..]), false); + } else { + apply_one_wildcard(wstr::from_char_slice(s), true); + } + } +} diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs new file mode 100644 index 000000000..9478d740d --- /dev/null +++ b/fish-rust/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(non_camel_case_types)] +#![allow(dead_code)] +#![allow(non_upper_case_globals)] +#![allow(clippy::needless_return)] + +#[macro_use] +extern crate lazy_static; + +mod fd_readable_set; +mod fds; +mod ffi; +mod ffi_init; +mod flog; +mod signal; +mod smoke; +mod topic_monitor; +mod wchar; +mod wchar_ext; +mod wchar_ffi; +mod wgetopt; diff --git a/fish-rust/src/signal.rs b/fish-rust/src/signal.rs new file mode 100644 index 000000000..faa646b97 --- /dev/null +++ b/fish-rust/src/signal.rs @@ -0,0 +1,40 @@ +use crate::topic_monitor::{generation_t, invalid_generations, topic_monitor_principal, topic_t}; + +/// A sigint_detector_t can be used to check if a SIGINT (or SIGHUP) has been delivered. +pub struct sigchecker_t { + topic: topic_t, + gen: generation_t, +} + +impl sigchecker_t { + /// Create a new checker for the given topic. + pub fn new(topic: topic_t) -> sigchecker_t { + let mut res = sigchecker_t { topic, gen: 0 }; + // Call check() to update our generation. + res.check(); + res + } + + /// Create a new checker for SIGHUP and SIGINT. + pub fn new_sighupint() -> sigchecker_t { + Self::new(topic_t::sighupint) + } + + /// Check if a sigint has been delivered since the last call to check(), or since the detector + /// was created. + pub fn check(&mut self) -> bool { + let tm = topic_monitor_principal(); + let gen = tm.generation_for_topic(self.topic); + let changed = self.gen != gen; + self.gen = gen; + changed + } + + /// Wait until a sigint is delivered. + pub fn wait(&self) { + let tm = topic_monitor_principal(); + let mut gens = invalid_generations(); + *gens.at_mut(self.topic) = self.gen; + tm.check(&mut gens, true /* wait */); + } +} diff --git a/fish-rust/src/smoke.rs b/fish-rust/src/smoke.rs new file mode 100644 index 000000000..105d065c1 --- /dev/null +++ b/fish-rust/src/smoke.rs @@ -0,0 +1,21 @@ +#[cxx::bridge(namespace = rust)] +mod ffi { + extern "Rust" { + fn add(left: usize, right: usize) -> usize; + } +} + +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/fish-rust/src/topic_monitor.rs b/fish-rust/src/topic_monitor.rs new file mode 100644 index 000000000..09b63e2bf --- /dev/null +++ b/fish-rust/src/topic_monitor.rs @@ -0,0 +1,640 @@ +use crate::fd_readable_set::fd_readable_set_t; +use crate::fds::{self, autoclose_pipes_t}; +use crate::ffi::{self as ffi, c_int}; +use crate::flog::FLOG; +use crate::wchar::{widestrs, wstr, WString}; +use crate::wchar_ffi::wcharz; +use nix::errno::Errno; +use nix::unistd; +use std::cell::UnsafeCell; +use std::mem; +use std::pin::Pin; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Condvar, Mutex, MutexGuard, +}; + +/** Topic monitoring support. Topics are conceptually "a thing that can happen." For example, +delivery of a SIGINT, a child process exits, etc. It is possible to post to a topic, which means +that that thing happened. + +Associated with each topic is a current generation, which is a 64 bit value. When you query a +topic, you get back a generation. If on the next query the generation has increased, then it +indicates someone posted to the topic. + +For example, if you are monitoring a child process, you can query the sigchld topic. If it has +increased since your last query, it is possible that your child process has exited. + +Topic postings may be coalesced. That is there may be two posts to a given topic, yet the +generation only increases by 1. The only guarantee is that after a topic post, the current +generation value is larger than any value previously queried. + +Tying this all together is the topic_monitor_t. This provides the current topic generations, and +also provides the ability to perform a blocking wait for any topic to change in a particular topic +set. This is the real power of topics: you can wait for a sigchld signal OR a thread exit. +*/ + +#[cxx::bridge] +mod topic_monitor_ffi { + /// Simple value type containing the values for a topic. + /// This should be kept in sync with topic_t. + #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] + struct generation_list_t { + pub sighupint: u64, + pub sigchld: u64, + pub internal_exit: u64, + } + + extern "Rust" { + fn invalid_generations() -> generation_list_t; + fn set_min_from(self: &mut generation_list_t, topic: topic_t, other: &generation_list_t); + fn at(self: &generation_list_t, topic: topic_t) -> u64; + fn at_mut(self: &mut generation_list_t, topic: topic_t) -> &mut u64; + //fn describe(self: &generation_list_t) -> UniquePtr; + } + + /// The list of topics which may be observed. + #[repr(u8)] + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] + pub enum topic_t { + sighupint, // Corresponds to both SIGHUP and SIGINT signals. + sigchld, // Corresponds to SIGCHLD signal. + internal_exit, // Corresponds to an internal process exit. + } + + extern "Rust" { + type topic_monitor_t; + fn new_topic_monitor() -> Box; + + fn topic_monitor_principal() -> &'static topic_monitor_t; + fn post(self: &topic_monitor_t, topic: topic_t); + fn current_generations(self: &topic_monitor_t) -> generation_list_t; + fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> u64; + fn check(self: &topic_monitor_t, gens: *mut generation_list_t, wait: bool) -> bool; + } +} + +pub use topic_monitor_ffi::{generation_list_t, topic_t}; +pub type generation_t = u64; + +/// A generation value which indicates the topic is not of interest. +pub const invalid_generation: generation_t = std::u64::MAX; + +pub fn all_topics() -> [topic_t; 3] { + [topic_t::sighupint, topic_t::sigchld, topic_t::internal_exit] +} + +#[widestrs] +impl generation_list_t { + pub fn new() -> Self { + Self::default() + } + + fn describe(&self) -> WString { + let mut result = WString::new(); + for gen in self.as_array() { + if result.len() > 0 { + result.push(','); + } + if gen == invalid_generation { + result.push_str("-1"); + } else { + result.push_str(&gen.to_string()); + } + } + return result; + } + + /// \return the a mutable reference to the value for a topic. + pub fn at_mut(&mut self, topic: topic_t) -> &mut generation_t { + match topic { + topic_t::sighupint => &mut self.sighupint, + topic_t::sigchld => &mut self.sigchld, + topic_t::internal_exit => &mut self.internal_exit, + _ => panic!("invalid topic"), + } + } + + /// \return the value for a topic. + pub fn at(&self, topic: topic_t) -> generation_t { + match topic { + topic_t::sighupint => self.sighupint, + topic_t::sigchld => self.sigchld, + topic_t::internal_exit => self.internal_exit, + _ => panic!("invalid topic"), + } + } + + /// \return ourselves as an array. + pub fn as_array(&self) -> [generation_t; 3] { + [self.sighupint, self.sigchld, self.internal_exit] + } + + /// Set the value of \p topic to the smaller of our value and the value in \p other. + pub fn set_min_from(&mut self, topic: topic_t, other: &generation_list_t) { + if self.at(topic) > other.at(topic) { + *self.at_mut(topic) = other.at(topic); + } + } + + /// \return whether a topic is valid. + pub fn is_valid(&self, topic: topic_t) -> bool { + self.at(topic) != invalid_generation + } + + /// \return whether any topic is valid. + pub fn any_valid(&self) -> bool { + let mut valid = false; + for gen in self.as_array() { + if gen != invalid_generation { + valid = true; + } + } + valid + } + + /// Generation list containing invalid generations only. + pub fn invalids() -> generation_list_t { + generation_list_t { + sighupint: invalid_generation, + sigchld: invalid_generation, + internal_exit: invalid_generation, + } + } +} + +/// CXX wrapper as it does not support member functions. +pub fn invalid_generations() -> generation_list_t { + generation_list_t::invalids() +} + +/// A simple binary semaphore. +/// On systems that do not support unnamed semaphores (macOS in particular) this is built on top of +/// a self-pipe. Note that post() must be async-signal safe. +pub struct binary_semaphore_t { + // Whether our semaphore was successfully initialized. + sem_ok_: bool, + + // The semaphore, if initalized. + // This is Box'd so it has a stable address. + sem_: Pin>>, + + // Pipes used to emulate a semaphore, if not initialized. + pipes_: autoclose_pipes_t, +} + +impl binary_semaphore_t { + pub fn new() -> binary_semaphore_t { + #[allow(unused_mut, unused_assignments)] + let mut sem_ok_ = false; + // sem_t does not have an initializer in Rust so we use zeroed(). + #[allow(unused_mut)] + let mut sem_ = Pin::from(Box::new(UnsafeCell::new(unsafe { mem::zeroed() }))); + let mut pipes_ = autoclose_pipes_t::default(); + // sem_init always fails with ENOSYS on Mac and has an annoying deprecation warning. + // On BSD sem_init uses a file descriptor under the hood which doesn't get CLOEXEC (see #7304). + // So use fast semaphores on Linux only. + #[cfg(target_os = "linux")] + { + let res = unsafe { libc::sem_init(sem_.get(), 0, 0) }; + sem_ok_ = res == 0; + } + if !sem_ok_ { + let pipes = fds::make_autoclose_pipes(); + assert!(pipes.is_some(), "Failed to make pubsub pipes"); + pipes_ = pipes.unwrap(); + + // // Whoof. Thread Sanitizer swallows signals and replays them at its leisure, at the point + // // where instrumented code makes certain blocking calls. But tsan cannot interrupt a signal + // // call, so if we're blocked in read() (like the topic monitor wants to be!), we'll never + // // receive SIGCHLD and so deadlock. So if tsan is enabled, we mark our fd as non-blocking + // // (so reads will never block) and use select() to poll it. + if cfg!(feature = "FISH_TSAN_WORKAROUNDS") { + ffi::make_fd_nonblocking(c_int(pipes_.read.fd())); + } + } + binary_semaphore_t { + sem_ok_, + sem_, + pipes_, + } + } + + /// Release a waiting thread. + #[widestrs] + pub fn post(&self) { + // Beware, we are in a signal handler. + if self.sem_ok_ { + let res = unsafe { libc::sem_post(self.sem_.get()) }; + // sem_post is non-interruptible. + if res < 0 { + self.die("sem_post"L); + } + } else { + // Write exactly one byte. + let success; + loop { + let v: u8 = 0; + let ret = unistd::write(self.pipes_.write.fd(), std::slice::from_ref(&v)); + if ret.err() == Some(Errno::EINTR) { + continue; + } + success = ret.is_ok(); + break; + } + if !success { + self.die("write"L); + } + } + } + + /// Wait for a post. + /// This loops on EINTR. + #[widestrs] + pub fn wait(&self) { + if self.sem_ok_ { + let mut res; + loop { + res = unsafe { libc::sem_wait(self.sem_.get()) }; + if res < 0 && Errno::last() == Errno::EINTR { + continue; + } + break; + } + // Other errors here are very unexpected. + if res < 0 { + self.die("sem_wait"L); + } + } else { + let fd = self.pipes_.read.fd(); + // We must read exactly one byte. + loop { + // Under tsan our notifying pipe is non-blocking, so we would busy-loop on the read() + // call until data is available (that is, fish would use 100% cpu while waiting for + // processes). This call prevents that. + if cfg!(feature = "FISH_TSAN_WORKAROUNDS") { + let _ = fd_readable_set_t::is_fd_readable(fd, fd_readable_set_t::kNoTimeout); + } + let mut ignored: u8 = 0; + let amt = unistd::read(fd, std::slice::from_mut(&mut ignored)); + if amt.ok() == Some(1) { + break; + } + // EAGAIN should only be returned in TSan case. + if amt.is_err() + && (amt.err() != Some(Errno::EINTR) && amt.err() != Some(Errno::EAGAIN)) + { + self.die("read"L); + } + } + } + } + + pub fn die(&self, msg: &wstr) { + ffi::wperror(wcharz!(msg)); + panic!("die"); + } +} + +impl Drop for binary_semaphore_t { + fn drop(&mut self) { + // We never use sem_t on Mac. The #ifdef avoids deprecation warnings. + #[cfg(target_os = "linux")] + { + if self.sem_ok_ { + _ = unsafe { libc::sem_destroy(self.sem_.get()) }; + } + } + } +} + +impl Default for binary_semaphore_t { + fn default() -> Self { + Self::new() + } +} + +/// The topic monitor class. This permits querying the current generation values for topics, +/// optionally blocking until they increase. +/// What we would like to write is that we have a set of topics, and threads wait for changes on a +/// condition variable which is tickled in post(). But this can't work because post() may be called +/// from a signal handler and condition variables are not async-signal safe. +/// So instead the signal handler announces changes via a binary semaphore. +/// In the wait case, what generally happens is: +/// A thread fetches the generations, see they have not changed, and then decides to try to wait. +/// It does so by atomically swapping in STATUS_NEEDS_WAKEUP to the status bits. +/// If that succeeds, it waits on the binary semaphore. The post() call will then wake the thread +/// up. If if failed, then either a post() call updated the status values (so perhaps there is a +/// new topic post) or some other thread won the race and called wait() on the semaphore. Here our +/// thread will wait on the data_notifier_ queue. +type topic_bitmask_t = u8; + +fn topic_to_bit(t: topic_t) -> topic_bitmask_t { + 1 << t.repr +} + +// Some stuff that needs to be protected by the same lock. +#[derive(Default)] +struct data_t { + /// The current values. + current: generation_list_t, + + /// A flag indicating that there is a current reader. + /// The 'reader' is responsible for calling sema_.wait(). + has_reader: bool, +} + +/// Sentinel status value indicating that a thread is waiting and needs a wakeup. +/// Note it is an error for this bit to be set and also any topic bit. +const STATUS_NEEDS_WAKEUP: u8 = 128; +type status_bits_t = u8; + +#[derive(Default)] +pub struct topic_monitor_t { + data_: Mutex, + + /// Condition variable for broadcasting notifications. + /// This is associated with data_'s mutex. + data_notifier_: Condvar, + + /// A status value which describes our current state, managed via atomics. + /// Three possibilities: + /// 0: no changed topics, no thread is waiting. + /// 128: no changed topics, some thread is waiting and needs wakeup. + /// anything else: some changed topic, no thread is waiting. + /// Note that if the msb is set (status == 128) no other bit may be set. + status_: AtomicU8, + + /// Binary semaphore used to communicate changes. + /// If status_ is STATUS_NEEDS_WAKEUP, then a thread has commited to call wait() on our sema and + /// this must be balanced by the next call to post(). Note only one thread may wait at a time. + sema_: binary_semaphore_t, +} + +/// The principal topic monitor. +/// Do not attempt to move this into a lazy_static, it must be accessed from a signal handler. +static mut s_principal: *const topic_monitor_t = std::ptr::null(); + +/// Create a new topic monitor. Exposed for the FFI. +pub fn new_topic_monitor() -> Box { + Box::new(topic_monitor_t::default()) +} + +impl topic_monitor_t { + /// Initialize the principal monitor, and return it. + /// This should be called only on the main thread. + pub fn initialize() -> &'static Self { + unsafe { + if s_principal.is_null() { + // We simply leak. + s_principal = Box::into_raw(new_topic_monitor()); + } + &*s_principal + } + } + + pub fn post(&self, topic: topic_t) { + // Beware, we may be in a signal handler! + // Atomically update the pending topics. + let topicbit = topic_to_bit(topic); + const relaxed: Ordering = Ordering::Relaxed; + + // CAS in our bit, capturing the old status value. + let mut oldstatus: status_bits_t = 0; + let mut cas_success = false; + while !cas_success { + oldstatus = self.status_.load(relaxed); + // Clear wakeup bit and set our topic bit. + let mut newstatus = oldstatus; + newstatus &= !STATUS_NEEDS_WAKEUP; // note: bitwise not + newstatus |= topicbit; + cas_success = self + .status_ + .compare_exchange_weak(oldstatus, newstatus, relaxed, relaxed) + .is_ok(); + } + // Note that if the STATUS_NEEDS_WAKEUP bit is set, no other bits must be set. + assert!( + ((oldstatus == STATUS_NEEDS_WAKEUP) == ((oldstatus & STATUS_NEEDS_WAKEUP) != 0)), + "If STATUS_NEEDS_WAKEUP is set no other bits should be set" + ); + + // If the bit was already set, then someone else posted to this topic and nobody has reacted to + // it yet. In that case we're done. + if (oldstatus & topicbit) != 0 { + return; + } + + // We set a new bit. + // Check if we should wake up a thread because it was waiting. + if (oldstatus & STATUS_NEEDS_WAKEUP) != 0 { + std::sync::atomic::fence(Ordering::Release); + self.sema_.post(); + } + } + + /// Apply any pending updates to the data. + /// This accepts data because it must be locked. + /// \return the updated generation list. + fn updated_gens_in_data(&self, data: &mut MutexGuard) -> generation_list_t { + // Atomically acquire the pending updates, swapping in 0. + // If there are no pending updates (likely) or a thread is waiting, just return. + // Otherwise CAS in 0 and update our topics. + const relaxed: Ordering = Ordering::Relaxed; + let mut changed_topic_bits: topic_bitmask_t = 0; + let mut cas_success = false; + while !cas_success { + changed_topic_bits = self.status_.load(relaxed); + if changed_topic_bits == 0 || changed_topic_bits == STATUS_NEEDS_WAKEUP { + return data.current; + } + cas_success = self + .status_ + .compare_exchange_weak(changed_topic_bits, 0, relaxed, relaxed) + .is_ok(); + } + assert!( + (changed_topic_bits & STATUS_NEEDS_WAKEUP) == 0, + "Thread waiting bit should not be set" + ); + + // Update the current generation with our topics and return it. + for topic in all_topics() { + if changed_topic_bits & topic_to_bit(topic) != 0 { + *data.current.at_mut(topic) += 1; + FLOG!( + topic_monitor, + "Updating topic", + topic, + "to", + data.current.at(topic) + ); + } + } + // Report our change. + self.data_notifier_.notify_all(); + return data.current; + } + + /// \return the current generation list, opportunistically applying any pending updates. + fn updated_gens(&self) -> generation_list_t { + let mut data = self.data_.lock().unwrap(); + return self.updated_gens_in_data(&mut data); + } + + /// Access the current generations. + pub fn current_generations(self: &topic_monitor_t) -> generation_list_t { + self.updated_gens() + } + + /// Access the generation for a topic. + pub fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> generation_t { + self.current_generations().at(topic) + } + + /// Given a list of input generations, attempt to update them to something newer. + /// If \p gens is older, then just return those by reference, and directly return false (not + /// becoming the reader). + /// If \p gens is current and there is not a reader, then do not update \p gens and return true, + /// indicating we should become the reader. Now it is our responsibility to wait on the + /// semaphore and notify on a change via the condition variable. If \p gens is current, and + /// there is already a reader, then wait until the reader notifies us and try again. + fn try_update_gens_maybe_becoming_reader(&self, gens: &mut generation_list_t) -> bool { + let mut become_reader = false; + let mut data = self.data_.lock().unwrap(); + loop { + // See if the updated gen list has changed. If so we don't need to become the reader. + let current = self.updated_gens_in_data(&mut data); + // FLOG(topic_monitor, "TID", thread_id(), "local ", gens->describe(), ": current", + // current.describe()); + if *gens != current { + *gens = current; + break; + } + + // The generations haven't changed. Perhaps we become the reader. + // Note we still hold the lock, so this cannot race with any other thread becoming the + // reader. + if data.has_reader { + // We already have a reader, wait for it to notify us and loop again. + data = self.data_notifier_.wait(data).unwrap(); + continue; + } else { + // We will try to become the reader. + // Reader bit should not be set in this case. + assert!( + (self.status_.load(Ordering::Relaxed) & STATUS_NEEDS_WAKEUP) == 0, + "No thread should be waiting" + ); + // Try becoming the reader by marking the reader bit. + let expected_old: status_bits_t = 0; + if self + .status_ + .compare_exchange( + expected_old, + STATUS_NEEDS_WAKEUP, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .is_err() + { + // We failed to become the reader, perhaps because another topic post just arrived. + // Loop again. + continue; + } + // We successfully did a CAS from 0 -> STATUS_NEEDS_WAKEUP. + // Now any successive topic post must signal us. + //FLOG(topic_monitor, "TID", thread_id(), "becoming reader"); + become_reader = true; + data.has_reader = true; + break; + } + } + return become_reader; + } + + /// Wait for some entry in the list of generations to change. + /// \return the new gens. + fn await_gens(&self, input_gens: &generation_list_t) -> generation_list_t { + let mut gens = *input_gens; + while gens == *input_gens { + let become_reader = self.try_update_gens_maybe_becoming_reader(&mut gens); + if become_reader { + // Now we are the reader. Read from the pipe, and then update with any changes. + // Note we no longer hold the lock. + assert!( + gens == *input_gens, + "Generations should not have changed if we are the reader." + ); + + // Wait to be woken up. + self.sema_.wait(); + + // We are finished waiting. We must stop being the reader, and post on the condition + // variable to wake up any other threads waiting for us to finish reading. + let mut data = self.data_.lock().unwrap(); + gens = data.current; + // FLOG(topic_monitor, "TID", thread_id(), "local", input_gens.describe(), + // "read() complete, current is", gens.describe()); + assert!(data.has_reader, "We should be the reader"); + data.has_reader = false; + self.data_notifier_.notify_all(); + } + } + return gens; + } + + /// For each valid topic in \p gens, check to see if the current topic is larger than + /// the value in \p gens. + /// If \p wait is set, then wait if there are no changes; otherwise return immediately. + /// \return true if some topic changed, false if none did. + /// On a true return, this updates the generation list \p gens. + pub fn check(&self, gens: *mut generation_list_t, wait: bool) -> bool { + assert!(!gens.is_null(), "gens must not be null"); + let gens = unsafe { &mut *gens }; + if !gens.any_valid() { + return false; + } + + let mut current: generation_list_t = self.updated_gens(); + let mut changed = false; + loop { + // Load the topic list and see if anything has changed. + for topic in all_topics() { + if gens.is_valid(topic) { + assert!( + gens.at(topic) <= current.at(topic), + "Incoming gen count exceeded published count" + ); + if gens.at(topic) < current.at(topic) { + *gens.at_mut(topic) = current.at(topic); + changed = true; + } + } + } + + // If we're not waiting, or something changed, then we're done. + if !wait || changed { + break; + } + + // Wait until our gens change. + current = self.await_gens(¤t); + } + return changed; + } +} + +pub fn topic_monitor_init() { + topic_monitor_t::initialize(); +} + +pub fn topic_monitor_principal() -> &'static topic_monitor_t { + unsafe { + assert!( + !s_principal.is_null(), + "Principal topic monitor not initialized" + ); + &*s_principal + } +} diff --git a/fish-rust/src/wchar.rs b/fish-rust/src/wchar.rs new file mode 100644 index 000000000..855b8e16a --- /dev/null +++ b/fish-rust/src/wchar.rs @@ -0,0 +1,34 @@ +use crate::ffi; +pub use cxx::CxxWString; +pub use ffi::{wchar_t, wcharz_t}; +pub use widestring::utf32str; +pub use widestring::{Utf32Str as wstr, Utf32String as WString}; + +/// Support for wide strings. +/// There are two wide string types that are commonly used: +/// - wstr: a string slice without a nul terminator. Like `&str` but wide chars. +/// - WString: an owning string without a nul terminator. Like `String` but wide chars. + +/// Creates a wstr string slice, like the "L" prefix of C++. +/// The result is of type wstr. +/// It is NOT nul-terminated. +macro_rules! L { + ($string:literal) => { + widestring::utf32str!($string) + }; +} +pub(crate) use L; + +/// A proc-macro for creating wide string literals using an L *suffix*. +/// Example usage: +/// ``` +/// #[widestrs] +/// pub fn func() { +/// let s = "hello"L; // type &'static wstr +/// } +/// ``` +/// Note: the resulting string is NOT nul-terminated. +pub use widestring_suffix::widestrs; + +/// Pull in our extensions. +pub use crate::wchar_ext::{CharPrefixSuffix, WExt}; diff --git a/fish-rust/src/wchar_ext.rs b/fish-rust/src/wchar_ext.rs new file mode 100644 index 000000000..d31757d07 --- /dev/null +++ b/fish-rust/src/wchar_ext.rs @@ -0,0 +1,137 @@ +use crate::wchar::{wstr, WString}; +use widestring::utfstr::CharsUtf32; + +/// A thing that a wide string can start with or end with. +/// It must have a chars() method which returns a double-ended char iterator. +pub trait CharPrefixSuffix { + type Iter: DoubleEndedIterator; + fn chars(self) -> Self::Iter; +} + +impl CharPrefixSuffix for char { + type Iter = std::iter::Once; + fn chars(self) -> Self::Iter { + std::iter::once(self) + } +} + +impl<'a> CharPrefixSuffix for &'a str { + type Iter = std::str::Chars<'a>; + fn chars(self) -> Self::Iter { + str::chars(self) + } +} + +impl<'a> CharPrefixSuffix for &'a wstr { + type Iter = CharsUtf32<'a>; + fn chars(self) -> Self::Iter { + wstr::chars(self) + } +} + +impl<'a> CharPrefixSuffix for &'a WString { + type Iter = CharsUtf32<'a>; + fn chars(self) -> Self::Iter { + wstr::chars(&*self) + } +} + +/// \return true if \p prefix is a prefix of \p contents. +fn iter_prefixes_iter(mut prefix: Prefix, mut contents: Contents) -> bool +where + Prefix: Iterator, + Contents: Iterator, + Prefix::Item: PartialEq, +{ + while let Some(c1) = prefix.next() { + match contents.next() { + Some(c2) if c1 == c2 => {} + _ => return false, + } + } + true +} + +/// Convenience functions for WString. +pub trait WExt { + /// Access the chars of a WString or wstr. + fn as_char_slice(&self) -> &[char]; + + /// \return the char at an index. + /// If the index is equal to the length, return '\0'. + /// If the index exceeds the length, then panic. + fn char_at(&self, index: usize) -> char { + let chars = self.as_char_slice(); + if index == chars.len() { + '\0' + } else { + chars[index] + } + } + + /// \return the index of the first occurrence of the given char, or None. + fn find_char(&self, c: char) -> Option { + self.as_char_slice().iter().position(|&x| x == c) + } + + /// \return whether we start with a given Prefix. + /// The Prefix can be a char, a &str, a &wstr, or a &WString. + fn starts_with(&self, prefix: Prefix) -> bool { + iter_prefixes_iter(prefix.chars(), self.as_char_slice().iter().copied()) + } + + /// \return whether we end with a given Suffix. + /// The Suffix can be a char, a &str, a &wstr, or a &WString. + fn ends_with(&self, suffix: Suffix) -> bool { + iter_prefixes_iter( + suffix.chars().rev(), + self.as_char_slice().iter().copied().rev(), + ) + } +} + +impl WExt for WString { + fn as_char_slice(&self) -> &[char] { + self.as_utfstr().as_char_slice() + } +} + +impl WExt for wstr { + fn as_char_slice(&self) -> &[char] { + wstr::as_char_slice(self) + } +} + +#[cfg(test)] +mod tests { + use super::WExt; + use crate::wchar::{WString, L}; + /// Write some tests. + #[cfg(test)] + fn test_find_char() { + assert_eq!(Some(0), L!("abc").find_char('a')); + assert_eq!(Some(1), L!("abc").find_char('b')); + assert_eq!(None, L!("abc").find_char('X')); + assert_eq!(None, L!("").find_char('X')); + } + + #[cfg(test)] + fn test_prefix() { + assert!(L!("").starts_with(L!(""))); + assert!(L!("abc").starts_with(L!(""))); + assert!(L!("abc").starts_with('a')); + assert!(L!("abc").starts_with("ab")); + assert!(L!("abc").starts_with(L!("ab"))); + assert!(L!("abc").starts_with(&WString::from_str("abc"))); + } + + #[cfg(test)] + fn test_suffix() { + assert!(L!("").ends_with(L!(""))); + assert!(L!("abc").ends_with(L!(""))); + assert!(L!("abc").ends_with('c')); + assert!(L!("abc").ends_with("bc")); + assert!(L!("abc").ends_with(L!("bc"))); + assert!(L!("abc").ends_with(&WString::from_str("abc"))); + } +} diff --git a/fish-rust/src/wchar_ffi.rs b/fish-rust/src/wchar_ffi.rs new file mode 100644 index 000000000..cc4af96b2 --- /dev/null +++ b/fish-rust/src/wchar_ffi.rs @@ -0,0 +1,131 @@ +use crate::ffi; +pub use cxx::CxxWString; +pub use ffi::{wchar_t, wcharz_t}; +pub use widestring::U32CString as W0String; +pub use widestring::{u32cstr, utf32str}; +pub use widestring::{Utf32Str as wstr, Utf32String as WString}; + +/// We have the following string types for FFI purposes: +/// - CxxWString: the Rust view of a C++ wstring. +/// - W0String: an owning string with a nul terminator. +/// - wcharz_t: a "newtyped" pointer to a nul-terminated string, implemented in C++. +/// This is useful for FFI boundaries, to work around autocxx limitations on pointers. + +/// \return the length of a nul-terminated raw string. +pub fn wcslen(str: *const wchar_t) -> usize { + assert!(!str.is_null(), "Null pointer"); + let mut len = 0; + unsafe { + while *str.offset(len) != 0 { + len += 1; + } + } + len as usize +} + +impl wcharz_t { + /// \return the chars of a wcharz_t. + pub fn chars(&self) -> &[char] { + assert!(!self.str_.is_null(), "Null wcharz"); + let data = self.str_ as *const char; + let len = self.size(); + unsafe { std::slice::from_raw_parts(data, len) } + } +} + +/// Convert wcharz_t to an WString. +impl From<&wcharz_t> for WString { + fn from(wcharz: &wcharz_t) -> Self { + WString::from_chars(wcharz.chars()) + } +} + +/// Convert a wstr or WString to a W0String, which contains a nul-terminator. +/// This is useful for passing across FFI boundaries. +/// In general you don't need to use this directly - use the c_str macro below. +pub fn wstr_to_u32string>(str: Str) -> W0String { + W0String::from_ustr(str.as_ref()).expect("String contained intermediate NUL character") +} + +/// Convert a wstr to a nul-terminated pointer. +/// This needs to be a macro so we can create a temporary with the proper lifetime. +macro_rules! c_str { + ($string:expr) => { + crate::wchar_ffi::wstr_to_u32string($string) + .as_ucstr() + .as_ptr() + .cast::() + }; +} + +/// Convert a wstr to a wcharz_t. +macro_rules! wcharz { + ($string:expr) => { + crate::wchar::wcharz_t { + str_: crate::wchar_ffi::c_str!($string), + } + }; +} + +pub(crate) use c_str; +pub(crate) use wcharz; + +lazy_static! { + /// A shared, empty CxxWString. + static ref EMPTY_WSTRING: cxx::UniquePtr = cxx::CxxWString::create(&[]); +} + +/// \return a reference to a shared empty wstring. +pub fn empty_wstring() -> &'static cxx::CxxWString { + &EMPTY_WSTRING +} + +/// Implement Debug for wcharz_t. +impl std::fmt::Debug for wcharz_t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.str_.is_null() { + write!(f, "((null))") + } else { + self.chars().fmt(f) + } + } +} + +/// Convert self to a CxxWString, in preparation for using over FFI. +/// We can't use "From" as WString is implemented in an external crate. +pub trait WCharToFFI { + fn to_ffi(&self) -> cxx::UniquePtr; +} + +/// WString may be converted to CxxWString. +impl WCharToFFI for WString { + fn to_ffi(&self) -> cxx::UniquePtr { + cxx::CxxWString::create(self.as_char_slice()) + } +} + +/// wstr (wide string slices) may be converted to CxxWString. +impl WCharToFFI for wstr { + fn to_ffi(&self) -> cxx::UniquePtr { + cxx::CxxWString::create(self.as_char_slice()) + } +} + +/// wcharz_t (wide char) may be converted to CxxWString. +impl WCharToFFI for wcharz_t { + fn to_ffi(&self) -> cxx::UniquePtr { + cxx::CxxWString::create(self.chars()) + } +} + +/// Convert from a CxxWString, in preparation for using over FFI. +pub trait WCharFromFFI { + /// Convert from a CxxWString for FFI purposes. + fn from_ffi(&self) -> Target; +} + +impl WCharFromFFI for cxx::UniquePtr { + fn from_ffi(&self) -> WString { + WString::from_chars(self.as_chars()) + } +} diff --git a/fish-rust/src/wgetopt.rs b/fish-rust/src/wgetopt.rs new file mode 100644 index 000000000..c6a93ec75 --- /dev/null +++ b/fish-rust/src/wgetopt.rs @@ -0,0 +1,610 @@ +// A version of the getopt library for use with wide character strings. +// +/* Declarations for getopt. + Copyright (C) 1989, 90, 91, 92, 93, 94 Free Software Foundation, Inc. + +This file is part of the GNU C Library. Its master source is NOT part of +the C library, however. The master source lives in /gd/gnu/lib. + +The GNU C Library is free software; you can redistribute it and/or +modify it under the terms of the GNU Library General Public License as +published by the Free Software Foundation; either version 2 of the +License, or (at your option) any later version. + +The GNU C Library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Library General Public License for more details. + +You should have received a copy of the GNU Library General Public +License along with the GNU C Library; see the file COPYING.LIB. If +not, write to the Free Software Foundation, Inc., 675 Mass Ave, +Cambridge, MA 02139, USA. */ + +/// Note wgetopter expects an mutable array of const strings. It modifies the order of the +/// strings, but not their contents. +use crate::wchar::{utf32str, wstr, WExt}; + +// Describe how to deal with options that follow non-option ARGV-elements. +// +// If the caller did not specify anything, the default is PERMUTE. +// +// REQUIRE_ORDER means don't recognize them as options; stop option processing when the first +// non-option is seen. This is what Unix does. This mode of operation is selected by using `+' +// as the first character of the list of option characters. +// +// PERMUTE is the default. We permute the contents of ARGV as we scan, so that eventually all +// the non-options are at the end. This allows options to be given in any order, even with +// programs that were not written to expect this. +// +// RETURN_IN_ORDER is an option available to programs that were written to expect options and +// other ARGV-elements in any order and that care about the ordering of the two. We describe +// each non-option ARGV-element as if it were the argument of an option with character code 1. +// Using `-' as the first character of the list of option characters selects this mode of +// operation. +// +// The special argument `--' forces an end of option-scanning regardless of the value of +// `ordering'. In the case of RETURN_IN_ORDER, only `--' can cause `getopt' to return EOF with +// `woptind' != ARGC. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Ordering { + REQUIRE_ORDER, + PERMUTE, + RETURN_IN_ORDER, +} + +impl Default for Ordering { + fn default() -> Self { + Ordering::PERMUTE + } +} + +fn empty_wstr() -> &'static wstr { + Default::default() +} + +pub struct wgetopter_t<'opts, 'args, 'argarray> { + // Argv. + argv: &'argarray mut [&'args wstr], + + // For communication from `getopt' to the caller. When `getopt' finds an option that takes an + // argument, the argument value is returned here. Also, when `ordering' is RETURN_IN_ORDER, each + // non-option ARGV-element is returned here. + pub woptarg: Option<&'args wstr>, + + shortopts: &'opts wstr, + longopts: &'opts [woption<'opts>], + + // The next char to be scanned in the option-element in which the last option character we + // returned was found. This allows us to pick up the scan where we left off. + // + // If this is empty, it means resume the scan by advancing to the next ARGV-element. + nextchar: &'args wstr, + + // Index in ARGV of the next element to be scanned. This is used for communication to and from + // the caller and for communication between successive calls to `getopt'. + // + // On entry to `getopt', zero means this is the first call; initialize. + // + // When `getopt' returns EOF, this is the index of the first of the non-option elements that the + // caller should itself scan. + // + // Otherwise, `woptind' communicates from one call to the next how much of ARGV has been scanned + // so far. + + // XXX 1003.2 says this must be 1 before any call. + pub woptind: usize, + + // Set to an option character which was unrecognized. + woptopt: char, + + // Describe how to deal with options that follow non-option ARGV-elements. + ordering: Ordering, + + // Handle permutation of arguments. + + // Describe the part of ARGV that contains non-options that have been skipped. `first_nonopt' + // is the index in ARGV of the first of them; `last_nonopt' is the index after the last of them. + pub first_nonopt: usize, + pub last_nonopt: usize, + + missing_arg_return_colon: bool, + initialized: bool, +} + +// Names for the values of the `has_arg' field of `woption'. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum woption_argument_t { + no_argument, + required_argument, + optional_argument, +} + +/// Describe the long-named options requested by the application. The LONG_OPTIONS argument to +/// getopt_long or getopt_long_only is a vector of `struct option' terminated by an element +/// containing a name which is zero. +/// +/// The field `has_arg' is: +/// no_argument (or 0) if the option does not take an argument, +/// required_argument (or 1) if the option requires an argument, +/// optional_argument (or 2) if the option takes an optional argument. +/// +/// If the field `flag' is not NULL, it points to a variable that is set to the value given in the +/// field `val' when the option is found, but left unchanged if the option is not found. +/// +/// To have a long-named option do something other than set an `int' to a compiled-in constant, such +/// as set a value from `optarg', set the option's `flag' field to zero and its `val' field to a +/// nonzero value (the equivalent single-letter option character, if there is one). For long +/// options that have a zero `flag' field, `getopt' returns the contents of the `val' field. +#[derive(Debug, Clone, Copy)] +pub struct woption<'a> { + /// Long name for switch. + pub name: &'a wstr, + + pub has_arg: woption_argument_t, + + /// If \c flag is non-null, this is the value that flag will be set to. Otherwise, this is the + /// return-value of the function call. + pub val: char, +} + +/// Helper function to create a woption. +pub const fn wopt<'a>(name: &'a wstr, has_arg: woption_argument_t, val: char) -> woption<'a> { + woption { name, has_arg, val } +} + +impl<'opts, 'args, 'argarray> wgetopter_t<'opts, 'args, 'argarray> { + pub fn new( + shortopts: &'opts wstr, + longopts: &'opts [woption], + argv: &'argarray mut [&'args wstr], + ) -> Self { + return wgetopter_t { + woptopt: '?', + argv, + shortopts, + longopts, + first_nonopt: 0, + initialized: false, + last_nonopt: 0, + missing_arg_return_colon: false, + nextchar: Default::default(), + ordering: Ordering::PERMUTE, + woptarg: None, + woptind: 0, + }; + } + + pub fn wgetopt_long(&mut self) -> Option { + assert!(self.woptind <= self.argc(), "woptind is out of range"); + let mut ignored = 0; + return self._wgetopt_internal(&mut ignored, false); + } + + pub fn wgetopt_long_idx(&mut self, opt_index: &mut usize) -> Option { + return self._wgetopt_internal(opt_index, false); + } + + /// \return the number of arguments. + fn argc(&self) -> usize { + return self.argv.len(); + } + + // Exchange two adjacent subsequences of ARGV. One subsequence is elements + // [first_nonopt,last_nonopt) which contains all the non-options that have been skipped so far. The + // other is elements [last_nonopt,woptind), which contains all the options processed since those + // non-options were skipped. + // + // `first_nonopt' and `last_nonopt' are relocated so that they describe the new indices of the + // non-options in ARGV after they are moved. + fn exchange(&mut self) { + let mut bottom = self.first_nonopt; + let middle = self.last_nonopt; + let mut top = self.woptind; + + // Exchange the shorter segment with the far end of the longer segment. That puts the shorter + // segment into the right place. It leaves the longer segment in the right place overall, but it + // consists of two parts that need to be swapped next. + while top > middle && middle > bottom { + if top - middle > middle - bottom { + // Bottom segment is the short one. + let len = middle - bottom; + + // Swap it with the top part of the top segment. + for i in 0..len { + self.argv.swap(bottom + i, top - (middle - bottom) + i); + } + // Exclude the moved bottom segment from further swapping. + top -= len; + } else { + // Top segment is the short one. + let len = top - middle; + + // Swap it with the bottom part of the bottom segment. + for i in 0..len { + self.argv.swap(bottom + i, middle + i); + } + // Exclude the moved top segment from further swapping. + bottom += len; + } + } + + // Update records for the slots the non-options now occupy. + self.first_nonopt += self.woptind - self.last_nonopt; + self.last_nonopt = self.woptind; + } + + // Initialize the internal data when the first call is made. + fn _wgetopt_initialize(&mut self) { + // Start processing options with ARGV-element 1 (since ARGV-element 0 is the program name); the + // sequence of previously skipped non-option ARGV-elements is empty. + self.first_nonopt = 1; + self.last_nonopt = 1; + self.woptind = 1; + self.nextchar = empty_wstr(); + + let mut optstring = self.shortopts; + + // Determine how to handle the ordering of options and nonoptions. + if optstring.char_at(0) == '-' { + self.ordering = Ordering::RETURN_IN_ORDER; + optstring = &optstring[1..]; + } else if optstring.char_at(0) == '+' { + self.ordering = Ordering::REQUIRE_ORDER; + optstring = &optstring[1..]; + } else { + self.ordering = Ordering::PERMUTE; + } + + if optstring.char_at(0) == ':' { + self.missing_arg_return_colon = true; + optstring = &optstring[1..]; + } + + self.shortopts = optstring; + self.initialized = true; + } + + // Advance to the next ARGV-element. + // \return Some(\0) on success, or None or another value if we should stop. + fn _advance_to_next_argv(&mut self) -> Option { + let argc = self.argc(); + if self.ordering == Ordering::PERMUTE { + // If we have just processed some options following some non-options, exchange them so + // that the options come first. + if self.first_nonopt != self.last_nonopt && self.last_nonopt != self.woptind { + self.exchange(); + } else if self.last_nonopt != self.woptind { + self.first_nonopt = self.woptind; + } + + // Skip any additional non-options and extend the range of non-options previously + // skipped. + while self.woptind < argc + && (self.argv[self.woptind].char_at(0) != '-' || self.argv[self.woptind].len() == 1) + { + self.woptind += 1; + } + self.last_nonopt = self.woptind; + } + + // The special ARGV-element `--' means premature end of options. Skip it like a null option, + // then exchange with previous non-options as if it were an option, then skip everything + // else like a non-option. + if self.woptind != argc && self.argv[self.woptind] == "--" { + self.woptind += 1; + + if self.first_nonopt != self.last_nonopt && self.last_nonopt != self.woptind { + self.exchange(); + } else if self.first_nonopt == self.last_nonopt { + self.first_nonopt = self.woptind; + } + self.last_nonopt = argc; + self.woptind = argc; + } + + // If we have done all the ARGV-elements, stop the scan and back over any non-options that + // we skipped and permuted. + + if self.woptind == argc { + // Set the next-arg-index to point at the non-options that we previously skipped, so the + // caller will digest them. + if self.first_nonopt != self.last_nonopt { + self.woptind = self.first_nonopt; + } + return None; + } + + // If we have come to a non-option and did not permute it, either stop the scan or describe + // it to the caller and pass it by. + if self.argv[self.woptind].char_at(0) != '-' || self.argv[self.woptind].len() == 1 { + if self.ordering == Ordering::REQUIRE_ORDER { + return None; + } + self.woptarg = Some(self.argv[self.woptind]); + self.woptind += 1; + return Some(char::from(1)); + } + + // We have found another option-ARGV-element. Skip the initial punctuation. + let skip = if !self.longopts.is_empty() && self.argv[self.woptind].char_at(1) == '-' { + 2 + } else { + 1 + }; + self.nextchar = self.argv[self.woptind][skip..].into(); + return Some(char::from(0)); + } + + // Check for a matching short opt. + fn _handle_short_opt(&mut self) -> char { + // Look at and handle the next short option-character. + let mut c = self.nextchar.char_at(0); + self.nextchar = &self.nextchar[1..]; + + let temp = match self.shortopts.chars().position(|sc| sc == c) { + Some(pos) => &self.shortopts[pos..], + None => utf32str!(""), + }; + + // Increment `woptind' when we start to process its last character. + if self.nextchar.is_empty() { + self.woptind += 1; + } + + if temp.is_empty() || c == ':' { + self.woptopt = c; + + if !self.nextchar.is_empty() { + self.woptind += 1; + } + return '?'; + } + + if temp.char_at(1) != ':' { + return c; + } + + if temp.char_at(2) == ':' { + // This is an option that accepts an argument optionally. + if !self.nextchar.is_empty() { + self.woptarg = Some(self.nextchar.clone()); + self.woptind += 1; + } else { + self.woptarg = None; + } + self.nextchar = empty_wstr(); + } else { + // This is an option that requires an argument. + if !self.nextchar.is_empty() { + self.woptarg = Some(self.nextchar.clone()); + // If we end this ARGV-element by taking the rest as an arg, we must advance to + // the next element now. + self.woptind += 1; + } else if self.woptind == self.argc() { + self.woptopt = c; + c = if self.missing_arg_return_colon { + ':' + } else { + '?' + }; + } else { + // We already incremented `woptind' once; increment it again when taking next + // ARGV-elt as argument. + self.woptarg = Some(self.argv[self.woptind]); + self.woptind += 1; + } + self.nextchar = empty_wstr(); + } + + return c; + } + + fn _update_long_opt( + &mut self, + pfound: &woption, + nameend: usize, + longind: &mut usize, + option_index: usize, + retval: &mut char, + ) { + self.woptind += 1; + assert!(self.nextchar.char_at(nameend) == '\0' || self.nextchar.char_at(nameend) == '='); + if self.nextchar.char_at(nameend) == '=' { + if pfound.has_arg != woption_argument_t::no_argument { + self.woptarg = Some(self.nextchar[(nameend + 1)..].into()); + } else { + self.nextchar = empty_wstr(); + *retval = '?'; + return; + } + } else if pfound.has_arg == woption_argument_t::required_argument { + if self.woptind < self.argc() { + self.woptarg = Some(self.argv[self.woptind]); + self.woptind += 1; + } else { + self.nextchar = empty_wstr(); + *retval = if self.missing_arg_return_colon { + ':' + } else { + '?' + }; + return; + } + } + + self.nextchar = empty_wstr(); + *longind = option_index; + *retval = pfound.val; + } + + // Find a matching long opt. + fn _find_matching_long_opt( + &self, + nameend: usize, + exact: &mut bool, + ambig: &mut bool, + indfound: &mut usize, + ) -> Option> { + let mut pfound: Option = None; + let mut option_index = 0; + + // Test all long options for either exact match or abbreviated matches. + for p in self.longopts.iter() { + if p.name.starts_with(&self.nextchar[..nameend]) { + // Exact match found. + pfound = Some(*p); + *indfound = option_index; + *exact = true; + break; + } else if pfound.is_none() { + // First nonexact match found. + pfound = Some(*p); + *indfound = option_index; + } else { + // Second or later nonexact match found. + *ambig = true; + } + option_index += 1; + } + return pfound; + } + + // Check for a matching long opt. + fn _handle_long_opt( + &mut self, + longind: &mut usize, + long_only: bool, + retval: &mut char, + ) -> bool { + let mut exact = false; + let mut ambig = false; + let mut indfound: usize = 0; + + let mut nameend = 0; + while self.nextchar.char_at(nameend) != '\0' && self.nextchar.char_at(nameend) != '=' { + nameend += 1; + } + + let pfound = self._find_matching_long_opt(nameend, &mut exact, &mut ambig, &mut indfound); + + if ambig && !exact { + self.nextchar = empty_wstr(); + self.woptind += 1; + *retval = '?'; + return true; + } + + if let Some(pfound) = pfound { + self._update_long_opt(&pfound, nameend, longind, indfound, retval); + return true; + } + + // Can't find it as a long option. If this is not getopt_long_only, or the option starts + // with '--' or is not a valid short option, then it's an error. Otherwise interpret it as a + // short option. + if !long_only + || self.argv[self.woptind].char_at(1) == '-' + || !self + .shortopts + .as_char_slice() + .contains(&self.nextchar.char_at(0)) + { + self.nextchar = empty_wstr(); + self.woptind += 1; + *retval = '?'; + return true; + } + + return false; + } + + // Scan elements of ARGV (whose length is ARGC) for option characters given in OPTSTRING. + // + // If an element of ARGV starts with '-', and is not exactly "-" or "--", then it is an option + // element. The characters of this element (aside from the initial '-') are option characters. If + // `getopt' is called repeatedly, it returns successively each of the option characters from each of + // the option elements. + // + // If `getopt' finds another option character, it returns that character, updating `woptind' and + // `nextchar' so that the next call to `getopt' can resume the scan with the following option + // character or ARGV-element. + // + // If there are no more option characters, `getopt' returns `EOF'. Then `woptind' is the index in + // ARGV of the first ARGV-element that is not an option. (The ARGV-elements have been permuted so + // that those that are not options now come last.) + // + // OPTSTRING is a string containing the legitimate option characters. If an option character is seen + // that is not listed in OPTSTRING, return '?'. + // + // If a char in OPTSTRING is followed by a colon, that means it wants an arg, so the following text + // in the same ARGV-element, or the text of the following ARGV-element, is returned in `optarg'. + // Two colons mean an option that wants an optional arg; if there is text in the current + // ARGV-element, it is returned in `w.woptarg', otherwise `w.woptarg' is set to zero. + // + // If OPTSTRING starts with `-' or `+', it requests different methods of handling the non-option + // ARGV-elements. See the comments about RETURN_IN_ORDER and REQUIRE_ORDER, above. + // + // Long-named options begin with `--' instead of `-'. Their names may be abbreviated as long as the + // abbreviation is unique or is an exact match for some defined option. If they have an argument, + // it follows the option name in the same ARGV-element, separated from the option name by a `=', or + // else the in next ARGV-element. When `getopt' finds a long-named option, it returns 0 if that + // option's `flag' field is nonzero, the value of the option's `val' field if the `flag' field is + // zero. + // + // LONGOPTS is a vector of `struct option' terminated by an element containing a name which is zero. + // + // LONGIND returns the index in LONGOPT of the long-named option found. It is only valid when a + // long-named option has been found by the most recent call. + // + // If LONG_ONLY is nonzero, '-' as well as '--' can introduce long-named options. + fn _wgetopt_internal(&mut self, longind: &mut usize, long_only: bool) -> Option { + if !self.initialized { + self._wgetopt_initialize(); + } + self.woptarg = None; + + if self.nextchar.is_empty() { + let narg = self._advance_to_next_argv(); + if narg != Some(char::from(0)) { + return narg; + } + } + + // Decode the current option-ARGV-element. + + // Check whether the ARGV-element is a long option. + // + // If long_only and the ARGV-element has the form "-f", where f is a valid short option, don't + // consider it an abbreviated form of a long option that starts with f. Otherwise there would + // be no way to give the -f short option. + // + // On the other hand, if there's a long option "fubar" and the ARGV-element is "-fu", do + // consider that an abbreviation of the long option, just like "--fu", and not "-f" with arg + // "u". + // + // This distinction seems to be the most useful approach. + if !self.longopts.is_empty() && self.woptind < self.argc() { + let arg = self.argv[self.woptind]; + let mut try_long = false; + if arg.char_at(0) == '-' && arg.char_at(1) == '-' { + // Like --foo + try_long = true; + } else if long_only && arg.len() >= 3 { + // Like -fu + try_long = true; + } else if !self.shortopts.as_char_slice().contains(&arg.char_at(1)) { + // Like -f, but f is not a short arg. + try_long = true; + } + if try_long { + let mut retval = '\0'; + if self._handle_long_opt(longind, long_only, &mut retval) { + return Some(retval); + } + } + } + + return Some(self._handle_short_opt()); + } +} diff --git a/fish-rust/widestring-suffix/Cargo.lock b/fish-rust/widestring-suffix/Cargo.lock new file mode 100644 index 000000000..f5e974052 --- /dev/null +++ b/fish-rust/widestring-suffix/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "widestring-suffix" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/fish-rust/widestring-suffix/Cargo.toml b/fish-rust/widestring-suffix/Cargo.toml new file mode 100644 index 000000000..d756a5b17 --- /dev/null +++ b/fish-rust/widestring-suffix/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "widestring-suffix" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0", features = ["full", "visit-mut"] } +proc-macro2 = "1.0" +quote = "1.0" diff --git a/fish-rust/widestring-suffix/src/lib.rs b/fish-rust/widestring-suffix/src/lib.rs new file mode 100644 index 000000000..4162e7274 --- /dev/null +++ b/fish-rust/widestring-suffix/src/lib.rs @@ -0,0 +1,51 @@ +extern crate proc_macro as pm; + +use proc_macro2::{Group, Literal, TokenStream, TokenTree}; +use quote::quote_spanned; +use syn::{Lit, LitStr}; + +/// A proc macro which allows easy creation of nul-terminated wide strings. +/// It replaces strings with an L suffix like so: +/// "foo"L +/// with a call like so: +/// crate::wchar::L!("foo") +#[proc_macro_attribute] +pub fn widestrs(_attr: pm::TokenStream, input: pm::TokenStream) -> pm::TokenStream { + let s = widen_stream(input.into()); + s.into() +} + +fn widen_token_tree(tt: TokenTree) -> TokenStream { + match tt { + TokenTree::Group(group) => { + let wide_stream = widen_stream(group.stream()); + TokenTree::Group(Group::new(group.delimiter(), wide_stream)).into() + } + TokenTree::Literal(lit) => widen_literal(lit), + tt => tt.into(), + } +} + +fn widen_stream(input: TokenStream) -> TokenStream { + input.into_iter().map(widen_token_tree).collect() +} + +fn try_parse_literal(tt: TokenTree) -> Option { + let ts: TokenStream = tt.into(); + match syn::parse2::(ts) { + Ok(Lit::Str(lit)) => Some(lit), + _ => None, + } +} + +fn widen_literal(lit: Literal) -> TokenStream { + let tt = TokenTree::Literal(lit); + match try_parse_literal(tt.clone()) { + Some(lit) if lit.suffix() == "L" => { + let value = lit.value(); + let span = lit.span(); + quote_spanned!(span=> crate::wchar::L!(#value)).into() + } + _ => tt.into(), + } +} diff --git a/fish-rust/widestring-suffix/tests/test.rs b/fish-rust/widestring-suffix/tests/test.rs new file mode 100644 index 000000000..eb11e1b72 --- /dev/null +++ b/fish-rust/widestring-suffix/tests/test.rs @@ -0,0 +1,24 @@ +use widestring_suffix::widestrs; + +mod wchar { + macro_rules! L { + ($string:expr) => { + 42 + }; + } + + pub(crate) use L; +} + +#[widestrs] +mod stuff { + pub fn test1() { + let s = "abc"L; + assert_eq!(s, 42); + } +} + +#[test] +fn test_widestring() { + stuff::test1(); +} diff --git a/src/builtins/function.cpp b/src/builtins/function.cpp index 54672da4e..1eca5b2ef 100644 --- a/src/builtins/function.cpp +++ b/src/builtins/function.cpp @@ -27,7 +27,7 @@ #include "../parser.h" #include "../parser_keywords.h" #include "../proc.h" -#include "../signal.h" +#include "../signals.h" #include "../wait_handle.h" #include "../wgetopt.h" #include "../wutil.h" // IWYU pragma: keep diff --git a/src/builtins/wait.cpp b/src/builtins/wait.cpp index 4f9ad0898..b8bbcfed0 100644 --- a/src/builtins/wait.cpp +++ b/src/builtins/wait.cpp @@ -19,7 +19,7 @@ #include "../maybe.h" #include "../parser.h" #include "../proc.h" -#include "../signal.h" +#include "../signals.h" #include "../topic_monitor.h" #include "../wait_handle.h" #include "../wgetopt.h" diff --git a/src/common.cpp b/src/common.cpp index a1cc7c63c..9800b3070 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -39,7 +39,7 @@ #include "future_feature_flags.h" #include "global_safety.h" #include "iothread.h" -#include "signal.h" +#include "signals.h" #include "termsize.h" #include "topic_monitor.h" #include "wcstringutil.h" @@ -1061,9 +1061,7 @@ static wcstring escape_string_pcre2(const wcstring &in) { case L'-': case L']': out.push_back('\\'); - __fallthrough__ - default: - out.push_back(c); + __fallthrough__ default : out.push_back(c); } } @@ -1225,8 +1223,8 @@ maybe_t read_unquoted_escape(const wchar_t *input, wcstring *result, boo // that are valid on their own, which is true for UTF-8) byte_buff.push_back(static_cast(res)); result_char_or_none = none(); - if (input[in_pos] == L'\\' - && (input[in_pos + 1] == L'X' || input[in_pos + 1] == L'x')) { + if (input[in_pos] == L'\\' && + (input[in_pos + 1] == L'X' || input[in_pos + 1] == L'x')) { in_pos++; continue; } diff --git a/src/common.h b/src/common.h index 0ba47f3bc..c30ac2c0a 100644 --- a/src/common.h +++ b/src/common.h @@ -342,7 +342,7 @@ void format_ullong_safe(wchar_t buff[64], unsigned long long val); void narrow_string_safe(char buff[64], const wchar_t *s); /// Stored in blocks to reference the file which created the block. -using filename_ref_t = std::shared_ptr; +using filename_ref_t = std::shared_ptr; using scoped_lock = std::lock_guard; @@ -446,15 +446,16 @@ wcstring vformat_string(const wchar_t *format, va_list va_orig); void append_format(wcstring &str, const wchar_t *format, ...); void append_formatv(wcstring &target, const wchar_t *format, va_list va_orig); -#ifdef HAVE_STD__MAKE_UNIQUE -using std::make_unique; -#else +#ifndef HAVE_STD__MAKE_UNIQUE /// make_unique implementation +namespace std { template std::unique_ptr make_unique(Args &&...args) { return std::unique_ptr(new T(std::forward(args)...)); } +} // namespace std #endif +using std::make_unique; /// This functions returns the end of the quoted substring beginning at \c pos. Returns 0 on error. /// diff --git a/src/env_universal_common.cpp b/src/env_universal_common.cpp index 4bdd1c372..db50120b5 100644 --- a/src/env_universal_common.cpp +++ b/src/env_universal_common.cpp @@ -37,6 +37,7 @@ #include "env.h" #include "env_universal_common.h" #include "fallback.h" // IWYU pragma: keep +#include "fd_readable_set.rs.h" #include "flog.h" #include "path.h" #include "utf8.h" @@ -1335,7 +1336,7 @@ class universal_notifier_named_pipe_t final : public universal_notifier_t { // If we're no longer readable, go back to wait mode. // Conversely, if we have been readable too long, perhaps some fish died while its // written data was still on the pipe; drain some. - if (!fd_readable_set_t::poll_fd_readable(pipe_fd.fd())) { + if (!poll_fd_readable(pipe_fd.fd())) { set_state(waiting_for_readable); } else if (get_time() >= state_start_usec + k_readable_too_long_duration_usec) { drain_excess(); @@ -1355,7 +1356,7 @@ class universal_notifier_named_pipe_t final : public universal_notifier_t { // change occurred with ours. if (get_time() >= state_start_usec + k_flash_duration_usec) { drain_written(); - if (!fd_readable_set_t::poll_fd_readable(pipe_fd.fd())) { + if (!poll_fd_readable(pipe_fd.fd())) { set_state(waiting_for_readable); } else { set_state(polling_during_readable); diff --git a/src/event.cpp b/src/event.cpp index fe2669230..a0b2e8c34 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -20,7 +20,7 @@ #include "maybe.h" #include "parser.h" #include "proc.h" -#include "signal.h" +#include "signals.h" #include "termsize.h" #include "wcstringutil.h" #include "wutil.h" // IWYU pragma: keep diff --git a/src/fd_monitor.cpp b/src/fd_monitor.cpp index 6d6934014..7f932c53f 100644 --- a/src/fd_monitor.cpp +++ b/src/fd_monitor.cpp @@ -116,7 +116,8 @@ bool fd_monitor_item_t::poke_item(const poke_list_t &pokelist) { void fd_monitor_t::run_in_background() { ASSERT_IS_BACKGROUND_THREAD(); poke_list_t pokelist; - fd_readable_set_t fds; + auto fds_box = new_fd_readable_set(); + auto &fds = *fds_box; for (;;) { // Poke any items that need it. if (!pokelist.empty()) { @@ -131,7 +132,7 @@ void fd_monitor_t::run_in_background() { fds.add(change_signal_fd); auto now = std::chrono::steady_clock::now(); - uint64_t timeout_usec = fd_monitor_item_t::kNoTimeout; + uint64_t timeout_usec = kNoTimeout; for (auto &item : items_) { fds.add(item.fd.fd()); @@ -145,8 +146,7 @@ void fd_monitor_t::run_in_background() { // We refer to this as the wait-lap. bool is_wait_lap = (items_.size() == 0); if (is_wait_lap) { - assert(timeout_usec == fd_monitor_item_t::kNoTimeout && - "Should not have a timeout on wait-lap"); + assert(timeout_usec == kNoTimeout && "Should not have a timeout on wait-lap"); timeout_usec = 256 * kUsecPerMsec; } diff --git a/src/fd_monitor.h b/src/fd_monitor.h index 6b4005a68..311606940 100644 --- a/src/fd_monitor.h +++ b/src/fd_monitor.h @@ -11,6 +11,7 @@ #include // IWYU pragma: keep #include "common.h" +#include "fd_readable_set.rs.h" #include "fds.h" #include "maybe.h" @@ -33,9 +34,6 @@ struct fd_monitor_item_t { /// The callback may close \p fd, in which case the item is removed. using callback_t = std::function; - /// A sentinel value meaning no timeout. - static constexpr uint64_t kNoTimeout = fd_readable_set_t::kNoTimeout; - /// The fd to monitor. autoclose_fd_t fd{}; diff --git a/src/fds.cpp b/src/fds.cpp index 0dbae2eb3..225b6b7b4 100644 --- a/src/fds.cpp +++ b/src/fds.cpp @@ -29,109 +29,6 @@ void autoclose_fd_t::close() { fd_ = -1; } -fd_readable_set_t::fd_readable_set_t() { clear(); } - -#if FISH_READABLE_SET_USE_POLL - -// Convert from a usec to a poll-friendly msec. -static int usec_to_poll_msec(uint64_t timeout_usec) { - uint64_t timeout_msec = timeout_usec / kUsecPerMsec; - // Round to nearest, down for halfway. - timeout_msec += ((timeout_usec % kUsecPerMsec) > kUsecPerMsec / 2) ? 1 : 0; - if (timeout_usec == fd_readable_set_t::kNoTimeout || - timeout_msec > std::numeric_limits::max()) { - // Negative values mean wait forever in poll-speak. - return -1; - } - return static_cast(timeout_msec); -} - -void fd_readable_set_t::clear() { pollfds_.clear(); } - -static inline bool pollfd_less_than(const pollfd &lhs, int rhs) { return lhs.fd < rhs; } - -void fd_readable_set_t::add(int fd) { - if (fd >= 0) { - auto where = std::lower_bound(pollfds_.begin(), pollfds_.end(), fd, pollfd_less_than); - if (where == pollfds_.end() || where->fd != fd) { - pollfds_.insert(where, pollfd{fd, POLLIN, 0}); - } - } -} - -bool fd_readable_set_t::test(int fd) const { - // If a pipe is widowed with no data, Linux sets POLLHUP but not POLLIN, so test for both. - auto where = std::lower_bound(pollfds_.begin(), pollfds_.end(), fd, pollfd_less_than); - return where != pollfds_.end() && where->fd == fd && (where->revents & (POLLIN | POLLHUP)); -} - -// static -int fd_readable_set_t::do_poll(struct pollfd *fds, size_t count, uint64_t timeout_usec) { - assert(count <= std::numeric_limits::max() && "count too big"); - return ::poll(fds, static_cast(count), usec_to_poll_msec(timeout_usec)); -} - -int fd_readable_set_t::check_readable(uint64_t timeout_usec) { - if (pollfds_.empty()) return 0; - return do_poll(&pollfds_[0], pollfds_.size(), timeout_usec); -} - -// static -bool fd_readable_set_t::is_fd_readable(int fd, uint64_t timeout_usec) { - if (fd < 0) return false; - struct pollfd pfd { - fd, POLLIN, 0 - }; - int ret = fd_readable_set_t::do_poll(&pfd, 1, timeout_usec); - return ret > 0 && (pfd.revents & POLLIN); -} - -#else -// Implementation based on select(). - -void fd_readable_set_t::clear() { - FD_ZERO(&fdset_); - nfds_ = 0; -} - -void fd_readable_set_t::add(int fd) { - if (fd >= FD_SETSIZE) { - FLOGF(error, "fd %d too large for select()", fd); - return; - } - if (fd >= 0) { - FD_SET(fd, &fdset_); - nfds_ = std::max(nfds_, fd + 1); - } -} - -bool fd_readable_set_t::test(int fd) const { return fd >= 0 && FD_ISSET(fd, &fdset_); } - -int fd_readable_set_t::check_readable(uint64_t timeout_usec) { - if (timeout_usec == kNoTimeout) { - return ::select(nfds_, &fdset_, nullptr, nullptr, nullptr); - } else { - struct timeval tvs; - tvs.tv_sec = timeout_usec / kUsecPerSec; - tvs.tv_usec = timeout_usec % kUsecPerSec; - return ::select(nfds_, &fdset_, nullptr, nullptr, &tvs); - } -} - -// static -bool fd_readable_set_t::is_fd_readable(int fd, uint64_t timeout_usec) { - if (fd < 0) return false; - fd_readable_set_t s; - s.add(fd); - int res = s.check_readable(timeout_usec); - return res > 0 && s.test(fd); -} - -#endif // not FISH_READABLE_SET_USE_POLL - -// static -bool fd_readable_set_t::poll_fd_readable(int fd) { return is_fd_readable(fd, 0); } - #ifdef HAVE_EVENTFD // Note we do not want to use EFD_SEMAPHORE because we are binary (not counting) semaphore. fd_event_signaller_t::fd_event_signaller_t() { @@ -284,6 +181,15 @@ maybe_t make_autoclose_pipes() { return autoclose_pipes_t(std::move(read_end), std::move(write_end)); } +pipes_ffi_t make_pipes_ffi() { + pipes_ffi_t res = {-1, -1}; + if (auto pipes = make_autoclose_pipes()) { + res.read = pipes->read.acquire(); + res.write = pipes->write.acquire(); + } + return res; +} + int set_cloexec(int fd, bool should_set) { // Note we don't want to overwrite existing flags like O_NONBLOCK which may be set. So fetch the // existing flags and modify them. diff --git a/src/fds.h b/src/fds.h index 0b315eb95..0f5b508ce 100644 --- a/src/fds.h +++ b/src/fds.h @@ -24,6 +24,9 @@ /// (like >&5). extern const int k_first_high_fd; +/// A sentinel value indicating no timeout. +#define kNoTimeout (std::numeric_limits::max()) + /// A helper class for managing and automatically closing a file descriptor. class autoclose_fd_t : noncopyable_t { int fd_; @@ -63,62 +66,6 @@ class autoclose_fd_t : noncopyable_t { ~autoclose_fd_t() { close(); } }; -// Resolve whether to use poll() or select(). -#ifndef FISH_READABLE_SET_USE_POLL -#ifdef __APPLE__ -// Apple's `man poll`: "The poll() system call currently does not support devices." -#define FISH_READABLE_SET_USE_POLL 0 -#else -// Use poll other places so we can support unlimited fds. -#define FISH_READABLE_SET_USE_POLL 1 -#endif -#endif - -/// A modest wrapper around select() or poll(), according to FISH_READABLE_SET_USE_POLL. -/// This allows accumulating a set of fds and then seeing if they are readable. -/// This only handles readability. -struct fd_readable_set_t { - /// Construct an empty set. - fd_readable_set_t(); - - /// Reset back to an empty set. - void clear(); - - /// Add an fd to the set. The fd is ignored if negative (for convenience). - void add(int fd); - - /// \return true if the given fd is marked as set, in our set. \returns false if negative. - bool test(int fd) const; - - /// Call select() or poll(), according to FISH_READABLE_SET_USE_POLL. Note this destructively - /// modifies the set. \return the result of select() or poll(). - int check_readable(uint64_t timeout_usec = fd_readable_set_t::kNoTimeout); - - /// Check if a single fd is readable, with a given timeout. - /// \return true if readable, false if not. - static bool is_fd_readable(int fd, uint64_t timeout_usec); - - /// Check if a single fd is readable, without blocking. - /// \return true if readable, false if not. - static bool poll_fd_readable(int fd); - - /// A special timeout value which may be passed to indicate no timeout. - static constexpr uint64_t kNoTimeout = std::numeric_limits::max(); - - private: -#if FISH_READABLE_SET_USE_POLL - // Our list of FDs, sorted by fd. - std::vector pollfds_{}; - - // Helper function. - static int do_poll(struct pollfd *fds, size_t count, uint64_t timeout_usec); -#else - // The underlying fdset and nfds value to pass to select(). - fd_set fdset_; - int nfds_{0}; -#endif -}; - /// Helper type returned from making autoclose pipes. struct autoclose_pipes_t { /// Read end of the pipe. @@ -137,6 +84,14 @@ struct autoclose_pipes_t { /// \return pipes on success, none() on error. maybe_t make_autoclose_pipes(); +/// Create pipes. +/// Upon failure both values will be negative. +struct pipes_ffi_t { + int read; + int write; +}; +pipes_ffi_t make_pipes_ffi(); + /// An event signaller implemented using a file descriptor, so it can plug into select(). /// This is like a binary semaphore. A call to post() will signal an event, making the fd readable. /// Multiple calls to post() may be coalesced. On Linux this uses eventfd(); on other systems this diff --git a/src/fish.cpp b/src/fish.cpp index 56dba892e..b2c3641a5 100644 --- a/src/fish.cpp +++ b/src/fish.cpp @@ -39,11 +39,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #include "ast.h" #include "common.h" +#include "cxxgen.h" #include "env.h" #include "event.h" #include "expand.h" #include "fallback.h" // IWYU pragma: keep #include "fds.h" +#include "ffi_init.rs.h" #include "fish_version.h" #include "flog.h" #include "function.h" @@ -59,7 +61,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #include "path.h" #include "proc.h" #include "reader.h" -#include "signal.h" +#include "signals.h" #include "wcstringutil.h" #include "wutil.h" // IWYU pragma: keep @@ -319,6 +321,7 @@ static int fish_parse_opt(int argc, char **argv, fish_cmd_opts_t *opts) { } case 'd': { activate_flog_categories_by_pattern(str2wcstring(optarg)); + rust_activate_flog_categories_by_pattern(str2wcstring(optarg).c_str()); for (auto cat : get_flog_categories()) { if (cat->enabled) { std::fwprintf(stdout, L"Debug enabled for category: %ls\n", cat->name); @@ -427,6 +430,7 @@ int main(int argc, char **argv) { program_name = L"fish"; set_main_thread(); setup_fork_guards(); + rust_init(); signal_unblock_all(); setlocale(LC_ALL, ""); diff --git a/src/fish_key_reader.cpp b/src/fish_key_reader.cpp index db3c1ac93..1e1cb79ba 100644 --- a/src/fish_key_reader.cpp +++ b/src/fish_key_reader.cpp @@ -20,8 +20,10 @@ #include #include "common.h" +#include "cxxgen.h" #include "env.h" #include "fallback.h" // IWYU pragma: keep +#include "ffi_init.rs.h" #include "fish_version.h" #include "input.h" #include "input_common.h" @@ -30,7 +32,7 @@ #include "print_help.h" #include "proc.h" #include "reader.h" -#include "signal.h" +#include "signals.h" #include "wutil.h" // IWYU pragma: keep struct config_paths_t determine_config_directory_paths(const char *argv0); @@ -271,6 +273,7 @@ static void process_input(bool continuous_mode, bool verbose) { set_interactive_session(true); set_main_thread(); setup_fork_guards(); + rust_init(); env_init(); reader_init(); parser_t &parser = parser_t::principal_parser(); diff --git a/src/fish_test_helper.cpp b/src/fish_test_helper.cpp index 06689eeca..dbb7390f9 100644 --- a/src/fish_test_helper.cpp +++ b/src/fish_test_helper.cpp @@ -2,6 +2,7 @@ // programs, allowing fish to test its behavior. #include +#include #include #include diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index ac7a59187..bdc2c4d68 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -53,13 +53,16 @@ #include "color.h" #include "common.h" #include "complete.h" +#include "cxxgen.h" #include "enum_set.h" #include "env.h" #include "env_universal_common.h" #include "expand.h" #include "fallback.h" // IWYU pragma: keep #include "fd_monitor.h" +#include "fd_readable_set.rs.h" #include "fds.h" +#include "ffi_init.rs.h" #include "function.h" #include "future_feature_flags.h" #include "global_safety.h" @@ -85,7 +88,8 @@ #include "reader.h" #include "redirection.h" #include "screen.h" -#include "signal.h" +#include "signals.h" +#include "smoke.rs.h" #include "termsize.h" #include "timer.h" #include "tokenizer.h" @@ -844,7 +848,7 @@ static void test_fd_monitor() { constexpr uint64_t usec_per_msec = 1000; // Items which will never receive data or be called back. - item_maker_t item_never(fd_monitor_item_t::kNoTimeout); + item_maker_t item_never(kNoTimeout); item_maker_t item_hugetimeout(100000000LLU * usec_per_msec); // Item which should get no data, and time out. @@ -854,13 +858,13 @@ static void test_fd_monitor() { item_maker_t item42_timeout(16 * usec_per_msec); // Item which should get exactly 42 bytes, and not time out. - item_maker_t item42_nottimeout(fd_monitor_item_t::kNoTimeout); + item_maker_t item42_nottimeout(kNoTimeout); // Item which should get 42 bytes, then get notified it is closed. item_maker_t item42_thenclose(16 * usec_per_msec); // Item which gets one poke. - item_maker_t item_pokee(fd_monitor_item_t::kNoTimeout); + item_maker_t item_pokee(kNoTimeout); // Item which should be called back once. item_maker_t item_oneshot(16 * usec_per_msec); @@ -4289,7 +4293,7 @@ bool poll_notifier(const std::unique_ptr ¬e) { bool result = false; int fd = note->notification_fd(); - if (fd >= 0 && fd_readable_set_t::poll_fd_readable(fd)) { + if (fd >= 0 && poll_fd_readable(fd)) { result = note->notification_fd_became_readable(fd); } return result; @@ -6682,7 +6686,8 @@ void test_dirname_basename() { static void test_topic_monitor() { say(L"Testing topic monitor"); - topic_monitor_t monitor; + auto monitor_box = new_topic_monitor(); + topic_monitor_t &monitor = *monitor_box; generation_list_t gens{}; constexpr auto t = topic_t::sigchld; gens.sigchld = 0; @@ -6706,12 +6711,13 @@ static void test_topic_monitor() { static void test_topic_monitor_torture() { say(L"Torture-testing topic monitor"); - topic_monitor_t monitor; + auto monitor_box = new_topic_monitor(); + topic_monitor_t &monitor = *monitor_box; const size_t thread_count = 64; constexpr auto t1 = topic_t::sigchld; constexpr auto t2 = topic_t::sighupint; std::vector gens; - gens.resize(thread_count, generation_list_t::invalids()); + gens.resize(thread_count, invalid_generations()); std::atomic post_count{}; for (auto &gen : gens) { gen = monitor.current_generations(); @@ -7137,6 +7143,11 @@ void test_wgetopt() { do_test(join_strings(arguments, L' ') == L"emacsnw emacs -nw"); } +void test_rust_smoke() { + size_t x = rust::add(37, 5); + do_test(x == 42); +} + // typedef void (test_entry_point_t)(); using test_entry_point_t = void (*)(); struct test_t { @@ -7256,8 +7267,8 @@ static const test_t s_tests[]{ {TEST_GROUP("re"), test_re_named}, {TEST_GROUP("re"), test_re_name_extraction}, {TEST_GROUP("re"), test_re_substitute}, - {TEST_GROUP("re"), test_re_substitute}, {TEST_GROUP("wgetopt"), test_wgetopt}, + {TEST_GROUP("rust_smoke"), test_rust_smoke}, }; void list_tests() { @@ -7312,6 +7323,7 @@ int main(int argc, char **argv) { say(L"Testing low-level functionality"); set_main_thread(); setup_fork_guards(); + rust_init(); proc_init(); env_init(); misc_init(); diff --git a/src/flog.cpp b/src/flog.cpp index f5e3b887d..b6f0ee61d 100644 --- a/src/flog.cpp +++ b/src/flog.cpp @@ -180,6 +180,8 @@ void set_flog_output_file(FILE *f) { void log_extra_to_flog_file(const wcstring &s) { g_logger.acquire()->log_extra(s.c_str()); } +int get_flog_file_fd() { return s_flog_file_fd; } + std::vector get_flog_categories() { std::vector result(s_all_categories.begin(), s_all_categories.end()); std::sort(result.begin(), result.end(), [](const category_t *a, const category_t *b) { diff --git a/src/flog.h b/src/flog.h index 4a3627f3f..085be6d78 100644 --- a/src/flog.h +++ b/src/flog.h @@ -197,6 +197,10 @@ std::vector get_flog_categories(); /// This is used by the tracing machinery. void log_extra_to_flog_file(const wcstring &s); +/// \return the FD for the flog file. +/// This is exposed for the Rust bridge. +int get_flog_file_fd(); + /// Output to the fish log a sequence of arguments, separated by spaces, and ending with a newline. /// We save and restore errno because we don't want this to affect other code. #define FLOG(wht, ...) \ diff --git a/src/function.cpp b/src/function.cpp index b64d57bc0..283d57233 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -28,7 +28,7 @@ #include "parse_constants.h" #include "parser.h" #include "parser_keywords.h" -#include "signal.h" +#include "signals.h" #include "wcstringutil.h" #include "wutil.h" // IWYU pragma: keep diff --git a/src/input.cpp b/src/input.cpp index 562cf7f33..515106b9f 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -28,8 +28,8 @@ #include "parser.h" #include "proc.h" #include "reader.h" -#include "signal.h" // IWYU pragma: keep -#include "wutil.h" // IWYU pragma: keep +#include "signals.h" // IWYU pragma: keep +#include "wutil.h" // IWYU pragma: keep /// A name for our own key mapping for nul. static const wchar_t *k_nul_mapping_name = L"nul"; diff --git a/src/input_common.cpp b/src/input_common.cpp index d2e6e78c0..bd5eba595 100644 --- a/src/input_common.cpp +++ b/src/input_common.cpp @@ -2,7 +2,7 @@ #include "config.h" #include -#include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include #include @@ -22,6 +22,7 @@ #include "env.h" #include "env_universal_common.h" #include "fallback.h" // IWYU pragma: keep +#include "fd_readable_set.rs.h" #include "fds.h" #include "flog.h" #include "input_common.h" @@ -58,7 +59,8 @@ using readb_result_t = int; static readb_result_t readb(int in_fd) { assert(in_fd >= 0 && "Invalid in fd"); universal_notifier_t& notifier = universal_notifier_t::default_notifier(); - fd_readable_set_t fdset; + auto fdset_box = new_fd_readable_set(); + fd_readable_set_t& fdset = *fdset_box; for (;;) { fdset.clear(); fdset.add(in_fd); @@ -73,7 +75,7 @@ static readb_result_t readb(int in_fd) { // Get its suggested delay (possibly none). // Note a 0 here means do not poll. - uint64_t timeout = fd_readable_set_t::kNoTimeout; + uint64_t timeout = kNoTimeout; if (uint64_t usecs_delay = notifier.usec_delay_between_polls()) { timeout = usecs_delay; } diff --git a/src/io.cpp b/src/io.cpp index 5ba1b3c3b..9cc881ca7 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -334,6 +334,10 @@ const wcstring &output_stream_t::contents() const { return g_empty_string; } int output_stream_t::flush_and_check_error() { return STATUS_CMD_OK; } +fd_output_stream_t::fd_output_stream_t(int fd) : fd_(fd), sigcheck_(topic_t::sighupint) { + assert(fd_ >= 0 && "Invalid fd"); +} + bool fd_output_stream_t::append(const wchar_t *s, size_t amt) { if (errored_) return false; int res = wwrite_to_fd(s, amt, this->fd_); diff --git a/src/io.h b/src/io.h index bd23eddf4..205b91b56 100644 --- a/src/io.h +++ b/src/io.h @@ -16,7 +16,7 @@ #include "fds.h" #include "global_safety.h" #include "redirection.h" -#include "signal.h" +#include "signals.h" #include "topic_monitor.h" using std::shared_ptr; @@ -413,9 +413,7 @@ class null_output_stream_t final : public output_stream_t { class fd_output_stream_t final : public output_stream_t { public: /// Construct from a file descriptor, which must be nonegative. - explicit fd_output_stream_t(int fd) : fd_(fd), sigcheck_(topic_t::sighupint) { - assert(fd_ >= 0 && "Invalid fd"); - } + explicit fd_output_stream_t(int fd); int flush_and_check_error() override; @@ -496,6 +494,11 @@ struct io_streams_t : noncopyable_t { std::shared_ptr job_group{}; io_streams_t(output_stream_t &out, output_stream_t &err) : out(out), err(err) {} + + /// autocxx junk. + output_stream_t &get_out() { return out; }; + output_stream_t &get_err() { return err; }; + io_streams_t(const io_streams_t &) = delete; }; #endif diff --git a/src/iothread.cpp b/src/iothread.cpp index c9bbe20ee..62fd6c6e4 100644 --- a/src/iothread.cpp +++ b/src/iothread.cpp @@ -16,6 +16,7 @@ #include "common.h" #include "fallback.h" +#include "fd_readable_set.rs.h" #include "fds.h" #include "flog.h" #include "maybe.h" @@ -213,7 +214,7 @@ void iothread_perform_impl(void_function_t &&func, bool cant_wait) { int iothread_port() { return get_notify_signaller().read_fd(); } void iothread_service_main_with_timeout(uint64_t timeout_usec) { - if (fd_readable_set_t::is_fd_readable(iothread_port(), timeout_usec)) { + if (is_fd_readable(iothread_port(), timeout_usec)) { iothread_service_main(); } } diff --git a/src/parser.cpp b/src/parser.cpp index b9402fdd0..89c962f27 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -29,7 +29,7 @@ #include "parse_constants.h" #include "parse_execution.h" #include "proc.h" -#include "signal.h" +#include "signals.h" #include "wutil.h" // IWYU pragma: keep class io_chain_t; @@ -454,7 +454,7 @@ wcstring parser_t::current_line() { void parser_t::job_add(shared_ptr job) { assert(job != nullptr); assert(!job->processes.empty()); - job_list.push_front(std::move(job)); + job_list.insert(job_list.begin(), std::move(job)); } void parser_t::job_promote(job_t *job) { @@ -664,6 +664,10 @@ void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &erro } } +RustFFIJobList parser_t::ffi_jobs() const { + return RustFFIJobList{const_cast(job_list.data()), job_list.size()}; +} + block_t::block_t(block_type_t t) : block_type(t) {} wcstring block_t::description() const { diff --git a/src/parser.h b/src/parser.h index b1dfc0d51..cc0683fb0 100644 --- a/src/parser.h +++ b/src/parser.h @@ -13,6 +13,7 @@ #include #include "common.h" +#include "cxx.h" #include "env.h" #include "expand.h" #include "job_group.h" @@ -38,7 +39,7 @@ inline bool event_block_list_blocks_type(const event_blockage_list_t &ebls) { } /// Types of blocks. -enum class block_type_t : uint16_t { +enum class block_type_t : uint8_t { while_block, /// While loop block for_block, /// For loop block if_block, /// If block @@ -469,7 +470,10 @@ class parser_t : public std::enable_shared_from_this { std::shared_ptr shared(); /// \return a cancel poller for checking if this parser has been signalled. + /// autocxx falls over with this so hide it. +#if INCLUDE_RUST_HEADERS cancel_checker_t cancel_checker() const; +#endif /// \return the operation context for this parser. operation_context_t context(); @@ -477,6 +481,9 @@ class parser_t : public std::enable_shared_from_this { /// Checks if the max eval depth has been exceeded bool is_eval_depth_exceeded() const { return eval_level >= FISH_MAX_EVAL_DEPTH; } + /// autocxx junk. + RustFFIJobList ffi_jobs() const; + ~parser_t(); }; diff --git a/src/postfork.cpp b/src/postfork.cpp index a2884eb33..570bcd7a5 100644 --- a/src/postfork.cpp +++ b/src/postfork.cpp @@ -24,7 +24,7 @@ #include "postfork.h" #include "proc.h" #include "redirection.h" -#include "signal.h" +#include "signals.h" #include "wutil.h" // IWYU pragma: keep #ifndef JOIN_THREADS_BEFORE_FORK diff --git a/src/proc.cpp b/src/proc.cpp index 8be091d50..eb6310b10 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -45,7 +45,7 @@ #include "parser.h" #include "proc.h" #include "reader.h" -#include "signal.h" +#include "signals.h" #include "wutil.h" // IWYU pragma: keep /// The signals that signify crashes to us. @@ -170,11 +170,17 @@ maybe_t job_t::get_statuses() const { return st; } +const process_list_t &job_t::get_processes() const { return processes; } + +RustFFIProcList job_t::ffi_processes() const { + return RustFFIProcList{const_cast(processes.data()), processes.size()}; +} + void internal_proc_t::mark_exited(proc_status_t status) { assert(!exited() && "Process is already exited"); status_.store(status, std::memory_order_relaxed); exited_.store(true, std::memory_order_release); - topic_monitor_t::principal().post(topic_t::internal_exit); + topic_monitor_principal().post(topic_t::internal_exit); FLOG(proc_internal_proc, L"Internal proc", internal_proc_id_, L"exited with status", status.status_value()); } @@ -248,7 +254,7 @@ static void handle_child_status(const shared_ptr &job, process_t *proc, process_t::process_t() = default; void process_t::check_generations_before_launch() { - gens_ = topic_monitor_t::principal().current_generations(); + gens_ = topic_monitor_principal().current_generations(); } void process_t::mark_aborted_before_launch() { @@ -362,7 +368,7 @@ static void process_mark_finished_children(parser_t &parser, bool block_ok) { // The exit generation tells us if we have an exit; the signal generation allows for detecting // SIGHUP and SIGINT. // Go through each process and figure out if and how it wants to be reaped. - generation_list_t reapgens = generation_list_t::invalids(); + generation_list_t reapgens = invalid_generations(); for (const auto &j : parser.jobs()) { for (const auto &proc : j->processes) { if (!j->can_reap(proc)) continue; @@ -381,7 +387,7 @@ static void process_mark_finished_children(parser_t &parser, bool block_ok) { } // Now check for changes, optionally waiting. - if (!topic_monitor_t::principal().check(&reapgens, block_ok)) { + if (!topic_monitor_principal().check(&reapgens, block_ok)) { // Nothing changed. return; } diff --git a/src/proc.h b/src/proc.h index 63ec2ccd9..cc41b620d 100644 --- a/src/proc.h +++ b/src/proc.h @@ -94,8 +94,9 @@ class proc_status_t { /// Construct directly from an exit code. static proc_status_t from_exit_code(int ret) { - assert(ret >= 0 && "trying to create proc_status_t from failed wait{,id,pid}() call" - " or invalid builtin exit code!"); + assert(ret >= 0 && + "trying to create proc_status_t from failed wait{,id,pid}() call" + " or invalid builtin exit code!"); // Some paranoia. constexpr int zerocode = w_exitcode(0, 0); @@ -349,6 +350,11 @@ using process_ptr_t = std::unique_ptr; using process_list_t = std::vector; class parser_t; +struct RustFFIProcList { + process_ptr_t *procs; + size_t count; +}; + /// A struct representing a job. A job is a pipeline of one or more processes. class job_t : noncopyable_t { public: @@ -383,6 +389,9 @@ class job_t : noncopyable_t { job_t(const properties_t &props, wcstring command_str); ~job_t(); + /// Autocxx needs to see this. + job_t(const job_t &) = delete; + /// Returns the command as a wchar_t *. */ const wchar_t *command_wcstr() const { return command_str.c_str(); } @@ -440,6 +449,9 @@ class job_t : noncopyable_t { /// A non-user-visible, never-recycled job ID. const internal_job_id_t internal_job_id; + /// Getter to enable ffi. + internal_job_id_t get_internal_job_id() const { return internal_job_id; } + /// Flags associated with the job. struct flags_t { /// Whether the specified job is completely constructed: every process in the job has been @@ -522,9 +534,21 @@ class job_t : noncopyable_t { /// \returns the statuses for this job. maybe_t get_statuses() const; + + /// \returns the list of processes. + const process_list_t &get_processes() const; + + /// autocxx junk. + RustFFIProcList ffi_processes() const; }; using job_ref_t = std::shared_ptr; +// Helper junk for autocxx. +struct RustFFIJobList { + job_ref_t *jobs; + size_t count; +}; + /// Whether this shell is attached to a tty. bool is_interactive_session(); void set_interactive_session(bool flag); @@ -540,7 +564,7 @@ bool no_exec(); void mark_no_exec(); // List of jobs. -using job_list_t = std::deque; +using job_list_t = std::vector; /// The current job control mode. /// diff --git a/src/reader.cpp b/src/reader.cpp index bb1e101ee..a52101fec 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -54,6 +54,7 @@ #include "exec.h" #include "expand.h" #include "fallback.h" // IWYU pragma: keep +#include "fd_readable_set.rs.h" #include "fds.h" #include "flog.h" #include "function.h" @@ -75,7 +76,7 @@ #include "proc.h" #include "reader.h" #include "screen.h" -#include "signal.h" +#include "signals.h" #include "termsize.h" #include "tokenizer.h" #include "wcstringutil.h" @@ -3362,7 +3363,7 @@ maybe_t reader_data_t::read_normal_chars(readline_loop_state_t &rl while (accumulated_chars.size() < limit) { bool allow_commands = (accumulated_chars.empty()); auto evt = inputter.read_char(allow_commands ? normal_handler : empty_handler); - if (!event_is_normal_char(evt) || !fd_readable_set_t::poll_fd_readable(conf.in)) { + if (!event_is_normal_char(evt) || !poll_fd_readable(conf.in)) { event_needing_handling = std::move(evt); break; } else if (evt.input_style == char_input_style_t::notfirst && accumulated_chars.empty() && diff --git a/src/rustffi.cpp b/src/rustffi.cpp new file mode 100644 index 000000000..d5e4980a6 --- /dev/null +++ b/src/rustffi.cpp @@ -0,0 +1,21 @@ +#include + +#include "wutil.h" + +extern "C" { +void fishffi$unique_ptr$wcstring$null(std::unique_ptr *ptr) noexcept { + new (ptr) std::unique_ptr(); +} +void fishffi$unique_ptr$wcstring$raw(std::unique_ptr *ptr, wcstring *raw) noexcept { + new (ptr) std::unique_ptr(raw); +} +const wcstring *fishffi$unique_ptr$wcstring$get(const std::unique_ptr &ptr) noexcept { + return ptr.get(); +} +wcstring *fishffi$unique_ptr$wcstring$release(std::unique_ptr &ptr) noexcept { + return ptr.release(); +} +void fishffi$unique_ptr$wcstring$drop(std::unique_ptr *ptr) noexcept { + ptr->~unique_ptr(); +} +} // extern "C" diff --git a/src/signal.cpp b/src/signals.cpp similarity index 96% rename from src/signal.cpp rename to src/signals.cpp index 804fc53a9..5b91d5e8a 100644 --- a/src/signal.cpp +++ b/src/signals.cpp @@ -16,7 +16,7 @@ #include "fallback.h" // IWYU pragma: keep #include "global_safety.h" #include "reader.h" -#include "signal.h" +#include "signals.h" #include "termsize.h" #include "topic_monitor.h" #include "wutil.h" // IWYU pragma: keep @@ -243,7 +243,7 @@ static void fish_signal_handler(int sig, siginfo_t *info, void *context) { if (!observed) { reader_sighup(); } - topic_monitor_t::principal().post(topic_t::sighupint); + topic_monitor_principal().post(topic_t::sighupint); break; case SIGTERM: @@ -261,12 +261,12 @@ static void fish_signal_handler(int sig, siginfo_t *info, void *context) { s_cancellation_signal = SIGINT; } reader_handle_sigint(); - topic_monitor_t::principal().post(topic_t::sighupint); + topic_monitor_principal().post(topic_t::sighupint); break; case SIGCHLD: // A child process stopped or exited. - topic_monitor_t::principal().post(topic_t::sigchld); + topic_monitor_principal().post(topic_t::sigchld); break; case SIGALRM: @@ -429,7 +429,7 @@ sigchecker_t::sigchecker_t(topic_t signal) : topic_(signal) { } bool sigchecker_t::check() { - auto &tm = topic_monitor_t::principal(); + auto &tm = topic_monitor_principal(); generation_t gen = tm.generation_for_topic(topic_); bool changed = this->gen_ != gen; this->gen_ = gen; @@ -437,8 +437,8 @@ bool sigchecker_t::check() { } void sigchecker_t::wait() const { - auto &tm = topic_monitor_t::principal(); - generation_list_t gens = generation_list_t::invalids(); - gens.at(topic_) = this->gen_; + auto &tm = topic_monitor_principal(); + generation_list_t gens = invalid_generations(); + gens.at_mut(topic_) = this->gen_; tm.check(&gens, true /* wait */); } diff --git a/src/signal.h b/src/signals.h similarity index 100% rename from src/signal.h rename to src/signals.h diff --git a/src/topic_monitor.cpp b/src/topic_monitor.cpp deleted file mode 100644 index 626d3eec6..000000000 --- a/src/topic_monitor.cpp +++ /dev/null @@ -1,283 +0,0 @@ -#include "config.h" // IWYU pragma: keep - -#include "topic_monitor.h" - -#include - -#include - -#include "flog.h" -#include "iothread.h" -#include "maybe.h" -#include "wcstringutil.h" -#include "wutil.h" - -wcstring generation_list_t::describe() const { - wcstring result; - for (generation_t gen : this->as_array()) { - if (!result.empty()) result.push_back(L','); - if (gen == invalid_generation) { - result.append(L"-1"); - } else { - result.append(to_string(gen)); - } - } - return result; -} - -binary_semaphore_t::binary_semaphore_t() : sem_ok_(false) { - // sem_init always fails with ENOSYS on Mac and has an annoying deprecation warning. - // On BSD sem_init uses a file descriptor under the hood which doesn't get CLOEXEC (see #7304). - // So use fast semaphores on Linux only. -#ifdef __linux__ - sem_ok_ = (0 == sem_init(&sem_, 0, 0)); -#endif - if (!sem_ok_) { - auto pipes = make_autoclose_pipes(); - assert(pipes.has_value() && "Failed to make pubsub pipes"); - pipes_ = pipes.acquire(); - - // Whoof. Thread Sanitizer swallows signals and replays them at its leisure, at the point - // where instrumented code makes certain blocking calls. But tsan cannot interrupt a signal - // call, so if we're blocked in read() (like the topic monitor wants to be!), we'll never - // receive SIGCHLD and so deadlock. So if tsan is enabled, we mark our fd as non-blocking - // (so reads will never block) and use select() to poll it. -#ifdef FISH_TSAN_WORKAROUNDS - DIE_ON_FAILURE(make_fd_nonblocking(pipes_.read.fd())); -#endif - } -} - -binary_semaphore_t::~binary_semaphore_t() { - // We never use sem_t on Mac. The #ifdef avoids deprecation warnings. -#ifndef __APPLE__ - if (sem_ok_) (void)sem_destroy(&sem_); -#endif -} - -void binary_semaphore_t::die(const wchar_t *msg) const { - wperror(msg); - DIE("unexpected failure"); -} - -void binary_semaphore_t::post() { - if (sem_ok_) { - int res = sem_post(&sem_); - // sem_post is non-interruptible. - if (res < 0) die(L"sem_post"); - } else { - // Write exactly one byte. - ssize_t ret; - do { - const uint8_t v = 0; - ret = write(pipes_.write.fd(), &v, sizeof v); - } while (ret < 0 && errno == EINTR); - if (ret < 0) die(L"write"); - } -} - -void binary_semaphore_t::wait() { - if (sem_ok_) { - int res; - do { - res = sem_wait(&sem_); - } while (res < 0 && errno == EINTR); - // Other errors here are very unexpected. - if (res < 0) die(L"sem_wait"); - } else { - int fd = pipes_.read.fd(); - // We must read exactly one byte. - for (;;) { -#ifdef FISH_TSAN_WORKAROUNDS - // Under tsan our notifying pipe is non-blocking, so we would busy-loop on the read() - // call until data is available (that is, fish would use 100% cpu while waiting for - // processes). This call prevents that. - (void)fd_readable_set_t::is_fd_readable(fd, fd_readable_set_t::kNoTimeout); -#endif - uint8_t ignored; - auto amt = read(fd, &ignored, sizeof ignored); - if (amt == 1) break; - // EAGAIN should only be returned in TSan case. - if (amt < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) die(L"read"); - } - } -} - -/// Implementation of the principal monitor. This uses new (and leaks) to avoid registering a -/// pointless at-exit handler for the dtor. -static topic_monitor_t *const s_principal = new topic_monitor_t(); - -topic_monitor_t &topic_monitor_t::principal() { - // Do not attempt to move s_principal to a function-level static, it needs to be accessed from a - // signal handler so it must not be lazily created. - return *s_principal; -} - -topic_monitor_t::topic_monitor_t() = default; -topic_monitor_t::~topic_monitor_t() = default; - -void topic_monitor_t::post(topic_t topic) { - // Beware, we may be in a signal handler! - // Atomically update the pending topics. - const uint8_t topicbit = topic_to_bit(topic); - - // CAS in our bit, capturing the old status value. - status_bits_t oldstatus; - bool cas_success = false; - while (!cas_success) { - oldstatus = status_.load(std::memory_order_relaxed); - // Clear wakeup bit and set our topic bit. - status_bits_t newstatus = oldstatus; - newstatus &= ~STATUS_NEEDS_WAKEUP; - newstatus |= topicbit; - cas_success = status_.compare_exchange_weak(oldstatus, newstatus); - } - // Note that if the STATUS_NEEDS_WAKEUP bit is set, no other bits must be set. - assert(((oldstatus == STATUS_NEEDS_WAKEUP) == bool(oldstatus & STATUS_NEEDS_WAKEUP)) && - "If STATUS_NEEDS_WAKEUP is set no other bits should be set"); - - // If the bit was already set, then someone else posted to this topic and nobody has reacted to - // it yet. In that case we're done. - if (oldstatus & topicbit) { - return; - } - - // We set a new bit. - // Check if we should wake up a thread because it was waiting. - if (oldstatus & STATUS_NEEDS_WAKEUP) { - std::atomic_thread_fence(std::memory_order_release); - sema_.post(); - } -} - -generation_list_t topic_monitor_t::updated_gens_in_data(acquired_lock &data) { - // Atomically acquire the pending updates, swapping in 0. - // If there are no pending updates (likely) or a thread is waiting, just return. - // Otherwise CAS in 0 and update our topics. - const auto relaxed = std::memory_order_relaxed; - topic_bitmask_t changed_topic_bits; - bool cas_success; - do { - changed_topic_bits = status_.load(relaxed); - if (changed_topic_bits == 0 || changed_topic_bits == STATUS_NEEDS_WAKEUP) - return data->current; - cas_success = status_.compare_exchange_weak(changed_topic_bits, 0); - } while (!cas_success); - assert((changed_topic_bits & STATUS_NEEDS_WAKEUP) == 0 && - "Thread waiting bit should not be set"); - - // Update the current generation with our topics and return it. - for (topic_t topic : all_topics()) { - if (changed_topic_bits & topic_to_bit(topic)) { - data->current.at(topic) += 1; - FLOG(topic_monitor, "Updating topic", static_cast(topic), "to", - data->current.at(topic)); - } - } - // Report our change. - data_notifier_.notify_all(); - return data->current; -} - -generation_list_t topic_monitor_t::updated_gens() { - auto data = data_.acquire(); - return updated_gens_in_data(data); -} - -bool topic_monitor_t::try_update_gens_maybe_becoming_reader(generation_list_t *gens) { - bool become_reader = false; - auto data = data_.acquire(); - for (;;) { - // See if the updated gen list has changed. If so we don't need to become the reader. - auto current = updated_gens_in_data(data); - FLOG(topic_monitor, "TID", thread_id(), "local ", gens->describe(), ": current", - current.describe()); - if (*gens != current) { - *gens = current; - break; - } - - // The generations haven't changed. Perhaps we become the reader. - // Note we still hold the lock, so this cannot race with any other thread becoming the - // reader. - if (data->has_reader) { - // We already have a reader, wait for it to notify us and loop again. - data_notifier_.wait(data.get_lock()); - continue; - } else { - // We will try to become the reader. - // Reader bit should not be set in this case. - assert((status_.load() & STATUS_NEEDS_WAKEUP) == 0 && "No thread should be waiting"); - // Try becoming the reader by marking the reader bit. - status_bits_t expected_old = 0; - if (!status_.compare_exchange_strong(expected_old, STATUS_NEEDS_WAKEUP)) { - // We failed to become the reader, perhaps because another topic post just arrived. - // Loop again. - continue; - } - // We successfully did a CAS from 0 -> STATUS_NEEDS_WAKEUP. - // Now any successive topic post must signal us. - FLOG(topic_monitor, "TID", thread_id(), "becoming reader"); - become_reader = true; - data->has_reader = true; - break; - } - } - return become_reader; -} - -generation_list_t topic_monitor_t::await_gens(const generation_list_t &input_gens) { - generation_list_t gens = input_gens; - while (gens == input_gens) { - bool become_reader = try_update_gens_maybe_becoming_reader(&gens); - if (become_reader) { - // Now we are the reader. Read from the pipe, and then update with any changes. - // Note we no longer hold the lock. - assert(gens == input_gens && - "Generations should not have changed if we are the reader."); - - // Wait to be woken up. - sema_.wait(); - - // We are finished waiting. We must stop being the reader, and post on the condition - // variable to wake up any other threads waiting for us to finish reading. - auto data = data_.acquire(); - gens = data->current; - FLOG(topic_monitor, "TID", thread_id(), "local", input_gens.describe(), - "read() complete, current is", gens.describe()); - assert(data->has_reader && "We should be the reader"); - data->has_reader = false; - data_notifier_.notify_all(); - } - } - return gens; -} - -bool topic_monitor_t::check(generation_list_t *gens, bool wait) { - if (!gens->any_valid()) return false; - - generation_list_t current = updated_gens(); - bool changed = false; - for (;;) { - // Load the topic list and see if anything has changed. - for (topic_t topic : all_topics()) { - if (gens->is_valid(topic)) { - assert(gens->at(topic) <= current.at(topic) && - "Incoming gen count exceeded published count"); - if (gens->at(topic) < current.at(topic)) { - gens->at(topic) = current.at(topic); - changed = true; - } - } - } - - // If we're not waiting, or something changed, then we're done. - if (!wait || changed) { - break; - } - - // Wait until our gens change. - current = await_gens(current); - } - return changed; -} diff --git a/src/topic_monitor.h b/src/topic_monitor.h index adc54f5e4..f62cb9499 100644 --- a/src/topic_monitor.h +++ b/src/topic_monitor.h @@ -1,259 +1,25 @@ #ifndef FISH_TOPIC_MONITOR_H #define FISH_TOPIC_MONITOR_H -#include +#include "config.h" -#include -#include -#include // IWYU pragma: keep -#include -#include -#include +#include -#include "common.h" -#include "fds.h" - -/** Topic monitoring support. Topics are conceptually "a thing that can happen." For example, - delivery of a SIGINT, a child process exits, etc. It is possible to post to a topic, which means - that that thing happened. - - Associated with each topic is a current generation, which is a 64 bit value. When you query a - topic, you get back a generation. If on the next query the generation has increased, then it - indicates someone posted to the topic. - - For example, if you are monitoring a child process, you can query the sigchld topic. If it has - increased since your last query, it is possible that your child process has exited. - - Topic postings may be coalesced. That is there may be two posts to a given topic, yet the - generation only increases by 1. The only guarantee is that after a topic post, the current - generation value is larger than any value previously queried. - - Tying this all together is the topic_monitor_t. This provides the current topic generations, and - also provides the ability to perform a blocking wait for any topic to change in a particular topic - set. This is the real power of topics: you can wait for a sigchld signal OR a thread exit. - */ - -/// A generation is a counter incremented every time the value of a topic changes. -/// It is 64 bit so it will never wrap. using generation_t = uint64_t; -/// A generation value which indicates the topic is not of interest. -constexpr generation_t invalid_generation = std::numeric_limits::max(); +#if INCLUDE_RUST_HEADERS -/// The list of topics which may be observed. -enum class topic_t : uint8_t { - sighupint, // Corresponds to both SIGHUP and SIGINT signals. - sigchld, // Corresponds to SIGCHLD signal. - internal_exit, // Corresponds to an internal process exit. -}; +#include "topic_monitor.rs.h" -/// Helper to return all topics, allowing easy iteration. -inline std::array all_topics() { - return {{topic_t::sighupint, topic_t::sigchld, topic_t::internal_exit}}; -} +#else -/// Simple value type containing the values for a topic. -/// This should be kept in sync with topic_t. -class generation_list_t { - public: - generation_list_t() = default; - - generation_t sighupint{0}; - generation_t sigchld{0}; - generation_t internal_exit{0}; - - /// \return the value for a topic. - generation_t &at(topic_t topic) { - switch (topic) { - case topic_t::sigchld: - return sigchld; - case topic_t::sighupint: - return sighupint; - case topic_t::internal_exit: - return internal_exit; - } - DIE("Unreachable"); - } - - generation_t at(topic_t topic) const { - switch (topic) { - case topic_t::sighupint: - return sighupint; - case topic_t::sigchld: - return sigchld; - case topic_t::internal_exit: - return internal_exit; - } - DIE("Unreachable"); - } - - /// \return ourselves as an array. - std::array as_array() const { return {{sighupint, sigchld, internal_exit}}; } - - /// Set the value of \p topic to the smaller of our value and the value in \p other. - void set_min_from(topic_t topic, const generation_list_t &other) { - if (this->at(topic) > other.at(topic)) { - this->at(topic) = other.at(topic); - } - } - - /// \return whether a topic is valid. - bool is_valid(topic_t topic) const { return this->at(topic) != invalid_generation; } - - /// \return whether any topic is valid. - bool any_valid() const { - bool valid = false; - for (auto gen : as_array()) { - if (gen != invalid_generation) valid = true; - } - return valid; - } - - bool operator==(const generation_list_t &rhs) const { - return sighupint == rhs.sighupint && sigchld == rhs.sigchld && - internal_exit == rhs.internal_exit; - } - - bool operator!=(const generation_list_t &rhs) const { return !(*this == rhs); } - - /// return a string representation for debugging. - wcstring describe() const; - - /// Generation list containing invalid generations only. - static generation_list_t invalids() { - return generation_list_t(invalid_generation, invalid_generation, invalid_generation); - } - - private: - generation_list_t(generation_t sighupint, generation_t sigchld, generation_t internal_exit) - : sighupint(sighupint), sigchld(sigchld), internal_exit(internal_exit) {} -}; - -/// A simple binary semaphore. -/// On systems that do not support unnamed semaphores (macOS in particular) this is built on top of -/// a self-pipe. Note that post() must be async-signal safe. -class binary_semaphore_t { - public: - binary_semaphore_t(); - ~binary_semaphore_t(); - - /// Release a waiting thread. - void post(); - - /// Wait for a post. - /// This loops on EINTR. - void wait(); - - private: - // Print a message and exit. - void die(const wchar_t *msg) const; - - // Whether our semaphore was successfully initialized. - bool sem_ok_{}; - - // The semaphore, if initialized. - sem_t sem_{}; - - // Pipes used to emulate a semaphore, if not initialized. - autoclose_pipes_t pipes_{}; -}; - -/// The topic monitor class. This permits querying the current generation values for topics, -/// optionally blocking until they increase. -/// What we would like to write is that we have a set of topics, and threads wait for changes on a -/// condition variable which is tickled in post(). But this can't work because post() may be called -/// from a signal handler and condition variables are not async-signal safe. -/// So instead the signal handler announces changes via a binary semaphore. -/// In the wait case, what generally happens is: -/// A thread fetches the generations, see they have not changed, and then decides to try to wait. -/// It does so by atomically swapping in STATUS_NEEDS_WAKEUP to the status bits. -/// If that succeeds, it waits on the binary semaphore. The post() call will then wake the thread -/// up. If if failed, then either a post() call updated the status values (so perhaps there is a -/// new topic post) or some other thread won the race and called wait() on the semaphore. Here our -/// thread will wait on the data_notifier_ queue. -class topic_monitor_t : noncopyable_t, nonmovable_t { - private: - using topic_bitmask_t = uint8_t; - - // Some stuff that needs to be protected by the same lock. - struct data_t { - /// The current values. - generation_list_t current{}; - - /// A flag indicating that there is a current reader. - /// The 'reader' is responsible for calling sema_.wait(). - bool has_reader{false}; - }; - owning_lock data_{}; - - /// Condition variable for broadcasting notifications. - /// This is associated with data_'s mutex. - std::condition_variable data_notifier_{}; - - /// A status value which describes our current state, managed via atomics. - /// Three possibilities: - /// 0: no changed topics, no thread is waiting. - /// 128: no changed topics, some thread is waiting and needs wakeup. - /// anything else: some changed topic, no thread is waiting. - /// Note that if the msb is set (status == 128) no other bit may be set. - using status_bits_t = uint8_t; - std::atomic status_{}; - - /// Sentinel status value indicating that a thread is waiting and needs a wakeup. - /// Note it is an error for this bit to be set and also any topic bit. - static constexpr uint8_t STATUS_NEEDS_WAKEUP = 128; - - /// Binary semaphore used to communicate changes. - /// If status_ is STATUS_NEEDS_WAKEUP, then a thread has commited to call wait() on our sema and - /// this must be balanced by the next call to post(). Note only one thread may wait at a time. - binary_semaphore_t sema_{}; - - /// Apply any pending updates to the data. - /// This accepts data because it must be locked. - /// \return the updated generation list. - generation_list_t updated_gens_in_data(acquired_lock &data); - - /// Given a list of input generations, attempt to update them to something newer. - /// If \p gens is older, then just return those by reference, and directly return false (not - /// becoming the reader). - /// If \p gens is current and there is not a reader, then do not update \p gens and return true, - /// indicating we should become the reader. Now it is our responsibility to wait on the - /// semaphore and notify on a change via the condition variable. If \p gens is current, and - /// there is already a reader, then wait until the reader notifies us and try again. - bool try_update_gens_maybe_becoming_reader(generation_list_t *gens); - - /// Wait for some entry in the list of generations to change. - /// \return the new gens. - generation_list_t await_gens(const generation_list_t &input_gens); - - /// \return the current generation list, opportunistically applying any pending updates. - generation_list_t updated_gens(); - - /// Helper to convert a topic to a bitmask containing just that topic. - static topic_bitmask_t topic_to_bit(topic_t t) { return 1 << static_cast(t); } - - public: - topic_monitor_t(); - ~topic_monitor_t(); - - /// The principal topic_monitor. This may be fetched from a signal handler. - static topic_monitor_t &principal(); - - /// Post to a topic, potentially from a signal handler. - void post(topic_t topic); - - /// Access the current generations. - generation_list_t current_generations() { return updated_gens(); } - - /// Access the generation for a topic. - generation_t generation_for_topic(topic_t topic) { return current_generations().at(topic); } - - /// For each valid topic in \p gens, check to see if the current topic is larger than - /// the value in \p gens. - /// If \p wait is set, then wait if there are no changes; otherwise return immediately. - /// \return true if some topic changed, false if none did. - /// On a true return, this updates the generation list \p gens. - bool check(generation_list_t *gens, bool wait); +// Hacks to allow us to compile without Rust headers. +struct generation_list_t { + uint64_t sighupint; + uint64_t sigchld; + uint64_t internal_exit; }; #endif + +#endif diff --git a/src/wutil.cpp b/src/wutil.cpp index b1fa99d07..17d5e64c1 100644 --- a/src/wutil.cpp +++ b/src/wutil.cpp @@ -250,10 +250,10 @@ int wunlink(const wcstring &file_name) { return unlink(tmp.c_str()); } -void wperror(const wchar_t *s) { +void wperror(wcharz_t s) { int e = errno; - if (s[0] != L'\0') { - std::fwprintf(stderr, L"%ls: ", s); + if (s.str[0] != L'\0') { + std::fwprintf(stderr, L"%ls: ", s.str); } std::fwprintf(stderr, L"%s\n", std::strerror(e)); } diff --git a/src/wutil.h b/src/wutil.h index a0565faee..24d2daf26 100644 --- a/src/wutil.h +++ b/src/wutil.h @@ -11,7 +11,7 @@ #include #ifdef __APPLE__ // This include is required on macOS 10.10 for locale_t -#include // IWYU pragma: keep +#include // IWYU pragma: keep #endif #include @@ -24,6 +24,18 @@ #include "common.h" #include "maybe.h" +/// A POD wrapper around a null-terminated string, for ffi purposes. +/// This trivial type may be converted to and from const wchar_t *. +struct wcharz_t { + const wchar_t *str; + + /* implicit */ wcharz_t(const wchar_t *s) : str(s) {} + operator const wchar_t *() const { return str; } + + inline size_t size() const { return wcslen(str); } + inline size_t length() const { return size(); } +}; + class autoclose_fd_t; /// Wide character version of opendir(). Note that opendir() is guaranteed to set close-on-exec by @@ -43,7 +55,7 @@ int waccess(const wcstring &file_name, int mode); int wunlink(const wcstring &file_name); /// Wide character version of perror(). -void wperror(const wchar_t *s); +void wperror(wcharz_t s); /// Wide character version of getcwd(). wcstring wgetcwd();