diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e6ba914..df41685 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -59,7 +59,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Build loader fixtures
- run: cargo build --locked --example module_clock --example module_clock_two --example module_wrong_target --no-default-features --features modules,macros
+ run: cargo build --locked --example module_clock --example module_clock_two --example module_wrong_target --example module_context --example module_context_eager --no-default-features --features modules,macros
- name: Enforce 90% line coverage per source file
shell: bash
run: |
@@ -67,7 +67,9 @@ jobs:
module=$(find target/debug/examples -maxdepth 1 -type f -name 'libmodule_clock.so' -print -quit)
module_two=$(find target/debug/examples -maxdepth 1 -type f -name 'libmodule_clock_two.so' -print -quit)
wrong_target=$(find target/debug/examples -maxdepth 1 -type f -name 'libmodule_wrong_target.so' -print -quit)
- for artifact in "$module" "$module_two" "$wrong_target"; do
+ context_module=$(find target/debug/examples -maxdepth 1 -type f -name 'libmodule_context.so' -print -quit)
+ context_eager_module=$(find target/debug/examples -maxdepth 1 -type f -name 'libmodule_context_eager.so' -print -quit)
+ for artifact in "$module" "$module_two" "$wrong_target" "$context_module" "$context_eager_module"; do
if [ -z "$artifact" ] || [ ! -f "$artifact" ]; then
echo "a module loader fixture is missing" >&2
exit 1
@@ -91,7 +93,7 @@ jobs:
# process/network branches are not meaningful per-file coverage. The
# test-support module is integration-test glue whose platform-specific
# dynamic-loader and timeout paths run in the dedicated OS jobs instead.
- TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo llvm-cov --workspace --all-features --ignore-filename-regex 'crates/tinybus/src/ports/(listener|transport)\.rs$|crates/tinybus/src/module/github\.rs$|crates/tinybus/src/module/(host|mod)\.rs$|crates/tinybus/src/bin/tinybus\.rs$|crates/tinybus/src/connection\.rs$|crates/tinybus/src/test_support\.rs$' --fail-under-file-lines 90 -- --include-ignored
+ TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" TINYBUS_TEST_CONTEXT_MODULE="$PWD/$context_module" TINYBUS_TEST_CONTEXT_EAGER_MODULE="$PWD/$context_eager_module" cargo llvm-cov --workspace --all-features --ignore-filename-regex 'crates/tinybus/src/ports/(listener|transport)\.rs$|crates/tinybus/src/module/github\.rs$|crates/tinybus/src/module/(host|mod)\.rs$|crates/tinybus/src/bin/tinybus\.rs$|crates/tinybus/src/connection\.rs$|crates/tinybus/src/test_support\.rs$' --fail-under-file-lines 90 -- --include-ignored
# The bus is meant to be embeddable on every platform OpenHuman ships on. The
# socket adapter remains Unix-only, while the in-process module transport
@@ -128,7 +130,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build example module
- run: cargo build --locked --example module_clock --example module_clock_two --example module_wrong_target --no-default-features --features modules,macros
+ run: cargo build --locked --example module_clock --example module_clock_two --example module_wrong_target --example module_context --example module_context_eager --no-default-features --features modules,macros
- name: Test shared module test support
run: cargo test --locked --no-default-features --features modules,macros,test-support test_support::tests
- name: Exercise the real loader (Unix)
@@ -139,7 +141,9 @@ jobs:
module=$(find target/debug/examples -maxdepth 1 -type f \( -name 'libmodule_clock.so' -o -name 'libmodule_clock.dylib' \) -print -quit)
module_two=$(find target/debug/examples -maxdepth 1 -type f \( -name 'libmodule_clock_two.so' -o -name 'libmodule_clock_two.dylib' \) -print -quit)
wrong_target=$(find target/debug/examples -maxdepth 1 -type f \( -name 'libmodule_wrong_target.so' -o -name 'libmodule_wrong_target.dylib' \) -print -quit)
- for artifact in "$module" "$module_two" "$wrong_target"; do
+ context_module=$(find target/debug/examples -maxdepth 1 -type f \( -name 'libmodule_context.so' -o -name 'libmodule_context.dylib' \) -print -quit)
+ context_eager_module=$(find target/debug/examples -maxdepth 1 -type f \( -name 'libmodule_context_eager.so' -o -name 'libmodule_context_eager.dylib' \) -print -quit)
+ for artifact in "$module" "$module_two" "$wrong_target" "$context_module" "$context_eager_module"; do
if [ -z "$artifact" ] || [ ! -f "$artifact" ]; then
echo "a module example artifact is missing; the loader tests would not exercise it" >&2
exit 1
@@ -156,7 +160,7 @@ jobs:
digest="$(shasum -a 256 "$private/$library" | awk '{print $1}')"
fi
printf '"%s" = "%s"\n' "$library" "$digest" > "$private/modules.toml"
- TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo test --locked --no-default-features --features modules,macros,test-support -- --ignored
+ TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" TINYBUS_TEST_CONTEXT_MODULE="$PWD/$context_module" TINYBUS_TEST_CONTEXT_EAGER_MODULE="$PWD/$context_eager_module" cargo test --locked --no-default-features --features modules,macros,test-support -- --ignored
# Directory admission against this runner's real filesystem: a bundle
# in a directory other accounts can write falls back to the verified
# release cache, and on macOS the real /Applications is admitted.
@@ -190,13 +194,22 @@ jobs:
$acl.AddAccessRule($rule)
Set-Acl -LiteralPath $private -AclObject $acl
Copy-Item target/debug/examples/module_clock.dll $private
+ Copy-Item target/debug/examples/module_context.dll $private
+ Copy-Item target/debug/examples/module_context_eager.dll $private
+ Copy-Item target/debug/examples/module_wrong_target.dll $private
+ # Admit every intentional fixture by its actual built bytes so later
+ # checks, including the wrong-target gate, can exercise their contract.
+ $fixtureAllowlist = @('module_clock.dll', 'module_context.dll', 'module_context_eager.dll', 'module_wrong_target.dll') | ForEach-Object {
+ $digest = (Get-FileHash -LiteralPath (Join-Path $private $_) -Algorithm SHA256).Hash.ToLowerInvariant()
+ '"' + $_ + '" = "' + $digest + '"'
+ }
+ $fixtureAllowlist | Set-Content -LiteralPath (Join-Path $private modules.toml)
$clockTwoDirectory = Join-Path $private module-clock-two
New-Item -ItemType Directory -Force $clockTwoDirectory | Out-Null
Set-Acl -LiteralPath $clockTwoDirectory -AclObject $acl
Copy-Item target/debug/examples/module_clock_two.dll $clockTwoDirectory
$clockTwoHash = (Get-FileHash -LiteralPath (Join-Path $clockTwoDirectory module_clock_two.dll) -Algorithm SHA256).Hash.ToLowerInvariant()
'"module_clock_two.dll" = "' + $clockTwoHash + '"' | Set-Content -LiteralPath (Join-Path $clockTwoDirectory modules.toml) -NoNewline
- Copy-Item target/debug/examples/module_wrong_target.dll $private
$mixed = Join-Path $private mixed
New-Item -ItemType Directory -Force $mixed | Out-Null
Set-Acl -LiteralPath $mixed -AclObject $acl
@@ -207,6 +220,8 @@ jobs:
$wrongTarget = (Get-ChildItem "$private/module_wrong_target.dll" | Select-Object -First 1).FullName
$env:TEMP = $private
$env:TMP = $private
+ $env:TINYBUS_TEST_CONTEXT_MODULE = (Get-Item "$private/module_context.dll").FullName
+ $env:TINYBUS_TEST_CONTEXT_EAGER_MODULE = (Get-Item "$private/module_context_eager.dll").FullName
$env:TINYBUS_TEST_MODULE = $module
$env:TINYBUS_TEST_MODULE_TWO = $moduleTwo
$env:TINYBUS_TEST_WRONG_TARGET = $wrongTarget
diff --git a/Cargo.toml b/Cargo.toml
index b56957a..addae4a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ resolver = "3"
[workspace.package]
version = "0.1.3"
edition = "2024"
+rust-version = "1.85"
license = "GPL-3.0-only"
repository = "https://github.com/tinyhumansai/tinybus"
diff --git a/crates/tinybus-macros/Cargo.toml b/crates/tinybus-macros/Cargo.toml
index 9efec82..ccb7426 100644
--- a/crates/tinybus-macros/Cargo.toml
+++ b/crates/tinybus-macros/Cargo.toml
@@ -2,6 +2,7 @@
name = "tinybus-macros"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "The #[interface] attribute for tinybus. Not useful on its own; use the `tinybus` crate."
diff --git a/crates/tinybus-macros/src/lib.rs b/crates/tinybus-macros/src/lib.rs
index 14212f0..2793746 100644
--- a/crates/tinybus-macros/src/lib.rs
+++ b/crates/tinybus-macros/src/lib.rs
@@ -210,13 +210,15 @@ fn expand(
/// Read `name = "…"` off the attribute.
fn interface_name(args: &Punctuated) -> syn::Result {
for arg in args {
- if let Meta::NameValue(nv) = arg
- && nv.path.is_ident("name")
- && let syn::Expr::Lit(lit) = &nv.value
- && let syn::Lit::Str(s) = &lit.lit
- {
- return Ok(s.value());
+ let Meta::NameValue(nv) = arg else { continue };
+ if !nv.path.is_ident("name") {
+ continue;
}
+ let syn::Expr::Lit(lit) = &nv.value else {
+ continue;
+ };
+ let syn::Lit::Str(s) = &lit.lit else { continue };
+ return Ok(s.value());
}
Err(Error::new(
proc_macro2::Span::call_site(),
diff --git a/crates/tinybus-module/Cargo.toml b/crates/tinybus-module/Cargo.toml
index 48f4907..6cf7994 100644
--- a/crates/tinybus-module/Cargo.toml
+++ b/crates/tinybus-module/Cargo.toml
@@ -2,6 +2,7 @@
name = "tinybus-module"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Module-side runtime and export macro for trusted tinybus cdylibs."
diff --git a/crates/tinybus-module/src/lib.rs b/crates/tinybus-module/src/lib.rs
index c6cc333..468175b 100644
--- a/crates/tinybus-module/src/lib.rs
+++ b/crates/tinybus-module/src/lib.rs
@@ -3,11 +3,16 @@
//! Each module owns a Tokio runtime. A statically linked `cdylib` has its own
//! Tokio thread-locals, so attempting to borrow the host runtime is both
//! incorrect and capable of silently blocking a host worker thread.
+//!
+//! Caller context depends on the trusted host's optional ABI-v1 routing callback.
+//! Its assertion is captured per frame at enqueue time; older host prefixes
+//! remain compatible but supply unverified context. Sender headers remain intact
+//! for protocol routing and streams even when they carry no caller authority.
use std::ffi::c_void;
use std::future::Future;
use std::panic::{AssertUnwindSafe, catch_unwind};
-use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex as StdMutex, OnceLock};
use std::time::Duration;
@@ -164,6 +169,12 @@ unsafe impl Send for HostCalls {}
unsafe impl Sync for HostCalls {}
impl HostCalls {
+ fn is_broker_routed(&self) -> bool {
+ self.0
+ .broker_routing
+ .is_some_and(|routing| unsafe { routing(self.0.host_ctx) } == 1)
+ }
+
fn send(&self, bytes: &[u8]) -> i32 {
unsafe { (self.0.send)(self.0.host_ctx, bytes.as_ptr(), bytes.len()) }
}
@@ -244,9 +255,13 @@ impl tracing::field::Visit for LogVisitor {
}
}
+// Private envelope captures trusted-host routing before the frame is queued.
+type IncomingFrame = (Vec, bool);
+
struct ModuleTransport {
host: HostCalls,
- inbound: Mutex>>,
+ inbound: Mutex>,
+ provenance: Arc,
detach_on_panic: bool,
}
@@ -273,11 +288,16 @@ impl Transport for ModuleTransport {
async fn recv(&self) -> Result