From 6040ddc1c893e3b19d557ee27eb32f1df67b593f Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Thu, 5 Dec 2024 19:15:36 +0100 Subject: [PATCH] More code yay --- .github/FUNDING.yml | 1 + .github/workflows/release-plz.yml | 28 +++++++ .github/workflows/test.yml | 27 +++++++ Cargo.lock | 4 + Cargo.toml | 6 +- README.md | 19 ++--- con-cli/README.md | 2 + con-loader/README.md | 20 +++++ con/README.md | 119 ++++++++++++++++++++++++++++-- test-workspace/Cargo.toml | 23 ++++++ test-workspace/app/Cargo.toml | 6 ++ test-workspace/app/src/main.rs | 3 + 12 files changed, 238 insertions(+), 20 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/release-plz.yml create mode 100644 .github/workflows/test.yml create mode 100644 test-workspace/Cargo.toml create mode 100644 test-workspace/app/Cargo.toml create mode 100644 test-workspace/app/src/main.rs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7eac648 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [fasterthanlime] diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..8d5bf80 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,28 @@ +name: Release-plz + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: + - main + +jobs: + release-plz: + name: Release-plz + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PAT }} + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + env: + GITHUB_TOKEN: ${{ secrets.PAT }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..842432e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + merge_group: + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Install tools + uses: taiki-e/install-action@v2 + with: + tool: cargo-hack,just + - name: Run tests + shell: bash + run: | + rustup toolchain install nightly --component miri + just diff --git a/Cargo.lock b/Cargo.lock index a7e1d0e..98fd92e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,10 @@ dependencies = [ "memchr", ] +[[package]] +name = "app" +version = "0.1.0" + [[package]] name = "autocfg" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index 5022b4a..a290725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,11 @@ [workspace] members = [ "con", - "con-cli", "con-loader", + "con-cli", + "con-loader", "test-workspace/app", +] +exclude = [ + "test-workspace", ] resolver = "2" diff --git a/README.md b/README.md index 298b523..294dbcd 100644 --- a/README.md +++ b/README.md @@ -58,17 +58,8 @@ doesn't transform the token stream at all. It merely defines attributes like `[con::export]`, that a separate tool, `con-cli`, will look for, to know which trait definitions to generate. -In short, running `con` in a workspace will: - - * Find all crates whose name start with `mod-` - * Generating corresponding `con-` crates, containing: - * Data types (enums, structs, etc.) - * Traits (dyn-compatible ones, usually Send + Sync as well) - * Patch the `mod-` crate to `include!(".con/spec.rs")` so that the traits - are defined on that side, as well. - * Patch the `con-` crate to avoid depending on `con` there, remove all - dev dependencies, and remove the `impl` feature from "default" traits - * Add building and loading code to the `con-` crate itself - -The source for the `con-` crates is based on `src/lib.rs`, with the impl blocks removed, and -anything with a `#[cfg(feature = "impl")]` removed (caveat: for now only top-level items). +For more information, read crate-level documentations for: + + * [con-cli](https://crates.io/crates/con-cli) + * [con](https://crates.io/crates/con) + * [con-loader](https://crates.io/crates/con-loader) diff --git a/con-cli/README.md b/con-cli/README.md index 160670f..6cafac6 100644 --- a/con-cli/README.md +++ b/con-cli/README.md @@ -12,6 +12,8 @@ cargo install con-cli ``` +Note that con-cli needs `rustfmt` to be present at runtime. + ## Usage The CLI expects to be run from the root of a Cargo workspace containing mod crates. It will: diff --git a/con-loader/README.md b/con-loader/README.md index efba64e..d09cd0a 100644 --- a/con-loader/README.md +++ b/con-loader/README.md @@ -18,6 +18,14 @@ behavior of con-loader at runtime: In production, you probably want `CON_AUTOMATIC_MOD_BUILD` to be set to zero, as your mods should be pre-built, and put in the right place, next to the executable. +> **Warning** +> Make sure to build your mods with the `rubicon/import-globals` and `impl` +> features enabled, just like `con-loader` would do. +> +> See the [rubicon docs](https://crates.io/crates/rubicon) for more details: essentially, your +> mods need to refer to the same process-local and thread-local variables that your main app does, +> or stuff like tokio etc. will break. + That is, if your Cargo workspace looks like this: ``` @@ -46,3 +54,15 @@ workspace/ Except it doesn't actually need to be in `target/debug/` of anywhere — this could all be in a container image under `/app` or whatever. + +## ABI Safety + +con-loader uses [rubicon](https://github.com/bearcove/rubicon) to ensure that the ABI of the +module matches the ABI of the app they're being loaded into — this is not the concern of the +"con" family of crates however. + +If you mess something up, you should get a detailed panic with colors and emojis explaining +exactly what you got wrong. + +Note that if you need crates like tokio, tracing, eyre, etc. you should use their +patched versions, see the [rubicon compatibility tracker](https://github.com/bearcove/rubicon/issues/3). diff --git a/con/README.md b/con/README.md index 8caf9bf..c88f8f1 100644 --- a/con/README.md +++ b/con/README.md @@ -77,15 +77,99 @@ impl Client for ClientImpl { } ``` +## Dependencies + +Because the `con-XXX` crate is generated from the `mod-XXX` crate, it shares some dependencies +with it: any types that appear in the public API must be available to the `con-XXX` crate as well. + +However, some types and functions and third-party crates are only used in the implemention. Those +can be feature-gated both in the `Cargo.toml` manifest: + +```toml +[package] +name = "mod-clap" +version = "0.1.0" +edition = "2021" + +[lib] +# this is important for mods — the `con-` version of this crate will be a "rlib" +crate-type = ["cdylib"] + +[dependencies] +# camino types are used in the public API of this mod +camino = "1" +con = "1" + +# impl deps are marked "optional" +clap = { version = "4.5.13", features = ["derive"], optional = true } + +[features] +default = ["impl"] +# ... and they are enabled by the "impl" feature, which is itself enabled by default +impl = ["dep:clap"] +``` + +And in the `src/lib.rs` code itself: + +```rust +#[cfg(feature = "impl")] +#[derive(Default)] +struct ModImpl; + +#[cfg_attr(feature = "impl", derive(Parser))] +pub struct Args { + #[cfg_attr(feature = "impl", clap(default_value = "."))] + /// config file + pub path: Utf8PathBuf, +} + +#[con::export] +impl Mod for ModImpl { + fn parse(&self) -> Args { + Args::parse() + } +} +``` + +In the `con` version of the crate, only the non-impl dependencies and items will remain: + +```toml +# rough outline of what `con-cli` would generate for this `con-clap` crate + +[package] +name = "con-clap" +version = "0.1.0" +edition = "2021" + +[dependencies] +# only the dependencies used in the public API +camino = "1" +``` + +```rust +// generated code for the public API +pub struct Args { + /// config file + pub path: Utf8PathBuf, +} + +pub trait Mod: Send + Sync + 'static { + fn parse(&self) -> Args; +} +``` + +Note that filtering out items with `#[cfg(feature = "impl")]` isn't done via something like +[-Zunpretty-expand](https://github.com/rust-lang/rust/issues/43364), for myriad reasons. It's +done by parsing the AST with [syn](https://crates.io/crates/syn), removing offending items and +attributes, then formatting the AST with rustfmt. + ## Limitations con will expect all your exported traits to be [`dyn`](https://doc.rust-lang.org/std/keyword.dyn.html)-compatible (this used to be call "object safe") Here's a list of things you cannot do. -### You cannot have generic type parameters - -Traits exported by con cannot have generic type parameters. +### Traits cannot be generic over types ```rust // ❌ This won't work @@ -95,9 +179,9 @@ impl Parser for JsonParser { } ``` -### You cannot have generic methods +### Function arguments or return types cannot be generic -Methods in exported traits cannot have generic type parameters. Use trait objects or concrete types instead: +Methods in exported traits cannot have generic type parameters. ```rust // ❌ This won't work @@ -120,6 +204,19 @@ impl Handler for MyHandler { } ``` +### You can be generic over lifetimes + +Unlike with type parameters, traits can be generic over lifetimes: + +```rust +#[con::export] +impl Parser<'a> for MyParser { + fn parse(&self, input: &'a str) -> Result<&'a str>; +} +``` + +This works because lifetimes are erased at compile time and don't affect dynamic dispatch. + ### You can (and should) use boxed trait objects A surprising amount of things can be achieved through boxed trait objects if most of your traits are dyn-compatible: @@ -220,3 +317,15 @@ impl Builder for RequestBuilder { Essentially, as a consumer, we don't know the size of "Self" — so we need the indirection. References (`&self`, `&mut self`) are always fine. + +## Should `con` exist? + +Not really — much like [rubicon](https://github.com/bearcove/rubicon), all that +should be possible in stable Rust, with support from the compiler, etc. + +Half the reason to bother with an approach like con's is to avoid unnecessary +rebuilds. The _proper_ approach for that is being explored by other folks, see: + + * [Downstream dependencies of a crate are rebuilt despite the changes not being public-facing #14604](https://github.com/rust-lang/cargo/issues/14604) + +However, I live in the today, and for now I'll stick to my horrible codegen hacks. diff --git a/test-workspace/Cargo.toml b/test-workspace/Cargo.toml new file mode 100644 index 0000000..a290725 --- /dev/null +++ b/test-workspace/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +members = [ + "con", + "con-cli", + "con-loader", "test-workspace/app", +] +exclude = [ + "test-workspace", +] +resolver = "2" + +[profile.dev] +debug = 1 +split-debuginfo = "unpacked" +incremental = true + +[profile.dev.package."*"] +opt-level = 2 + +[profile.release] +debug = 1 +lto = "off" +split-debuginfo = "unpacked" diff --git a/test-workspace/app/Cargo.toml b/test-workspace/app/Cargo.toml new file mode 100644 index 0000000..211d3b2 --- /dev/null +++ b/test-workspace/app/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test-workspace/app/src/main.rs b/test-workspace/app/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/test-workspace/app/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}