From 4f9cd51f2898fb45efac6be5e8998330bbe3978b Mon Sep 17 00:00:00 2001 From: Zeeshan Ali Khan Date: Thu, 26 Mar 2026 13:12:14 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Split=20macro=20into=20?= =?UTF-8?q?a=20separate=20sub-crate,=20firmware-controller-macros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to directly depend on the needed crates in the main crate, instead of assuming the using crate provides them. The `controller` macro is re-exported through the main crate and we recommend using it through it. This is also consistent with how most Rust projects handle macros. --- .githooks/util.sh | 8 +- .github/workflows/rust.yaml | 8 +- CLAUDE.md | 110 ++++++--- CONTRIBUTING.md | 4 +- Cargo.lock | 117 +++++---- Cargo.toml | 50 +--- README.md | 16 -- firmware-controller-macros/Cargo.toml | 23 ++ .../src}/controller/item_impl.rs | 233 +++++++++--------- .../src}/controller/item_struct.rs | 36 +-- .../src}/controller/mod.rs | 5 + .../src}/lib.rs | 8 +- .../src}/util.rs | 0 firmware-controller/Cargo.toml | 60 +++++ firmware-controller/src/lib.rs | 27 ++ .../tests}/integration.rs | 0 release-plz.toml | 20 +- 17 files changed, 418 insertions(+), 307 deletions(-) create mode 100644 firmware-controller-macros/Cargo.toml rename {src => firmware-controller-macros/src}/controller/item_impl.rs (85%) rename {src => firmware-controller-macros/src}/controller/item_struct.rs (93%) rename {src => firmware-controller-macros/src}/controller/mod.rs (94%) rename {src => firmware-controller-macros/src}/lib.rs (72%) rename {src => firmware-controller-macros/src}/util.rs (100%) create mode 100644 firmware-controller/Cargo.toml create mode 100644 firmware-controller/src/lib.rs rename {tests => firmware-controller/tests}/integration.rs (100%) diff --git a/.githooks/util.sh b/.githooks/util.sh index 82f535f..51a333d 100644 --- a/.githooks/util.sh +++ b/.githooks/util.sh @@ -77,15 +77,15 @@ function ensure_clippy_installed() { } function check_formatting() { - hook_info "🎨 Running 'cargo +nightly fmt -- --check'" - cargo +nightly fmt -- --check \ + hook_info "🎨 Running 'cargo +nightly fmt --all -- --check'" + cargo +nightly fmt --all -- --check \ && hook_success "Project is formatted" \ || hook_failure "Cargo format detected errors." } function check_clippy() { - hook_info "🔍 Running 'cargo clippy -- -D warnings'" - cargo clippy -- -D warnings \ + hook_info "🔍 Running 'cargo clippy --workspace -- -D warnings'" + cargo clippy --workspace -- -D warnings \ && hook_success "Clippy detected no issues" \ || hook_failure "Cargo clippy detected errors." } diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 0a6805d..9c66bd3 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -21,7 +21,9 @@ jobs: with: toolchain: stable - name: Test - run: cargo --locked test --no-default-features --features "${{ matrix.backend }}" + run: >- + cargo --locked test --workspace --no-default-features + --features "${{ matrix.backend }}" fmt: runs-on: ubuntu-latest @@ -32,7 +34,7 @@ jobs: toolchain: nightly components: rustfmt - name: Check formatting - run: cargo --locked fmt -- --check + run: cargo --locked fmt --all -- --check clippy: runs-on: ubuntu-latest @@ -47,5 +49,5 @@ jobs: components: clippy - name: Catch common mistakes run: >- - cargo --locked clippy --no-default-features + cargo --locked clippy --workspace --no-default-features --features "${{ matrix.backend }}" -- -D warnings diff --git a/CLAUDE.md b/CLAUDE.md index bd52fa0..abbea34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,20 +1,33 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this +repository. ## Project Overview -This is a procedural macro crate that provides the `#[controller]` attribute macro for -firmware/actor development. By default it targets `no_std` environments using embassy. With the -`tokio` feature, it generates code for `std` environments using tokio. The macro generates -boilerplate for decoupling component interactions through: +This is a workspace with two crates that provide the `#[controller]` attribute macro for +firmware/actor development: + +* **`firmware-controller`** (main crate): Re-exports the macro from `-macros` and provides + `#[doc(hidden)]` re-exports of runtime dependencies (`futures`, `embassy-sync`, `embassy-time`, + `tokio`, `tokio-stream`) so generated code can reference them without requiring users to add + these dependencies manually. +* **`firmware-controller-macros`** (proc-macro crate): Contains the actual procedural macro + implementation. Generated code references external crates through + `::firmware_controller::__private::` re-exports. + +By default it targets `no_std` environments using embassy. With the `tokio` feature, it generates +code for `std` environments using tokio. The macro generates boilerplate for decoupling component +interactions through: * A controller struct that manages peripheral state. * Client API for sending commands to the controller. * Signal mechanism for broadcasting events (PubSubChannel). * Watch-based subscriptions for state change notifications (yields current value first). -The macro is applied to a module containing both the controller struct definition and its impl block, allowing coordinated code generation of the controller infrastructure, client API, and communication channels. +The macro is applied to a module containing both the controller struct definition and its impl +block, allowing coordinated code generation of the controller infrastructure, client API, and +communication channels. ## Build & Test Commands @@ -29,14 +42,14 @@ cargo test --locked --no-default-features --features tokio cargo test --locked # Check formatting (requires nightly) -cargo +nightly fmt -- --check +cargo +nightly fmt --all -- --check # Auto-format code (requires nightly) -cargo +nightly fmt +cargo +nightly fmt --all # Run clippy for both backends (CI fails on warnings) -cargo clippy --locked -- -D warnings -cargo clippy --locked --no-default-features --features tokio -- -D warnings +cargo clippy --workspace --locked -- -D warnings +cargo clippy --workspace --locked --no-default-features --features tokio -- -D warnings # Build the crate cargo build --locked @@ -47,27 +60,52 @@ cargo doc --locked ## Architecture +### Workspace Layout + +``` +firmware-controller/ # workspace root +├── firmware-controller/ # main/facade crate +│ ├── src/lib.rs # re-exports macro + #[doc(hidden)] deps +│ └── tests/integration.rs +└── firmware-controller-macros/ # proc-macro crate + └── src/ + ├── lib.rs # macro entry point + ├── util.rs # case conversion helpers + └── controller/ + ├── mod.rs # module orchestration + private_mod_path() + ├── item_struct.rs # struct field processing + └── item_impl.rs # impl block processing +``` + ### Backend Selection -The crate has two mutually exclusive features: `embassy` (default) and `tokio`. Code generation -functions use `#[cfg(feature = "...")]` in the proc macro code (not in generated code) to select -which token streams to emit. When `tokio` is enabled: +The crate has two mutually exclusive features: `embassy` (default) and `tokio`. The main crate +forwards these features to the macros crate and conditionally depends on the corresponding runtime +crates. Code generation functions use `#[cfg(feature = "...")]` in the proc macro code (not in +generated code) to select which token streams to emit. When `tokio` is enabled: -* `embassy_sync::channel::Channel` → `tokio::sync::mpsc` + `tokio::sync::oneshot` +* `embassy_sync::channel::Channel` -> `tokio::sync::mpsc` + `tokio::sync::oneshot` (request/response actor pattern) -* `embassy_sync::watch::Watch` → `tokio::sync::watch` (via `std::sync::OnceLock`) -* `embassy_sync::pubsub::PubSubChannel` → `tokio::sync::broadcast` +* `embassy_sync::watch::Watch` -> `tokio::sync::watch` (via `std::sync::OnceLock`) +* `embassy_sync::pubsub::PubSubChannel` -> `tokio::sync::broadcast` (via `std::sync::LazyLock`, with `tokio_stream::wrappers::BroadcastStream`) * Watch subscribers use `tokio_stream::wrappers::WatchStream`. -* `embassy_time::Ticker` → `tokio::time::interval` -* `futures::select_biased!` → `tokio::select! { biased; ... }` +* `embassy_time::Ticker` -> `tokio::time::interval` +* `futures::select_biased!` -> `tokio::select! { biased; ... }` * Static channels use `std::sync::LazyLock` since tokio channels lack const constructors. -### Macro Entry Point (`src/lib.rs`) +### Re-export Pattern + +Generated code references external crates through the main crate's `__private` module: +`::firmware_controller::__private::embassy_sync::...` etc. The `private_mod_path()` function in +`controller/mod.rs` returns this path as a `TokenStream`, and each code-generation function binds +it to a local `__priv` variable for use in `quote!` blocks. + +### Macro Entry Point (`firmware-controller-macros/src/lib.rs`) The `controller` attribute macro parses the input as an `ItemMod` (module) and calls `controller::expand_module()`. -### Module Processing (`src/controller/mod.rs`) +### Module Processing (`firmware-controller-macros/src/controller/mod.rs`) The `expand_module()` function: * Validates the module has a body with exactly one struct and one impl block. * Extracts the struct and impl items from the module. @@ -82,7 +120,7 @@ Channel capacities and subscriber limits are also defined here: * `BROADCAST_MAX_SUBSCRIBERS`: 16 (Watch for published fields, PubSubChannel for signals, embassy only) -### Struct Processing (`src/controller/item_struct.rs`) +### Struct Processing (`firmware-controller-macros/src/controller/item_struct.rs`) Processes the controller struct definition. Supports three field attributes: **`#[controller(publish)]`** - Enables state change subscriptions: @@ -105,7 +143,7 @@ The generated `new()` method returns `Option`, enforcing singleton semanti both user fields and generated sender fields, and sends initial values to Watch channels so subscribers get them immediately. -### Impl Processing (`src/controller/item_impl.rs`) +### Impl Processing (`firmware-controller-macros/src/controller/item_impl.rs`) Processes the controller impl block. Distinguishes between: **Proxied methods** (normal methods): @@ -137,29 +175,19 @@ Processes the controller impl block. Distinguishes between: * Generates client-side getter methods that request current field value. * Generates client-side setter methods that update field value (and broadcast if published). -The generated `run()` method contains a `select_biased!` (or `tokio::select! { biased; ... }`) loop -that receives method calls from clients, dispatches them to the user's implementations, and handles -periodic poll method calls. +The generated `run()` method contains a `select_biased!` (or `tokio::select! { biased; ... }`) +loop that receives method calls from clients, dispatches them to the user's implementations, and +handles periodic poll method calls. -### Utilities (`src/util.rs`) -Case conversion functions (`pascal_to_snake_case`, `snake_to_pascal_case`) used for generating type and method names. +### Utilities (`firmware-controller-macros/src/util.rs`) +Case conversion functions (`pascal_to_snake_case`, `snake_to_pascal_case`) used for generating +type and method names. ## Dependencies -User code must have these dependencies (per README): - -**Default (embassy)**: -* `futures` with `async-await` feature. -* `embassy-sync` for channels and synchronization. -* `embassy-time` for poll method timing (only required if using poll methods). - -**With `tokio` feature**: -* `futures` with `async-await` feature. -* `tokio` with `sync` feature (and `time` if using poll methods). -* `tokio-stream` with `sync` feature. - -Dev dependencies include `embassy-executor`, `embassy-time`, `tokio`, and `tokio-stream` for -testing. +The main crate directly depends on all runtime crates needed by generated code. Users only need +`firmware-controller` in their `Cargo.toml`. Dev dependencies (`embassy-executor`, `tokio` with +test features, etc.) are only needed for the test suite. ## Key Limitations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0c93e0..b50e787 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,7 +58,7 @@ than waiting for the CI servers to run tests for you. ```sh # Run the full test suite, including doc test and compile-tests -cargo test --all-features +cargo test --locked ``` Also please ensure that code is formatted correctly by running: @@ -70,7 +70,7 @@ cargo +nightly fmt --all and clippy doesn't see anything wrong with the code: ```sh -cargo clippy -- -D warnings +cargo clippy --workspace --locked -- -D warnings ``` Please note that there are times when clippy is wrong and you know what you are doing. In such diff --git a/Cargo.lock b/Cargo.lock index 5d1838f..5b3c6bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,6 @@ dependencies = [ "critical-section", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "byteorder" version = "1.5.0" @@ -31,9 +25,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "critical-section" @@ -78,9 +72,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -131,9 +125,9 @@ dependencies = [ [[package]] name = "embassy-time" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65" +checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a" dependencies = [ "cfg-if", "critical-section", @@ -148,9 +142,9 @@ dependencies = [ [[package]] name = "embassy-time-driver" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a244c7dc22c8d0289379c8d8830cae06bb93d8f990194d0de5efb3b5ae7ba6" +checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9" dependencies = [ "document-features", ] @@ -213,13 +207,20 @@ dependencies = [ "embassy-executor", "embassy-sync", "embassy-time", + "firmware-controller-macros", "futures", "heapless 0.7.17", + "tokio", + "tokio-stream", +] + +[[package]] +name = "firmware-controller-macros" +version = "0.5.0" +dependencies = [ "proc-macro2", "quote", "syn", - "tokio", - "tokio-stream", ] [[package]] @@ -230,9 +231,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -245,9 +246,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -255,15 +256,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -272,15 +273,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -289,21 +290,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -313,7 +314,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -366,25 +366,24 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nb" @@ -403,30 +402,24 @@ checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -454,9 +447,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "spin" @@ -469,9 +462,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -481,9 +474,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -538,9 +531,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "void" diff --git a/Cargo.toml b/Cargo.toml index 5bcbbb6..716354d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,47 +1,3 @@ -[package] -name = "firmware-controller" -description = "Controller (actor) macro to decouple interactions between components, supporting both embassy (no_std) and tokio (std) backends." -version = "0.5.0" -edition = "2021" -authors = [ - "Zeeshan Ali Khan ", - "Max Grollmann ", -] -license = "MIT" -repository = "https://github.com/layerx-world/firmware-controller/" - -[lib] -proc-macro = true - -[features] -default = ["embassy"] -embassy = [] -tokio = [] - -[dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["extra-traits", "fold", "full"] } - -[dev-dependencies] -heapless = { version = "0.7", default-features = false } -futures = { version = "0.3", default-features = false, features = [ - "async-await", - "std", - "executor", -] } -critical-section = { version = "1.2", features = ["std"] } -embassy-sync = "0.7.2" -embassy-executor = { version = "0.9.1", features = [ - "arch-std", - "executor-thread", -] } -embassy-time = { version = "0.5.0", features = ["mock-driver"] } -tokio = { version = "1", features = [ - "macros", - "rt", - "sync", - "test-util", - "time", -] } -tokio-stream = { version = "0.1", features = ["sync"] } +[workspace] +members = ["firmware-controller", "firmware-controller-macros"] +resolver = "2" diff --git a/README.md b/README.md index 7507a2f..c35ab59 100644 --- a/README.md +++ b/README.md @@ -271,22 +271,6 @@ When the `tokio` feature is enabled: 'static` (required by `tokio::sync::broadcast`), and published field types must implement `Send + Sync` (required by `tokio::sync::watch`). These constraints do not apply to the `embassy` backend. -## Dependencies assumed - -The `controller` macro assumes that you have the following dependencies in your `Cargo.toml`: - -### Default (embassy) - -* `futures` with `async-await` feature enabled. -* `embassy-sync` -* `embassy-time` (only required if using poll methods) - -### With `tokio` feature - -* `futures` with `async-await` feature enabled. -* `tokio` with `sync` feature (and `time` if using poll methods) -* `tokio-stream` with `sync` feature - ## Known limitations & Caveats * Method args/return type can't be reference types. diff --git a/firmware-controller-macros/Cargo.toml b/firmware-controller-macros/Cargo.toml new file mode 100644 index 0000000..ac21869 --- /dev/null +++ b/firmware-controller-macros/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "firmware-controller-macros" +description = "Procedural macros for firmware-controller. Do not use directly." +version = "0.5.0" +edition = "2021" +authors = [ + "Zeeshan Ali Khan ", + "Max Grollmann ", +] +license = "MIT" +repository = "https://github.com/layerx-world/firmware-controller/" + +[lib] +proc-macro = true + +[features] +embassy = [] +tokio = [] + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["extra-traits", "fold", "full"] } diff --git a/src/controller/item_impl.rs b/firmware-controller-macros/src/controller/item_impl.rs similarity index 85% rename from src/controller/item_impl.rs rename to firmware-controller-macros/src/controller/item_impl.rs index ac7f82b..7d4c9ac 100644 --- a/src/controller/item_impl.rs +++ b/firmware-controller-macros/src/controller/item_impl.rs @@ -176,8 +176,9 @@ fn generate_select_body( getter_arms: &[&TokenStream], poll_arms: &[TokenStream], ) -> TokenStream { + let __priv = super::private_mod_path(); quote! { - futures::select_biased! { + #__priv::futures::select_biased! { #(#arms,)* #(#setter_arms,)* #(#getter_arms,)* @@ -193,8 +194,9 @@ fn generate_select_body( getter_arms: &[&TokenStream], poll_arms: &[TokenStream], ) -> TokenStream { + let __priv = super::private_mod_path(); quote! { - tokio::select! { + #__priv::tokio::select! { biased; #(#arms)* #(#setter_arms)* @@ -329,6 +331,7 @@ impl<'a> ProxiedMethodArgs<'a> { #[cfg(feature = "embassy")] impl ProxiedMethodArgs<'_> { fn generate(&self, struct_name: &Ident) -> ProxiedMethod { + let __priv = super::private_mod_path(); let method_name = &self.method.sig.ident; let method_name_str = method_name.to_string(); let in_types = &self.in_args.types; @@ -350,17 +353,17 @@ impl ProxiedMethodArgs<'_> { let args_channel_declarations = quote! { static #input_channel_name: - embassy_sync::channel::Channel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::channel::Channel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, (#(#in_types),*), #capacity, - > = embassy_sync::channel::Channel::new(); + > = #__priv::embassy_sync::channel::Channel::new(); static #output_channel_name: - embassy_sync::channel::Channel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::channel::Channel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #out_type, #capacity, - > = embassy_sync::channel::Channel::new(); + > = #__priv::embassy_sync::channel::Channel::new(); }; let input_channel_rx_name = @@ -369,25 +372,25 @@ impl ProxiedMethodArgs<'_> { Ident::new(&format!("{method_name_str}_tx"), self.method.span()); let args_channels_rx_tx = quote! { let #input_channel_rx_name = - embassy_sync::channel::Channel::receiver( + #__priv::embassy_sync::channel::Channel::receiver( &#input_channel_name, ); let #output_channel_tx_name = - embassy_sync::channel::Channel::sender( + #__priv::embassy_sync::channel::Channel::sender( &#output_channel_name, ); }; let select_arm = quote! { - (#(#in_names),*) = futures::FutureExt::fuse( - embassy_sync::channel::Receiver::receive( + (#(#in_names),*) = #__priv::futures::FutureExt::fuse( + #__priv::embassy_sync::channel::Receiver::receive( &#input_channel_rx_name, ), ) => { let ret = self.#method_name(#(#in_names),*).await; - embassy_sync::channel::Sender::send( + #__priv::embassy_sync::channel::Sender::send( &#output_channel_tx_name, ret, ).await; @@ -406,29 +409,29 @@ impl ProxiedMethodArgs<'_> { let mut client_method = self.method.clone(); client_method.block = parse_quote!({ // Method call. - embassy_sync::channel::Sender::send( + #__priv::embassy_sync::channel::Sender::send( &self.#input_channel_tx_name, #in_names_tuple, ).await; // Method return. - embassy_sync::channel::Receiver::receive( + #__priv::embassy_sync::channel::Receiver::receive( &self.#output_channel_rx_name, ).await }); let client_method_tx_rx_declarations = quote! { #input_channel_tx_name: - embassy_sync::channel::Sender< + #__priv::embassy_sync::channel::Sender< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, (#(#in_types),*), #capacity, >, #output_channel_rx_name: - embassy_sync::channel::Receiver< + #__priv::embassy_sync::channel::Receiver< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #out_type, #capacity, >, @@ -436,11 +439,11 @@ impl ProxiedMethodArgs<'_> { let client_method_tx_rx_initializations = quote! { #input_channel_tx_name: - embassy_sync::channel::Channel::sender( + #__priv::embassy_sync::channel::Channel::sender( &#input_channel_name, ), #output_channel_rx_name: - embassy_sync::channel::Channel::receiver( + #__priv::embassy_sync::channel::Channel::receiver( &#output_channel_name, ), }; @@ -459,6 +462,7 @@ impl ProxiedMethodArgs<'_> { #[cfg(feature = "tokio")] impl ProxiedMethodArgs<'_> { fn generate(&self, struct_name: &Ident) -> ProxiedMethod { + let __priv = super::private_mod_path(); let method_name = &self.method.sig.ident; let method_name_str = method_name.to_string(); let in_types = &self.in_args.types; @@ -476,21 +480,21 @@ impl ProxiedMethodArgs<'_> { let args_channel_declarations = quote! { static #channel_name: std::sync::LazyLock<( - tokio::sync::mpsc::Sender<( + #__priv::tokio::sync::mpsc::Sender<( (#(#in_types,)*), - tokio::sync::oneshot::Sender<#out_type>, + #__priv::tokio::sync::oneshot::Sender<#out_type>, )>, std::sync::Mutex< Option< - tokio::sync::mpsc::Receiver<( + #__priv::tokio::sync::mpsc::Receiver<( (#(#in_types,)*), - tokio::sync::oneshot::Sender<#out_type>, + #__priv::tokio::sync::oneshot::Sender<#out_type>, )>, >, >, )> = std::sync::LazyLock::new(|| { let (tx, rx) = - tokio::sync::mpsc::channel(#capacity); + #__priv::tokio::sync::mpsc::channel(#capacity); (tx, std::sync::Mutex::new(Some(rx))) }); }; @@ -524,7 +528,7 @@ impl ProxiedMethodArgs<'_> { let mut client_method = self.method.clone(); client_method.block = parse_quote!({ let (__resp_tx, __resp_rx) = - tokio::sync::oneshot::channel(); + #__priv::tokio::sync::oneshot::channel(); self.#tx_name .send((#args, __resp_tx)) .await @@ -533,9 +537,9 @@ impl ProxiedMethodArgs<'_> { }); let client_method_tx_rx_declarations = quote! { - #tx_name: tokio::sync::mpsc::Sender<( + #tx_name: #__priv::tokio::sync::mpsc::Sender<( (#(#in_types,)*), - tokio::sync::oneshot::Sender<#out_type>, + #__priv::tokio::sync::oneshot::Sender<#out_type>, )>, }; @@ -579,6 +583,7 @@ impl Signal { struct_name: &Ident, args: &MethodInputArgs, ) -> Result { + let __priv = super::private_mod_path(); let MethodInputArgs { types, names } = args; let method_name = &method.sig.ident; @@ -607,25 +612,25 @@ impl Signal { let declarations = quote! { static #signal_channel_name: - embassy_sync::pubsub::PubSubChannel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::pubsub::PubSubChannel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #args_struct_name, #capacity, #max_subscribers, #max_publishers, - > = embassy_sync::pubsub::PubSubChannel::new(); + > = #__priv::embassy_sync::pubsub::PubSubChannel::new(); static #signal_publisher_name: - embassy_sync::once_lock::OnceLock< - embassy_sync::pubsub::publisher::Publisher< + #__priv::embassy_sync::once_lock::OnceLock< + #__priv::embassy_sync::pubsub::publisher::Publisher< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #args_struct_name, #capacity, #max_subscribers, #max_publishers, >, - > = embassy_sync::once_lock::OnceLock::new(); + > = #__priv::embassy_sync::once_lock::OnceLock::new(); #[derive(Debug, Clone)] pub struct #args_struct_name { @@ -634,9 +639,9 @@ impl Signal { pub struct #subscriber_struct_name { subscriber: - embassy_sync::pubsub::subscriber::Subscriber< + #__priv::embassy_sync::pubsub::subscriber::Subscriber< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #args_struct_name, #capacity, #max_subscribers, @@ -646,7 +651,7 @@ impl Signal { impl #subscriber_struct_name { pub fn new() -> Option { - embassy_sync::pubsub::PubSubChannel::subscriber( + #__priv::embassy_sync::pubsub::PubSubChannel::subscriber( &#signal_channel_name, ) .ok() @@ -654,7 +659,7 @@ impl Signal { } } - impl futures::Stream for #subscriber_struct_name { + impl #__priv::futures::Stream for #subscriber_struct_name { type Item = #args_struct_name; fn poll_next( @@ -664,22 +669,22 @@ impl Signal { let subscriber = core::pin::Pin::new( &mut *self.get_mut().subscriber, ); - futures::Stream::poll_next(subscriber, cx) + #__priv::futures::Stream::poll_next(subscriber, cx) } } }; method.block = parse_quote!({ let publisher = - embassy_sync::once_lock::OnceLock::get_or_init( + #__priv::embassy_sync::once_lock::OnceLock::get_or_init( &#signal_publisher_name, // Safety: The publisher is only initialized once. - || embassy_sync::pubsub::PubSubChannel::publisher( + || #__priv::embassy_sync::pubsub::PubSubChannel::publisher( &#signal_channel_name, ) .unwrap(), ); - embassy_sync::pubsub::publisher::Pub::publish( + #__priv::embassy_sync::pubsub::publisher::Pub::publish( publisher, #args_struct_name { #(#names),* }, ) @@ -704,6 +709,7 @@ impl Signal { struct_name: &Ident, args: &MethodInputArgs, ) -> Result { + let __priv = super::private_mod_path(); let MethodInputArgs { types, names } = args; let method_name = &method.sig.ident; @@ -727,12 +733,12 @@ impl Signal { let declarations = quote! { static #signal_channel_name: std::sync::LazyLock< - tokio::sync::broadcast::Sender< + #__priv::tokio::sync::broadcast::Sender< #args_struct_name, >, > = std::sync::LazyLock::new(|| { let (tx, _) = - tokio::sync::broadcast::channel( + #__priv::tokio::sync::broadcast::channel( #capacity, ); tx @@ -746,7 +752,7 @@ impl Signal { pub struct #subscriber_struct_name { inner: core::pin::Pin< Box< - dyn futures::Stream< + dyn #__priv::futures::Stream< Item = #args_struct_name, > + Send, >, @@ -755,11 +761,11 @@ impl Signal { impl #subscriber_struct_name { pub fn new() -> Option { - use futures::StreamExt; + use #__priv::futures::StreamExt; Some(Self { inner: Box::pin( - tokio_stream::wrappers::BroadcastStream::new( + #__priv::tokio_stream::wrappers::BroadcastStream::new( #signal_channel_name.subscribe(), ) .filter_map(|result| async move { @@ -770,7 +776,7 @@ impl Signal { } } - impl futures::Stream for #subscriber_struct_name { + impl #__priv::futures::Stream for #subscriber_struct_name { type Item = #args_struct_name; fn poll_next( @@ -811,15 +817,16 @@ enum PollDuration { #[cfg(feature = "embassy")] impl PollDuration { fn to_duration_expr(&self) -> TokenStream { + let __priv = super::private_mod_path(); match self { PollDuration::Seconds(n) => { - quote! { embassy_time::Duration::from_secs(#n) } + quote! { #__priv::embassy_time::Duration::from_secs(#n) } } PollDuration::Millis(n) => { - quote! { embassy_time::Duration::from_millis(#n) } + quote! { #__priv::embassy_time::Duration::from_millis(#n) } } PollDuration::Micros(n) => { - quote! { embassy_time::Duration::from_micros(#n) } + quote! { #__priv::embassy_time::Duration::from_micros(#n) } } } } @@ -1104,6 +1111,7 @@ struct PubGetter { #[cfg(feature = "embassy")] fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSetter { + let __priv = super::private_mod_path(); let field_name = &field.field_name; let field_type = &field.field_type; let setter_method_name = &field.setter_name; @@ -1127,17 +1135,17 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette let channel_declarations = quote! { static #input_channel_name: - embassy_sync::channel::Channel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::channel::Channel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #field_type, #capacity, - > = embassy_sync::channel::Channel::new(); + > = #__priv::embassy_sync::channel::Channel::new(); static #output_channel_name: - embassy_sync::channel::Channel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::channel::Channel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, (), #capacity, - > = embassy_sync::channel::Channel::new(); + > = #__priv::embassy_sync::channel::Channel::new(); }; let input_channel_rx_name = @@ -1146,11 +1154,11 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette Ident::new(&format!("{}_ack_tx", field_name_str), field_name.span()); let rx_tx = quote! { let #input_channel_rx_name = - embassy_sync::channel::Channel::receiver( + #__priv::embassy_sync::channel::Channel::receiver( &#input_channel_name, ); let #output_channel_tx_name = - embassy_sync::channel::Channel::sender( + #__priv::embassy_sync::channel::Channel::sender( &#output_channel_name, ); }; @@ -1162,14 +1170,14 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette }; let select_arm = quote! { - value = futures::FutureExt::fuse( - embassy_sync::channel::Receiver::receive( + value = #__priv::futures::FutureExt::fuse( + #__priv::embassy_sync::channel::Receiver::receive( &#input_channel_rx_name, ), ) => { #setter_body - embassy_sync::channel::Sender::send( + #__priv::embassy_sync::channel::Sender::send( &#output_channel_tx_name, (), ).await; @@ -1185,11 +1193,11 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette &self, value: #field_type, ) { - embassy_sync::channel::Sender::send( + #__priv::embassy_sync::channel::Sender::send( &self.#input_channel_tx_name, value, ).await; - embassy_sync::channel::Receiver::receive( + #__priv::embassy_sync::channel::Receiver::receive( &self.#output_channel_rx_name, ).await } @@ -1197,16 +1205,16 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette let client_tx_rx_declarations = quote! { #input_channel_tx_name: - embassy_sync::channel::Sender< + #__priv::embassy_sync::channel::Sender< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #field_type, #capacity, >, #output_channel_rx_name: - embassy_sync::channel::Receiver< + #__priv::embassy_sync::channel::Receiver< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, (), #capacity, >, @@ -1214,11 +1222,11 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette let client_tx_rx_initializations = quote! { #input_channel_tx_name: - embassy_sync::channel::Channel::sender( + #__priv::embassy_sync::channel::Channel::sender( &#input_channel_name, ), #output_channel_rx_name: - embassy_sync::channel::Channel::receiver( + #__priv::embassy_sync::channel::Channel::receiver( &#output_channel_name, ), }; @@ -1235,6 +1243,7 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette #[cfg(feature = "tokio")] fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSetter { + let __priv = super::private_mod_path(); let field_name = &field.field_name; let field_type = &field.field_type; let setter_method_name = &field.setter_name; @@ -1251,21 +1260,21 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette let channel_declarations = quote! { static #channel_name: std::sync::LazyLock<( - tokio::sync::mpsc::Sender<( + #__priv::tokio::sync::mpsc::Sender<( #field_type, - tokio::sync::oneshot::Sender<()>, + #__priv::tokio::sync::oneshot::Sender<()>, )>, std::sync::Mutex< Option< - tokio::sync::mpsc::Receiver<( + #__priv::tokio::sync::mpsc::Receiver<( #field_type, - tokio::sync::oneshot::Sender<()>, + #__priv::tokio::sync::oneshot::Sender<()>, )>, >, >, )> = std::sync::LazyLock::new(|| { let (tx, rx) = - tokio::sync::mpsc::channel(#capacity); + #__priv::tokio::sync::mpsc::channel(#capacity); (tx, std::sync::Mutex::new(Some(rx))) }); }; @@ -1302,7 +1311,7 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette value: #field_type, ) { let (__resp_tx, __resp_rx) = - tokio::sync::oneshot::channel(); + #__priv::tokio::sync::oneshot::channel(); self.#tx_name .send((value, __resp_tx)) .await @@ -1312,9 +1321,9 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette }; let client_tx_rx_declarations = quote! { - #tx_name: tokio::sync::mpsc::Sender<( + #tx_name: #__priv::tokio::sync::mpsc::Sender<( #field_type, - tokio::sync::oneshot::Sender<()>, + #__priv::tokio::sync::oneshot::Sender<()>, )>, }; @@ -1334,6 +1343,7 @@ fn generate_pub_setter(field: &SetterFieldInfo, struct_name: &Ident) -> PubSette #[cfg(feature = "embassy")] fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGetter { + let __priv = super::private_mod_path(); let field_name = &field.field_name; let field_type = &field.field_type; let getter_name = &field.getter_name; @@ -1357,17 +1367,17 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette let channel_declarations = quote! { static #input_channel_name: - embassy_sync::channel::Channel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::channel::Channel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, (), #capacity, - > = embassy_sync::channel::Channel::new(); + > = #__priv::embassy_sync::channel::Channel::new(); static #output_channel_name: - embassy_sync::channel::Channel< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::channel::Channel< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #field_type, #capacity, - > = embassy_sync::channel::Channel::new(); + > = #__priv::embassy_sync::channel::Channel::new(); }; let input_channel_rx_name = Ident::new( @@ -1380,25 +1390,25 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette ); let rx_tx = quote! { let #input_channel_rx_name = - embassy_sync::channel::Channel::receiver( + #__priv::embassy_sync::channel::Channel::receiver( &#input_channel_name, ); let #output_channel_tx_name = - embassy_sync::channel::Channel::sender( + #__priv::embassy_sync::channel::Channel::sender( &#output_channel_name, ); }; let select_arm = quote! { - _ = futures::FutureExt::fuse( - embassy_sync::channel::Receiver::receive( + _ = #__priv::futures::FutureExt::fuse( + #__priv::embassy_sync::channel::Receiver::receive( &#input_channel_rx_name, ), ) => { let value = core::clone::Clone::clone(&self.#field_name); - embassy_sync::channel::Sender::send( + #__priv::embassy_sync::channel::Sender::send( &#output_channel_tx_name, value, ).await; @@ -1415,11 +1425,11 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette ); let client_method = quote! { pub async fn #getter_name(&self) -> #field_type { - embassy_sync::channel::Sender::send( + #__priv::embassy_sync::channel::Sender::send( &self.#input_channel_tx_name, (), ).await; - embassy_sync::channel::Receiver::receive( + #__priv::embassy_sync::channel::Receiver::receive( &self.#output_channel_rx_name, ).await } @@ -1427,16 +1437,16 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette let client_tx_rx_declarations = quote! { #input_channel_tx_name: - embassy_sync::channel::Sender< + #__priv::embassy_sync::channel::Sender< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, (), #capacity, >, #output_channel_rx_name: - embassy_sync::channel::Receiver< + #__priv::embassy_sync::channel::Receiver< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #field_type, #capacity, >, @@ -1444,11 +1454,11 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette let client_tx_rx_initializations = quote! { #input_channel_tx_name: - embassy_sync::channel::Channel::sender( + #__priv::embassy_sync::channel::Channel::sender( &#input_channel_name, ), #output_channel_rx_name: - embassy_sync::channel::Channel::receiver( + #__priv::embassy_sync::channel::Channel::receiver( &#output_channel_name, ), }; @@ -1465,6 +1475,7 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette #[cfg(feature = "tokio")] fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGetter { + let __priv = super::private_mod_path(); let field_name = &field.field_name; let field_type = &field.field_type; let getter_name = &field.getter_name; @@ -1481,19 +1492,19 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette let channel_declarations = quote! { static #channel_name: std::sync::LazyLock<( - tokio::sync::mpsc::Sender< - tokio::sync::oneshot::Sender<#field_type>, + #__priv::tokio::sync::mpsc::Sender< + #__priv::tokio::sync::oneshot::Sender<#field_type>, >, std::sync::Mutex< Option< - tokio::sync::mpsc::Receiver< - tokio::sync::oneshot::Sender<#field_type>, + #__priv::tokio::sync::mpsc::Receiver< + #__priv::tokio::sync::oneshot::Sender<#field_type>, >, >, >, )> = std::sync::LazyLock::new(|| { let (tx, rx) = - tokio::sync::mpsc::channel(#capacity); + #__priv::tokio::sync::mpsc::channel(#capacity); (tx, std::sync::Mutex::new(Some(rx))) }); }; @@ -1520,15 +1531,15 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette let client_method = quote! { pub async fn #getter_name(&self) -> #field_type { let (__resp_tx, __resp_rx) = - tokio::sync::oneshot::channel(); + #__priv::tokio::sync::oneshot::channel(); self.#tx_name.send(__resp_tx).await.ok(); __resp_rx.await.unwrap() } }; let client_tx_rx_declarations = quote! { - #tx_name: tokio::sync::mpsc::Sender< - tokio::sync::oneshot::Sender<#field_type>, + #tx_name: #__priv::tokio::sync::mpsc::Sender< + #__priv::tokio::sync::oneshot::Sender<#field_type>, >, }; @@ -1554,6 +1565,7 @@ fn generate_pub_getter(field: &GetterFieldInfo, struct_name: &Ident) -> PubGette /// - select_arms: Select arms that wait on ticker.next()/tick(). #[cfg(feature = "embassy")] fn generate_poll_code(poll_methods: &[&PollMethod]) -> (Vec, Vec) { + let __priv = super::private_mod_path(); // Group poll methods by duration. let mut groups: BTreeMap> = BTreeMap::new(); for poll in poll_methods { @@ -1575,11 +1587,11 @@ fn generate_poll_code(poll_methods: &[&PollMethod]) -> (Vec, Vec { #(self.#method_names().await;)* @@ -1592,6 +1604,7 @@ fn generate_poll_code(poll_methods: &[&PollMethod]) -> (Vec, Vec (Vec, Vec) { + let __priv = super::private_mod_path(); // Group poll methods by duration. let mut groups: BTreeMap> = BTreeMap::new(); for poll in poll_methods { @@ -1613,7 +1626,7 @@ fn generate_poll_code(poll_methods: &[&PollMethod]) -> (Vec, Vec Result Result { + let __priv = super::private_mod_path(); let PublishNames { field_name, ty, @@ -386,16 +387,16 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { let sender_field_declaration = quote! { #sender_name: - embassy_sync::watch::Sender< + #__priv::embassy_sync::watch::Sender< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #ty, #max_subscribers, >, }; let sender_field_initialization = quote! { - #sender_name: embassy_sync::watch::Watch::sender(&#watch_channel_name), + #sender_name: #__priv::embassy_sync::watch::Watch::sender(&#watch_channel_name), }; // Watch send() is sync, but we keep the setter async for API compatibility. @@ -410,18 +411,18 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { let watch_channel_declaration = quote! { static #watch_channel_name: - embassy_sync::watch::Watch< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::watch::Watch< + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #ty, #max_subscribers, - > = embassy_sync::watch::Watch::new(); + > = #__priv::embassy_sync::watch::Watch::new(); }; let subscriber_declaration = quote! { pub struct #subscriber_struct_name { - receiver: embassy_sync::watch::Receiver< + receiver: #__priv::embassy_sync::watch::Receiver< 'static, - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + #__priv::embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, #ty, #max_subscribers, >, @@ -430,7 +431,7 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { impl #subscriber_struct_name { pub fn new() -> Option { - embassy_sync::watch::Watch::receiver( + #__priv::embassy_sync::watch::Watch::receiver( &#watch_channel_name, ) .map(|receiver| Self { @@ -440,7 +441,7 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { } } - impl futures::Stream for #subscriber_struct_name { + impl #__priv::futures::Stream for #subscriber_struct_name { type Item = #ty; fn poll_next( @@ -461,7 +462,7 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { // Create changed() future and poll it in place. let fut = this.receiver.changed(); - futures::pin_mut!(fut); + #__priv::futures::pin_mut!(fut); fut.poll(cx).map(Some) } } @@ -493,6 +494,7 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { #[cfg(feature = "tokio")] fn generate_publish_code_impl(n: &PublishNames) -> Result { + let __priv = super::private_mod_path(); let PublishNames { field_name, ty, @@ -515,26 +517,26 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { let watch_channel_declaration = quote! { static #watch_channel_name: - std::sync::OnceLock> + std::sync::OnceLock<#__priv::tokio::sync::watch::Sender<#ty>> = std::sync::OnceLock::new(); }; let subscriber_declaration = quote! { pub struct #subscriber_struct_name { - inner: tokio_stream::wrappers::WatchStream<#ty>, + inner: #__priv::tokio_stream::wrappers::WatchStream<#ty>, } impl #subscriber_struct_name { pub fn new() -> Option { #watch_channel_name.get().map(|sender| Self { - inner: tokio_stream::wrappers::WatchStream::new( + inner: #__priv::tokio_stream::wrappers::WatchStream::new( sender.subscribe(), ), }) } } - impl futures::Stream for #subscriber_struct_name { + impl #__priv::futures::Stream for #subscriber_struct_name { type Item = #ty; fn poll_next( @@ -542,7 +544,7 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { cx: &mut core::task::Context<'_>, ) -> core::task::Poll> { let this = self.get_mut(); - futures::Stream::poll_next( + #__priv::futures::Stream::poll_next( core::pin::Pin::new(&mut this.inner), cx, ) @@ -551,7 +553,7 @@ fn generate_publish_code_impl(n: &PublishNames) -> Result { }; let initial_value_send = quote! { - let (__tx, _) = tokio::sync::watch::channel( + let (__tx, _) = #__priv::tokio::sync::watch::channel( core::clone::Clone::clone(&__self.#field_name), ); #watch_channel_name.set(__tx).ok(); diff --git a/src/controller/mod.rs b/firmware-controller-macros/src/controller/mod.rs similarity index 94% rename from src/controller/mod.rs rename to firmware-controller-macros/src/controller/mod.rs index e03ef93..2b536d1 100644 --- a/src/controller/mod.rs +++ b/firmware-controller-macros/src/controller/mod.rs @@ -5,6 +5,11 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{spanned::Spanned, Item, ItemMod, Result}; +/// Returns the path to the `__private` module of the main crate. +pub(crate) fn private_mod_path() -> TokenStream { + quote!(::firmware_controller::__private) +} + const ALL_CHANNEL_CAPACITY: usize = 8; const SIGNAL_CHANNEL_CAPACITY: usize = 8; #[cfg(feature = "embassy")] diff --git a/src/lib.rs b/firmware-controller-macros/src/lib.rs similarity index 72% rename from src/lib.rs rename to firmware-controller-macros/src/lib.rs index 4f54590..0dbaf97 100644 --- a/src/lib.rs +++ b/firmware-controller-macros/src/lib.rs @@ -1,4 +1,7 @@ -#![doc = include_str!("../README.md")] +//! Procedural macros for `firmware-controller`. Do not use directly. +//! +//! See the [`firmware-controller`](https://docs.rs/firmware-controller) crate +//! for documentation. #[cfg(not(any(feature = "embassy", feature = "tokio")))] compile_error!("Either the `embassy` or `tokio` feature must be enabled"); @@ -12,7 +15,8 @@ use syn::{parse_macro_input, punctuated::Punctuated, ItemMod, Meta, Token}; mod controller; mod util; -/// See the crate-level documentation for more information. +/// See the [`firmware-controller`](https://docs.rs/firmware-controller) crate +/// for documentation. #[proc_macro_attribute] pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream { let _args = parse_macro_input!(attr with Punctuated::parse_terminated); diff --git a/src/util.rs b/firmware-controller-macros/src/util.rs similarity index 100% rename from src/util.rs rename to firmware-controller-macros/src/util.rs diff --git a/firmware-controller/Cargo.toml b/firmware-controller/Cargo.toml new file mode 100644 index 0000000..6cc8768 --- /dev/null +++ b/firmware-controller/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "firmware-controller" +description = "Controller (actor) macro to decouple interactions between components, supporting both embassy (no_std) and tokio (std) backends." +version = "0.5.0" +edition = "2021" +authors = [ + "Zeeshan Ali Khan ", + "Max Grollmann ", +] +license = "MIT" +repository = "https://github.com/layerx-world/firmware-controller/" +readme = "../README.md" + +[features] +default = ["embassy"] +embassy = [ + "firmware-controller-macros/embassy", + "dep:embassy-sync", + "dep:embassy-time", +] +tokio = [ + "firmware-controller-macros/tokio", + "dep:tokio", + "dep:tokio-stream", +] + +[dependencies] +firmware-controller-macros = { version = "=0.5.0", path = "../firmware-controller-macros", default-features = false } +futures = { version = "0.3", default-features = false, features = ["async-await"] } + +# Embassy backend dependencies. +embassy-sync = { version = "0.7.2", optional = true } +embassy-time = { version = "0.5.0", optional = true } + +# Tokio backend dependencies. +tokio = { version = "1", features = ["sync", "time"], optional = true } +tokio-stream = { version = "0.1", features = ["sync"], optional = true } + +[dev-dependencies] +heapless = { version = "0.7", default-features = false } +futures = { version = "0.3", default-features = false, features = [ + "async-await", + "std", + "executor", +] } +critical-section = { version = "1.2", features = ["std"] } +embassy-sync = "0.7.2" +embassy-executor = { version = "0.9.1", features = [ + "arch-std", + "executor-thread", +] } +embassy-time = { version = "0.5.0", features = ["mock-driver"] } +tokio = { version = "1", features = [ + "macros", + "rt", + "sync", + "test-util", + "time", +] } +tokio-stream = { version = "0.1", features = ["sync"] } diff --git a/firmware-controller/src/lib.rs b/firmware-controller/src/lib.rs new file mode 100644 index 0000000..0ba767f --- /dev/null +++ b/firmware-controller/src/lib.rs @@ -0,0 +1,27 @@ +#![doc = include_str!("../../README.md")] + +#[cfg(not(any(feature = "embassy", feature = "tokio")))] +compile_error!("Either the `embassy` or `tokio` feature must be enabled"); + +#[cfg(all(feature = "embassy", feature = "tokio"))] +compile_error!("The `embassy` and `tokio` features are mutually exclusive"); + +pub use firmware_controller_macros::controller; + +/// Re-exports for use by generated code. Not part of the public API. +#[doc(hidden)] +pub mod __private { + pub use futures; + + #[cfg(feature = "embassy")] + pub use embassy_sync; + + #[cfg(feature = "embassy")] + pub use embassy_time; + + #[cfg(feature = "tokio")] + pub use tokio; + + #[cfg(feature = "tokio")] + pub use tokio_stream; +} diff --git a/tests/integration.rs b/firmware-controller/tests/integration.rs similarity index 100% rename from tests/integration.rs rename to firmware-controller/tests/integration.rs diff --git a/release-plz.toml b/release-plz.toml index fe1cbdb..ad3d9f2 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -2,9 +2,8 @@ # https://release-plz.dev/docs/config [workspace] -# Use the existing tag naming convention. -git_tag_name = "v{{ version }}" -git_release_name = "Release {{ version }}" +# Disable changelog for all packages by default. +changelog_update = false # Enable all release features. git_release_enable = true @@ -20,6 +19,21 @@ custom_minor_increment_regex = """\ # PR configuration. pr_name = "🔖 Release" +# Main crate: owns the changelog and tag. +[[package]] +name = "firmware-controller" +changelog_update = true +changelog_path = "./CHANGELOG.md" +changelog_include = ["firmware-controller-macros"] +git_tag_name = "v{{ version }}" +git_release_name = "Release {{ version }}" + +# Macros crate: no changelog, no git tag/release (published to crates.io only). +[[package]] +name = "firmware-controller-macros" +git_tag_enable = false +git_release_enable = false + # Changelog configuration for gimoji-based commits. # See: https://zeenix.github.io/gimoji/ [changelog]