From 81d76fd87fd885cd01f0584a484274220ac1bd00 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 26 Sep 2026 01:40:57 +0530 Subject: [PATCH 1/6] Add optional linked TinyMemory module exports --- .github/workflows/ci.yml | 7 ++++++ crates/tinymemory-module/Cargo.lock | 6 ++--- crates/tinymemory-module/Cargo.toml | 5 ++++ crates/tinymemory-module/src/lib.rs | 23 ++++++++++++++----- crates/tinymemory-module/tests/static_link.rs | 19 +++++++++++++++ docs/specs/tinybus-module.md | 8 +++++++ vendor/tinybus | 2 +- 7 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 crates/tinymemory-module/tests/static_link.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db613d23..3afefde9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -382,6 +382,13 @@ jobs: - name: Unit tests run: cargo test --manifest-path crates/tinymemory-module/Cargo.toml --lib + - name: Linked module exports + run: | + cargo clippy --locked --manifest-path crates/tinymemory-module/Cargo.toml \ + --all-targets --features static-link -- -D warnings + cargo test --locked --manifest-path crates/tinymemory-module/Cargo.toml \ + --features static-link --test static_link + # The module is excluded from the root workspace, so the root coverage # gate cannot see it. Hold its production code to the same floor here. - name: Enforce module production-source coverage diff --git a/crates/tinymemory-module/Cargo.lock b/crates/tinymemory-module/Cargo.lock index 49938e65..f7074697 100644 --- a/crates/tinymemory-module/Cargo.lock +++ b/crates/tinymemory-module/Cargo.lock @@ -1697,7 +1697,7 @@ dependencies = [ [[package]] name = "tinybus" -version = "0.1.1" +version = "0.1.2" dependencies = [ "async-trait", "flate2", @@ -1716,7 +1716,7 @@ dependencies = [ [[package]] name = "tinybus-macros" -version = "0.1.1" +version = "0.1.2" dependencies = [ "proc-macro2", "quote", @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "tinybus-module" -version = "0.1.1" +version = "0.1.2" dependencies = [ "async-trait", "serde", diff --git a/crates/tinymemory-module/Cargo.toml b/crates/tinymemory-module/Cargo.toml index cddc6748..a150e354 100644 --- a/crates/tinymemory-module/Cargo.toml +++ b/crates/tinymemory-module/Cargo.toml @@ -21,6 +21,11 @@ publish = false # without going through the loader; the `cdylib` is what a release ships. crate-type = ["rlib", "cdylib"] +[features] +# Link this module into a host and use Rust-addressable ABI entry points. +# The default keeps the existing loadable cdylib exports. +static-link = [] + [dependencies] # The contract. Every type crossing the bus is one of these, and all of them # already carry serde impls — which is why this module needs no `wire` module of diff --git a/crates/tinymemory-module/src/lib.rs b/crates/tinymemory-module/src/lib.rs index dad9c948..554f8c6c 100644 --- a/crates/tinymemory-module/src/lib.rs +++ b/crates/tinymemory-module/src/lib.rs @@ -1,8 +1,10 @@ //! Loadable `TinyBus` module adapter for `TinyMemory`. //! //! This private workspace crate keeps the vendored `TinyBus` dependency out of -//! the published `tinymemory` crates. Its `cdylib` output is the -//! target-specific binary distributed in GitHub releases. +//! the published `tinymemory` crates. Its default `cdylib` output is the +//! target-specific binary distributed in GitHub releases. With `static-link`, +//! a host can instead reference the descriptor, manifest, and initializer by +//! Rust path without colliding with another module's C symbols. //! //! # What this module is for, stated honestly //! @@ -83,6 +85,10 @@ pub use embedding::{ pub use host::{RUNTIME_HOST_BUS_NAME, RUNTIME_HOST_INTERFACE, RUNTIME_HOST_OBJECT_PATH}; pub use service::{BUS_NAME, OBJECT_PATH}; +/// Rust-addressable TinyBus ABI entries for an in-process linked host. +#[cfg(feature = "static-link")] +pub use exports::{tinybus_module_init_v1, tinybus_module_manifest_v1, TINYBUS_MODULE_ABI_V1}; + use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; @@ -498,16 +504,21 @@ fn setup_error(message: impl Into) -> BusError { } } -// Isolate the generated public C symbols so the lint exception cannot hide -// undocumented Rust API. Their contract is TinyBus ABI v1, and none is a -// Rust-callable export from this crate. +// Isolate the generated ABI symbols so the lint exception cannot hide +// undocumented Rust API. The static-link feature exports these by Rust path; +// the default gives them the established dynamic C symbol names. #[allow( missing_docs, unreachable_pub, reason = "generated C ABI symbols are documented by the TinyBus module SDK" )] mod exports { - tinybus_module::module_export! { + #[cfg(not(feature = "static-link"))] + use tinybus_module::module_export as export_module; + #[cfg(feature = "static-link")] + use tinybus_module::module_export_static as export_module; + + export_module! { setup = super::setup, config = super::ModuleConfig, // Eight, derived rather than picked. Two are the floor this module has diff --git a/crates/tinymemory-module/tests/static_link.rs b/crates/tinymemory-module/tests/static_link.rs new file mode 100644 index 00000000..255fd869 --- /dev/null +++ b/crates/tinymemory-module/tests/static_link.rs @@ -0,0 +1,19 @@ +//! Public linked-module entry points remain callable by a Rust host. + +#![cfg(feature = "static-link")] + +use tinybus::module::abi::{TbModuleInit, TbSlice, ABI_MAGIC}; +use tinymemory_module::{ + tinybus_module_init_v1, tinybus_module_manifest_v1, TINYBUS_MODULE_ABI_V1, +}; + +#[test] +fn linked_module_exposes_its_descriptor_manifest_and_initializer() { + assert_eq!(TINYBUS_MODULE_ABI_V1.magic, ABI_MAGIC); + let manifest: extern "C" fn() -> TbSlice = tinybus_module_manifest_v1; + let slice = manifest(); + assert!(!slice.ptr.is_null()); + assert!(slice.len > 0); + let initialize: TbModuleInit = tinybus_module_init_v1; + assert_ne!(initialize as usize, 0); +} diff --git a/docs/specs/tinybus-module.md b/docs/specs/tinybus-module.md index 2c8cffda..8e4f7de4 100644 --- a/docs/specs/tinybus-module.md +++ b/docs/specs/tinybus-module.md @@ -3,6 +3,14 @@ `crates/tinymemory-module` is a `cdylib` speaking the TinyBus module ABI. A host loads it and gets a bound memory driver without compiling the engine. +The default build exports the TinyBus v1 C symbols for dynamic loading. A host +that compiles the module into its own executable can enable the module crate's +`static-link` feature and pass `tinymemory_module::TINYBUS_MODULE_ABI_V1`, +`tinymemory_module::tinybus_module_manifest_v1`, and +`tinymemory_module::tinybus_module_init_v1` to the linked TinyBus module host. +Both modes use the same manifest declaration; the linked entry points have no +unmangled global C symbol names. + ## What it buys, and what it does not **It sheds no dependencies.** This is measured, not assumed, and it is stated diff --git a/vendor/tinybus b/vendor/tinybus index 3aba4abe..11a7d0df 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit 3aba4abe8564fa803f3054bd5538f46daf43629b +Subproject commit 11a7d0df5689e574f1c14ec6269cbdc2328ae51c From 5c861b03c9d23537fd362eaf968e0bdd68c74fcf Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 26 Sep 2026 01:47:42 +0530 Subject: [PATCH 2/6] Permit scoped unsafe linked-host test calls --- crates/tinymemory-module/Cargo.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/tinymemory-module/Cargo.toml b/crates/tinymemory-module/Cargo.toml index a150e354..d8b91dc7 100644 --- a/crates/tinymemory-module/Cargo.toml +++ b/crates/tinymemory-module/Cargo.toml @@ -128,12 +128,11 @@ tinyinference-core = { path = "../../vendor/tinyinference/crates/tinyinference-c tinyinference-embeddings = { path = "../../vendor/tinyinference/crates/tinyinference-embeddings" } tinyinference-llm = { path = "../../vendor/tinyinference/crates/tinyinference-llm" } -# Mirrors the root package's set. `unsafe_code = "forbid"` holds even though -# `module_export!` emits `unsafe extern "C"` symbols: the macro's expansion -# carries its own hygiene context, so the C ABI surface does not trip the lint -# here. Verified against `tinywallet-module`, which forbids it too and builds. +# The linked-host integration test must call TinyBus's unsafe `attach_raw` ABI +# and borrow the generated manifest bytes. Keep unsafe denied everywhere else; +# the test documents its narrow lifetime and pointer invariants at each call. [lints.rust] -unsafe_code = "forbid" +unsafe_code = "deny" missing_docs = "warn" missing_debug_implementations = "warn" unreachable_pub = "warn" From 5c85acacb19b955a36b9fd5ffe8f0f49075e6e62 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 26 Sep 2026 01:47:43 +0530 Subject: [PATCH 3/6] Exercise linked TinyMemory through the bus host --- crates/tinymemory-module/tests/static_link.rs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/tinymemory-module/tests/static_link.rs b/crates/tinymemory-module/tests/static_link.rs index 255fd869..6e26c14e 100644 --- a/crates/tinymemory-module/tests/static_link.rs +++ b/crates/tinymemory-module/tests/static_link.rs @@ -1,10 +1,17 @@ //! Public linked-module entry points remain callable by a Rust host. #![cfg(feature = "static-link")] +#![allow(unsafe_code)] +use tinybus::broker::Broker; use tinybus::module::abi::{TbModuleInit, TbSlice, ABI_MAGIC}; +use tinybus::module::manifest::ModuleManifest; +use tinybus::module::ModuleHost; +use tinybus::transport::memory::MemoryBus; +use tinybus::Connection; use tinymemory_module::{ - tinybus_module_init_v1, tinybus_module_manifest_v1, TINYBUS_MODULE_ABI_V1, + tinybus_module_init_v1, tinybus_module_manifest_v1, BUS_NAME, OBJECT_PATH, + TINYBUS_MODULE_ABI_V1, }; #[test] @@ -17,3 +24,40 @@ fn linked_module_exposes_its_descriptor_manifest_and_initializer() { let initialize: TbModuleInit = tinybus_module_init_v1; assert_ne!(initialize as usize, 0); } + +#[tokio::test] +async fn linked_module_serves_memory_calls_through_tinybus( +) -> Result<(), Box> { + let workspace = tempfile::tempdir()?; + let bus = MemoryBus::new(); + let broker = Broker::new(); + let _broker_task = broker.spawn(bus.clone()); + let host = ModuleHost::new(broker); + + let slice = tinybus_module_manifest_v1(); + // SAFETY: the generated manifest owns its bytes in a process-lifetime + // OnceLock; its non-null pointer and length remain valid during parsing. + let bytes = unsafe { std::slice::from_raw_parts(slice.ptr, slice.len) }; + let manifest: ModuleManifest = serde_json::from_slice(bytes)?; + let config = serde_json::json!({ "workspace_dir": workspace.path() }); + + // SAFETY: these three entries come from the linked module in this process. + // They remain mapped and callable until exit, including all callbacks the + // host retains after initialization. + let loaded = unsafe { + host.attach_raw_with_config( + "linked-tinymemory", + TINYBUS_MODULE_ABI_V1, + manifest, + tinybus_module_init_v1, + config, + ) + }?; + assert_eq!(loaded.manifest.bus_name.as_str(), BUS_NAME); + + let client = Connection::connect(bus.connect().await?).await?; + let proxy = client.proxy(BUS_NAME, OBJECT_PATH, "ai.tinyhumans.tinymemory.Memory")?; + let driver_id: String = proxy.call("DriverId", ()).await?; + assert_eq!(driver_id, "tinycortex"); + Ok(()) +} From 5024c3d289a6c94c48160df7f599d67506f9016c Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 26 Sep 2026 01:59:16 +0530 Subject: [PATCH 4/6] Keep dynamic loader tests in dynamic mode --- crates/tinymemory-module/tests/module_e2e.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/tinymemory-module/tests/module_e2e.rs b/crates/tinymemory-module/tests/module_e2e.rs index 0a93a187..794b8899 100644 --- a/crates/tinymemory-module/tests/module_e2e.rs +++ b/crates/tinymemory-module/tests/module_e2e.rs @@ -31,6 +31,9 @@ //! `--ignored` alone runs them all in one process and the second will hang. This //! is the same constraint the `tinywallet` module's loader tests carry. +// All-features coverage builds the linked mode, whose ABI entries are Rust +// symbols. This suite loads the default cdylib by its C symbols instead. +#![cfg(not(feature = "static-link"))] #![allow( clippy::expect_used, clippy::unwrap_used, From 7238e07310f34bf1e9dab9b6693006505d60e51b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 26 Sep 2026 02:01:44 +0530 Subject: [PATCH 5/6] Measure dynamic module dispatch coverage --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3afefde9..b3f8b5d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -390,12 +390,14 @@ jobs: --features static-link --test static_link # The module is excluded from the root workspace, so the root coverage - # gate cannot see it. Hold its production code to the same floor here. + # gate cannot see it. Measure the default dynamic build here because the + # loader E2E supplies most dispatch coverage and cannot load a linked-mode + # artifact. The static feature has its own test and clippy step above. - name: Enforce module production-source coverage run: | set -euo pipefail cargo llvm-cov --manifest-path crates/tinymemory-module/Cargo.toml \ - --workspace --all-features \ + --workspace \ --ignore-filename-regex '(^|/)(tests|vendor)/|(^|/)(test|tests|test_helpers|test_support|test_seams)\.rs$|(_test|_tests|_test_support)\.rs$|/crates/tinymemory-core/src/(engine/parity|tree/retrieval/benchmarks)\.rs$|/crates/tinymemory-conformance/src/reference/full\.rs$' \ --fail-under-lines 80 --summary-only \ | tee "$GITHUB_STEP_SUMMARY" From 90a8c07433b05d44066bb9ed49b235ecefd5f492 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 26 Sep 2026 02:13:34 +0530 Subject: [PATCH 6/6] Cover both TinyMemory module export modes --- .github/workflows/ci.yml | 11 +++++++---- crates/tinymemory-module/tests/static_link.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3f8b5d7..564ba428 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -390,14 +390,17 @@ jobs: --features static-link --test static_link # The module is excluded from the root workspace, so the root coverage - # gate cannot see it. Measure the default dynamic build here because the - # loader E2E supplies most dispatch coverage and cannot load a linked-mode - # artifact. The static feature has its own test and clippy step above. + # gate cannot see it. Keep both profiles in one coverage report: default + # mode runs the dynamic loader E2E, while all-features runs the linked + # host test. The loader cannot open a static-link artifact, so these must + # be separate test passes over the same coverage target. - name: Enforce module production-source coverage run: | set -euo pipefail cargo llvm-cov --manifest-path crates/tinymemory-module/Cargo.toml \ - --workspace \ + --workspace --no-report + cargo llvm-cov --manifest-path crates/tinymemory-module/Cargo.toml \ + --workspace --all-features --no-clean \ --ignore-filename-regex '(^|/)(tests|vendor)/|(^|/)(test|tests|test_helpers|test_support|test_seams)\.rs$|(_test|_tests|_test_support)\.rs$|/crates/tinymemory-core/src/(engine/parity|tree/retrieval/benchmarks)\.rs$|/crates/tinymemory-conformance/src/reference/full\.rs$' \ --fail-under-lines 80 --summary-only \ | tee "$GITHUB_STEP_SUMMARY" diff --git a/crates/tinymemory-module/tests/static_link.rs b/crates/tinymemory-module/tests/static_link.rs index 6e26c14e..52d2e6f9 100644 --- a/crates/tinymemory-module/tests/static_link.rs +++ b/crates/tinymemory-module/tests/static_link.rs @@ -1,7 +1,6 @@ //! Public linked-module entry points remain callable by a Rust host. #![cfg(feature = "static-link")] -#![allow(unsafe_code)] use tinybus::broker::Broker; use tinybus::module::abi::{TbModuleInit, TbSlice, ABI_MAGIC}; @@ -25,6 +24,7 @@ fn linked_module_exposes_its_descriptor_manifest_and_initializer() { assert_ne!(initialize as usize, 0); } +#[allow(unsafe_code)] #[tokio::test] async fn linked_module_serves_memory_calls_through_tinybus( ) -> Result<(), Box> {