Skip to content

Commit

Permalink
More code yay
Browse files Browse the repository at this point in the history
  • Loading branch information
fasterthanlime committed Dec 5, 2024
1 parent 773e2de commit 6040ddc
Show file tree
Hide file tree
Showing 12 changed files with 238 additions and 20 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [fasterthanlime]
28 changes: 28 additions & 0 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
[workspace]
members = [
"con",
"con-cli", "con-loader",
"con-cli",
"con-loader", "test-workspace/app",
]
exclude = [
"test-workspace",
]
resolver = "2"

Expand Down
19 changes: 5 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions con-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions con-loader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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).
119 changes: 114 additions & 5 deletions con/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -95,9 +179,9 @@ impl Parser<T> for JsonParser<T> {
}
```

### 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
Expand All @@ -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:
Expand Down Expand Up @@ -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.
23 changes: 23 additions & 0 deletions test-workspace/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 6 additions & 0 deletions test-workspace/app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
3 changes: 3 additions & 0 deletions test-workspace/app/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

0 comments on commit 6040ddc

Please sign in to comment.