feat: initial fai-module-sdk v0.1.0 with #[fai_module] macro
All checks were successful
CI / Linux x86_64 (Forgejo) (push) Successful in 1m41s
All checks were successful
CI / Linux x86_64 (Forgejo) (push) Successful in 1m41s
The SDK provides a stable, ergonomic Rust surface for writing
F∆I Platform modules. A module is now a single decorated
function:
#[fai_module]
fn invoke(_ctx: Context, inputs: Inputs)
-> Result<Outputs, ModuleError>
{
let text = inputs.require_text("input")?;
Ok(Outputs::new().with_text("output", format!("echo: {text}")))
}
The macro absorbs every piece of WASM-module boilerplate that
previously lived per-repo: the wit_bindgen::generate! call, the
Guest trait impl, the WIT-to-SDK type conversions, the
wit_bindgen::export! glue, and the unsafe_op_in_unsafe_fn allow
attribute. Module repos depend only on fai-module-sdk and need
no wit/ directory of their own — the SDK ships a verified
mirror of the platform's frozen v1.0 WIT contract.
The SDK is the central lever for keeping the contract stable
without freezing module ergonomics. If the platform ever bumps
the WIT contract (additive minor or coordinated v2.0), the
change is absorbed inside the SDK; existing module source
keeps compiling against a new SDK release.
Components in this initial drop:
crates/fai-module-sdk
Public types — Context, Inputs, Outputs, Payload,
ModuleError. Helpers like require_text / require_json /
with_bytes / with_json. Pure Rust, no wit_bindgen
dependency in user-facing code.
crates/fai-module-sdk-macros
The proc-macro crate that emits the wit_bindgen call,
the Guest impl, the WIT<->SDK conversion code, and the
export! invocation. Uses runtime_path to route the
generated bindings through the SDK's re-export of
wit_bindgen::rt so user crates need no direct dep.
wit/world.wit
Mirror of fai/platform :: wit/world.wit, frozen at v1.0.
A snapshot test (crates/fai-module-sdk/tests/wit_freeze.rs)
asserts the SHA-256 matches the platform constant; CI
fails if the two ever drift.
examples/echo
Reference module that demonstrates the entire authoring
surface. Compiles to wasm32-wasip2 with the correct
v1.0 imports baked in (verified manually; CI runs the
same build).
Tests: 7 unit tests + 1 snapshot test. Cargo fmt and clippy
clean across the workspace and the example.
Forgejo CI workflow mirrors the platform pattern (manual
external-URL checkout + libssl-dev system deps + wasm32-wasip2
target + workspace fmt/clippy/build/test + example wasm build).
Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
commit
d3642853ec
22 changed files with 2095 additions and 0 deletions
21
crates/fai-module-sdk-macros/Cargo.toml
Normal file
21
crates/fai-module-sdk-macros/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "fai-module-sdk-macros"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Procedural macros for fai-module-sdk. Not used directly — see fai-module-sdk."
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
167
crates/fai-module-sdk-macros/src/lib.rs
Normal file
167
crates/fai-module-sdk-macros/src/lib.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
//! 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
|
||||
|
||||
#[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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue