diff --git a/crates/chain-module-sdk-macros/src/lib.rs b/crates/chain-module-sdk-macros/src/lib.rs index 66ef069..37af23c 100644 --- a/crates/chain-module-sdk-macros/src/lib.rs +++ b/crates/chain-module-sdk-macros/src/lib.rs @@ -95,6 +95,13 @@ pub fn fai_module(_attrs: TokenStream, input: TokenStream) -> TokenStream { let sdk_inputs = ::chain_module_sdk::Inputs::__from_pairs(sdk_pairs); + // Wire Context::emit to the real WIT host import + // for the duration of this invocation (WIT 1.1). + ::chain_module_sdk::__set_emit_hook(|name, payload| { + let wit = sdk_payload_to_wit(payload); + chain::platform::host::emit_event(name, &wit); + }); + match super::#user_fn_name(sdk_ctx, sdk_inputs) { Ok(outputs) => { let pairs = outputs.__into_pairs(); diff --git a/crates/chain-module-sdk/src/context.rs b/crates/chain-module-sdk/src/context.rs index 98f6ccf..32080d0 100644 --- a/crates/chain-module-sdk/src/context.rs +++ b/crates/chain-module-sdk/src/context.rs @@ -11,3 +11,23 @@ pub struct Context { pub capability_name: String, pub deadline_ms: u64, } + +impl Context { + /// Emit an ephemeral progress/stream event during this invocation + /// (WIT 1.1 `emit-event`). Delivered live to any client following + /// the flow (`SubmitStream`); it is **never** written to the audit + /// log. Use it for progress updates or token streaming. + /// + /// `name` is a caller-defined label (e.g. `"progress"`, `"token"`); + /// `payload` is the event body. The host enforces per-invocation + /// size + rate limits — an over-limit event is dropped, never an + /// error. When the module runs outside a hub that supports it (or + /// in a unit test) `emit` is a silent no-op. + /// + /// ```ignore + /// ctx.emit("progress", Payload::Text("50%".into())); + /// ``` + pub fn emit(&self, name: &str, payload: crate::Payload) { + crate::emit(name, payload); + } +} diff --git a/crates/chain-module-sdk/src/host.rs b/crates/chain-module-sdk/src/host.rs new file mode 100644 index 0000000..de9572d --- /dev/null +++ b/crates/chain-module-sdk/src/host.rs @@ -0,0 +1,35 @@ +//! Host-callback plumbing for `Context::emit` (WIT 1.1 `emit-event`). +//! +//! The WIT host imports (`emit-event`) are only reachable from the +//! `wit_bindgen`-generated glue inside the `#[fai_module]` expansion, +//! which lives in the module crate — not here. So the macro installs a +//! function pointer that reaches the real host import, and +//! [`Context::emit`](crate::Context::emit) calls it through this hook. +//! +//! On a non-wasm host target (unit tests) no hook is installed, so +//! `emit` is a silent no-op. The guest is single-threaded, so a +//! thread-local cell is sufficient and lock-free. + +use std::cell::Cell; + +use crate::Payload; + +thread_local! { + static EMIT_HOOK: Cell> = const { Cell::new(None) }; +} + +/// Install the emit hook. Called by the `#[fai_module]` expansion at +/// the start of each invocation. Not part of the stable public API. +#[doc(hidden)] +pub fn __set_emit_hook(f: fn(&str, Payload)) { + EMIT_HOOK.with(|c| c.set(Some(f))); +} + +/// Forward an event to the host, if a hook is installed. +pub(crate) fn emit(name: &str, payload: Payload) { + EMIT_HOOK.with(|c| { + if let Some(f) = c.get() { + f(name, payload); + } + }); +} diff --git a/crates/chain-module-sdk/src/lib.rs b/crates/chain-module-sdk/src/lib.rs index 87b03d4..88ff172 100644 --- a/crates/chain-module-sdk/src/lib.rs +++ b/crates/chain-module-sdk/src/lib.rs @@ -25,6 +25,7 @@ mod context; mod error; +mod host; mod inputs; mod outputs; mod payload; @@ -37,6 +38,12 @@ pub use payload::{BytesValue, FileRef, Payload}; pub use chain_module_sdk_macros::fai_module; +// Emit-hook plumbing used by the `#[fai_module]` expansion (wasm32) +// so `Context::emit` reaches the WIT `emit-event` host import. +#[doc(hidden)] +pub use host::__set_emit_hook; +pub(crate) use host::emit; + /// Convenience prelude — `use chain_module_sdk::prelude::*;` brings /// every type a typical module needs into scope. pub mod prelude { diff --git a/crates/chain-module-sdk/tests/wit_freeze.rs b/crates/chain-module-sdk/tests/wit_freeze.rs index 727896d..d4940a6 100644 --- a/crates/chain-module-sdk/tests/wit_freeze.rs +++ b/crates/chain-module-sdk/tests/wit_freeze.rs @@ -13,10 +13,13 @@ use std::path::PathBuf; use sha2::{Digest, Sha256}; /// Identical to FROZEN_WIT_SHA256 in -/// fai/platform :: crates/fai_runtime/tests/wit_freeze.rs. +/// fai/chain :: crates/chain_runtime/tests/wit_freeze.rs. /// Update both repos in lockstep, never one alone. +/// 2026-07-05 — WIT 1.0.0 → 1.1.0 (additive: host.emit-event). The SDK +/// wit/world.wit is now byte-identical to the platform's, so the two +/// frozen hashes match exactly. const PLATFORM_FROZEN_WIT_SHA256: &str = - "6f4e2266a4264832a16fe5a3704622cd978b290b90f3300c3ed236d98b689b5f"; + "2ba9d6c61c4c77d081679603a9389aef663429ec80f9c386cf9b1ca9d353bf6f"; fn world_wit_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/examples/progress/Cargo.lock b/examples/progress/Cargo.lock new file mode 100644 index 0000000..5282f1f --- /dev/null +++ b/examples/progress/Cargo.lock @@ -0,0 +1,439 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chain-module-sdk" +version = "0.1.2" +dependencies = [ + "chain-module-sdk-macros", + "serde", + "serde_json", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "chain-module-sdk-macros" +version = "0.1.2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "progress_example" +version = "0.1.0" +dependencies = [ + "chain-module-sdk", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-encoder" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1" +dependencies = [ + "leb128", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/progress/Cargo.toml b/examples/progress/Cargo.toml new file mode 100644 index 0000000..8606231 --- /dev/null +++ b/examples/progress/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "progress_example" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false +description = "Reference module showing WIT 1.1 host.emit-event via ctx.emit (progress + token streaming)." + +[lib] +crate-type = ["cdylib"] + +[dependencies] +chain-module-sdk = { path = "../../crates/chain-module-sdk" } + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 +strip = "symbols" + +[workspace] diff --git a/examples/progress/src/lib.rs b/examples/progress/src/lib.rs new file mode 100644 index 0000000..b222155 --- /dev/null +++ b/examples/progress/src/lib.rs @@ -0,0 +1,20 @@ +//! Reference module for WIT 1.1 `host.emit-event`. +//! +//! Echoes the input, but first emits a few ephemeral progress events +//! via `ctx.emit`. A client following the flow with `SubmitStream` +//! sees them live; they are never written to the audit log. + +use chain_module_sdk::prelude::*; + +#[fai_module] +fn invoke(ctx: Context, inputs: Inputs) -> Result { + let text = inputs.require_text("input")?; + + for pct in [0u32, 50, 100] { + ctx.emit("progress", Payload::Text(format!("{pct}%"))); + } + // A structured event, too. + ctx.emit("token", Payload::Text(text.to_string())); + + Ok(Outputs::new().with_text("output", format!("echo: {text}"))) +} diff --git a/wit/world.wit b/wit/world.wit index 58ac024..57023a6 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -2,7 +2,7 @@ // FROZEN. Wire contract between hub and modules. // ============================================================= // -// This file is the stable wire contract between the F∆I Hub +// This file is the stable wire contract between the Ch∆In Hub // and any module compiled against it. It is **frozen at v1.0**. // // Frozen means: @@ -20,11 +20,11 @@ // See `crates/fai_runtime/tests/wit_freeze.rs`. // // Module authors do NOT depend on this file directly. They -// use `fai-module-sdk`, which wraps these bindings behind a +// use `chain-module-sdk`, which wraps these bindings behind a // stable, ergonomic surface. That is what protects modules // from any future evolution of this contract. -package chain:platform@1.0.0; +package chain:platform@1.1.0; /// Types that flow between the host (hub) and modules. interface types { @@ -74,8 +74,19 @@ interface types { /// The host-provided interface that modules can import to talk back. interface host { + use types.{payload}; + /// Log a structured message. Level is "trace", "debug", "info", "warn", "error". log: func(level: string, message: string); + + /// Emit an ephemeral progress/stream event during an invocation. + /// Added in 1.1. Events are fanned out to any client following this + /// invocation live (SubmitStream); they are NOT written to the + /// tamper-evident audit log. `name` is a caller-defined event label + /// (e.g. "progress", "token"); `payload` is the event body. The host + /// enforces per-invocation size + rate limits — an event over the + /// limit is dropped with a warning and never fails the invocation. + emit-event: func(name: string, payload: payload); } /// The core interface every module exports.