chain-module-sdk-rust/crates/fai-module-sdk-macros/src/lib.rs
flemming-it eb7a1d5d42
Some checks failed
CI / Linux x86_64 (Forgejo) (push) Has been cancelled
fix(macros): emit WASM glue only on target_arch = "wasm32"
The #[fai_module] expansion previously always inserted the
wit_bindgen::generate! call, the Guest impl, and the export!
macro. That is correct on the wasm32 target but causes host
compilation of a module crate to fail because wit_bindgen's
runtime types are not in scope on a non-wasm target.

The orchestrator-llm module worked around this with a manual
#[cfg(target_arch = "wasm32")] gate around its hand-written
wit_bindgen call. The point of the SDK is to remove that
boilerplate, so the gate now lives inside the macro itself.

Module crates can now declare crate-type = ["cdylib", "rlib"]
and run integration tests via `cargo test` against the
`invoke` function directly without having the WASM glue on
the host build.

Bumps the SDK patch version 0.1.0 -> 0.1.1 per the per-commit
policy.

Signed-off-by: flemming-it <sf@flemming.it>
2026-05-01 14:57:44 +02:00

173 lines
7.2 KiB
Rust

//! Procedural macros powering `fai-module-sdk`.
//!
//! This crate exists only to host the `#[fai_module]` attribute.
//! Module authors should depend on `fai-module-sdk` (which
//! re-exports the macro), not on this crate directly.
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
/// The frozen WIT contract, embedded at proc-macro compile time.
/// The platform repo and this repo both ship a SHA-256 snapshot
/// test that fails if these copies drift.
const WIT_INLINE: &str = include_str!("../../../wit/world.wit");
/// Wrap a function with all the WASM module boilerplate.
///
/// The annotated function must have the signature:
///
/// ```ignore
/// fn invoke(ctx: Context, inputs: Inputs) -> Result<Outputs, ModuleError>
/// ```
///
/// The macro generates the `wit_bindgen::generate!` invocation,
/// the `Guest` implementation, the WIT-to-SDK conversions, and
/// the `wit_bindgen::export!` call. The function can be named
/// anything — only its signature matters.
#[proc_macro_attribute]
pub fn fai_module(_attrs: TokenStream, input: TokenStream) -> TokenStream {
let user_fn = parse_macro_input!(input as ItemFn);
let user_fn_name = user_fn.sig.ident.clone();
let wit_literal = WIT_INLINE;
let expanded = quote! {
#user_fn
// The WASM glue (wit_bindgen::generate!, Guest impl, export!)
// is only emitted for the wasm32 target. Host builds skip
// it so module crates can use crate-type = ["cdylib", "rlib"]
// and run integration tests via `cargo test` without
// dragging the wit_bindgen runtime onto the host target.
#[cfg(target_arch = "wasm32")]
#[doc(hidden)]
mod __fai_module_sdk_internal {
#![allow(
unsafe_op_in_unsafe_fn,
unused_imports,
dead_code,
clippy::all,
clippy::pedantic,
clippy::nursery
)]
::fai_module_sdk::__rt::wit_bindgen::generate!({
inline: #wit_literal,
world: "module",
runtime_path: "::fai_module_sdk::__rt::wit_bindgen::rt",
});
use exports::fai::platform::invoke::Guest;
use fai::platform::types::{
BytesValue as WitBytes,
FileRef as WitFileRef,
InvocationContext as WitContext,
InvocationError as WitError,
Payload as WitPayload,
};
struct Component;
impl Guest for Component {
fn invoke(
ctx: WitContext,
inputs: ::std::vec::Vec<(
::std::string::String,
WitPayload,
)>,
) -> ::core::result::Result<
::std::vec::Vec<(::std::string::String, WitPayload)>,
WitError,
> {
let sdk_ctx = ::fai_module_sdk::Context {
invocation_id: ctx.invocation_id,
capability_namespace: ctx.capability_namespace,
capability_name: ctx.capability_name,
deadline_ms: ctx.deadline_ms,
};
let sdk_pairs: ::std::vec::Vec<(
::std::string::String,
::fai_module_sdk::Payload,
)> = inputs
.into_iter()
.map(|(k, p)| (k, wit_payload_to_sdk(p)))
.collect();
let sdk_inputs =
::fai_module_sdk::Inputs::__from_pairs(sdk_pairs);
match super::#user_fn_name(sdk_ctx, sdk_inputs) {
Ok(outputs) => {
let pairs = outputs.__into_pairs();
let wit_pairs = pairs
.into_iter()
.map(|(k, p)| (k, sdk_payload_to_wit(p)))
.collect();
Ok(wit_pairs)
}
Err(e) => Err(sdk_error_to_wit(e)),
}
}
}
fn wit_payload_to_sdk(p: WitPayload) -> ::fai_module_sdk::Payload {
match p {
WitPayload::Text(s) => ::fai_module_sdk::Payload::Text(s),
WitPayload::Json(s) => ::fai_module_sdk::Payload::Json(s),
WitPayload::Bytes(b) => ::fai_module_sdk::Payload::Bytes(
::fai_module_sdk::BytesValue {
mime_type: b.mime_type,
data: b.data,
},
),
WitPayload::FileRef(f) => ::fai_module_sdk::Payload::FileRef(
::fai_module_sdk::FileRef {
uri: f.uri,
mime_type: f.mime_type,
size_bytes: f.size_bytes,
sha256: f.sha256,
},
),
}
}
fn sdk_payload_to_wit(p: ::fai_module_sdk::Payload) -> WitPayload {
match p {
::fai_module_sdk::Payload::Text(s) => WitPayload::Text(s),
::fai_module_sdk::Payload::Json(s) => WitPayload::Json(s),
::fai_module_sdk::Payload::Bytes(b) => WitPayload::Bytes(WitBytes {
mime_type: b.mime_type,
data: b.data,
}),
::fai_module_sdk::Payload::FileRef(f) => WitPayload::FileRef(WitFileRef {
uri: f.uri,
mime_type: f.mime_type,
size_bytes: f.size_bytes,
sha256: f.sha256,
}),
}
}
fn sdk_error_to_wit(e: ::fai_module_sdk::ModuleError) -> WitError {
match e {
::fai_module_sdk::ModuleError::InvalidInput(s) => {
WitError::InvalidInput(s)
}
::fai_module_sdk::ModuleError::PermissionDenied(s) => {
WitError::PermissionDenied(s)
}
::fai_module_sdk::ModuleError::ResourceExhausted(s) => {
WitError::ResourceExhausted(s)
}
::fai_module_sdk::ModuleError::DeadlineExceeded => {
WitError::DeadlineExceeded
}
::fai_module_sdk::ModuleError::Internal(s) => WitError::Internal(s),
}
}
export!(Component);
}
};
TokenStream::from(expanded)
}