From a536d873e691074f93f075b340994ae9cc2d79d0 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 22 Oct 2026 20:16:38 +0100 Subject: [PATCH] cargo: compile C for wasm32-wasip2 with a WASI clang The Nix package, the dev shell, and the test check set CC_wasm32_wasip2 and AR_wasm32_wasip2 to the clang of pkgsCross.wasi32 (wasi-libc), so build scripts with the cc crate work, for example rusqlite with bundled SQLite. The runtime-toolchain check links C into a probe with the packaged host and an empty PATH. --- PROTOCOL.md | 5 +++ cli/src/cargo_actor.rs | 3 ++ cli/tests/crates/cargo-c/Cargo.toml | 12 +++++++ cli/tests/crates/cargo-c/build.rs | 5 +++ cli/tests/crates/cargo-c/src/add.c | 1 + cli/tests/crates/cargo-c/src/main.rs | 25 ++++++++++++++ cli/tests/host.rs | 16 +++++++++ flake.nix | 51 ++++++++++++++++++++++++---- 8 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 cli/tests/crates/cargo-c/Cargo.toml create mode 100644 cli/tests/crates/cargo-c/build.rs create mode 100644 cli/tests/crates/cargo-c/src/add.c create mode 100644 cli/tests/crates/cargo-c/src/main.rs diff --git a/PROTOCOL.md b/PROTOCOL.md index 3a82b93..1ed21c8 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -265,6 +265,11 @@ The reply is `{"type": "built", "wasm": "/actor/cargo/public/builds//.w or `{"type": "build_failed", "errors": "...", "tests": {...}}`. Pass `wasm`, `source`, and `built_by` on to `spawn` or `upgrade`. +Build scripts can compile C for `wasm32-wasip2`: the host sets +`CC_wasm32_wasip2` and `AR_wasm32_wasip2` to a WASI clang with wasi-libc, so +the `cc` crate (for example `rusqlite` with `bundled`) works. Its headers are +WASI 0.1: C code that needs sockets does not build. + A Rust actor depends on the SDK with `sausage-actor = "0.1"`. `cargo` points that dependency at the SDK of the host, which it publishes at `/actor/cargo/public/sdk/`. diff --git a/cli/src/cargo_actor.rs b/cli/src/cargo_actor.rs index 2cd18d7..0c51d01 100644 --- a/cli/src/cargo_actor.rs +++ b/cli/src/cargo_actor.rs @@ -8,6 +8,9 @@ //! The broker knows nothing about Rust. This actor is one way to get a //! `.wasm` file. Other actors can add other languages. //! +//! Build scripts compile C for WASI with the clang in `CC_wasm32_wasip2` +//! and `AR_wasm32_wasip2`. The Nix package and the dev shell set both. +//! //! Actors depend on the SDK with `sausage-actor = "0.1"`. The SDK is not //! published: this actor copies the SDK of this host to `public/sdk/` (so //! that every actor can read it), and points the dependency there in the diff --git a/cli/tests/crates/cargo-c/Cargo.toml b/cli/tests/crates/cargo-c/Cargo.toml new file mode 100644 index 0000000..e5f9906 --- /dev/null +++ b/cli/tests/crates/cargo-c/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cargo-c" +version = "0.1.0" +edition = "2024" + +[dependencies] +sausage-actor = "0.1" + +[build-dependencies] +cc = "1" + +[workspace] diff --git a/cli/tests/crates/cargo-c/build.rs b/cli/tests/crates/cargo-c/build.rs new file mode 100644 index 0000000..c4a89ae --- /dev/null +++ b/cli/tests/crates/cargo-c/build.rs @@ -0,0 +1,5 @@ +// Compile C with the `cc` crate: natively for the unit test, and with the +// WASI clang (`CC_wasm32_wasip2`) for the actor. +fn main() { + cc::Build::new().file("src/add.c").compile("add"); +} diff --git a/cli/tests/crates/cargo-c/src/add.c b/cli/tests/crates/cargo-c/src/add.c new file mode 100644 index 0000000..f83c3dc --- /dev/null +++ b/cli/tests/crates/cargo-c/src/add.c @@ -0,0 +1 @@ +int c_add(int a, int b) { return a + b; } diff --git a/cli/tests/crates/cargo-c/src/main.rs b/cli/tests/crates/cargo-c/src/main.rs new file mode 100644 index 0000000..43a1d30 --- /dev/null +++ b/cli/tests/crates/cargo-c/src/main.rs @@ -0,0 +1,25 @@ +//! Adds two numbers with a C function: `{"type": "add", "a", "b"}` -> `{"type": "sum", "value"}`. +use sausage_actor::{json, messages, reply}; + +unsafe extern "C" { + fn c_add(a: i32, b: i32) -> i32; +} + +fn add(a: i32, b: i32) -> i32 { + unsafe { c_add(a, b) } +} + +fn main() { + for message in messages() { + let (a, b) = (message.body["a"].as_i64().unwrap_or(0), message.body["b"].as_i64().unwrap_or(0)); + let _ = reply(&message, json!({"type": "sum", "value": add(a as i32, b as i32)})); + } +} + +#[cfg(test)] +mod tests { + #[test] + fn c_code_runs_on_the_host_target() { + assert_eq!(super::add(2, 3), 5); + } +} diff --git a/cli/tests/host.rs b/cli/tests/host.rs index f32132e..668e805 100644 --- a/cli/tests/host.rs +++ b/cli/tests/host.rs @@ -769,6 +769,22 @@ async fn cargo_actor_runs_unit_tests_by_default_and_can_skip_them() { host.stop().await; } +#[tokio::test] +async fn cargo_actor_compiles_c_code_for_wasi() { + let world = create_world("cargo-c"); + let (host, mut mailbox) = start(&world).await; + let source = cargo_source("cargo-c"); + let id = host.broker.send("console", Some("cargo"), None, None, json!({"type": "build", "source": source.display().to_string()})); + let built = receive_reply(&mut mailbox, &id, Duration::from_secs(300)).await; + assert_eq!(built.body["type"], "built", "{built:?}"); + assert_eq!(built.body["tests"]["passed"], 1, "{built:?}"); + let wasm = host.world.resolve("console", built.body["wasm"].as_str().unwrap()).unwrap(); + spawn_type(&host, &mut mailbox, "c-add", &wasm, &source).await; + let sum = ask_type(&host, &mut mailbox, "c-add", json!({"type": "add", "a": 2, "b": 3}), "sum").await; + assert_eq!(sum.body["value"], 5); + host.stop().await; +} + #[tokio::test] async fn rejects_invalid_actor_ids() { let world = create_world("invalid-id"); diff --git a/flake.nix b/flake.nix index ff3b2ea..7fd8a52 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,14 @@ # PATH. Keep this same runtime path in the packaged host and exercise # the installed binary below with PATH deliberately empty. runtimePath = pkgs.lib.makeBinPath [ rust pkgs.stdenv.cc ]; + # A WASI clang with wasi-libc, so that build scripts (the `cc` crate) + # can compile C for wasm32-wasip2. Its headers are WASI 0.1: C code + # that needs sockets does not build. + wasiCc = pkgs.pkgsCross.wasi32.stdenv.cc; + wasiCcEnv = { + CC_wasm32_wasip2 = "${wasiCc}/bin/${wasiCc.targetPrefix}cc"; + AR_wasm32_wasip2 = "${wasiCc}/bin/${wasiCc.targetPrefix}ar"; + }; craneLib = (crane.mkLib pkgs).overrideToolchain rust; common = { pname = "sausage"; @@ -48,9 +56,12 @@ cargoExtraArgs = "--locked -p sausage"; doCheck = false; nativeBuildInputs = [ pkgs.makeWrapper ]; - # The `cargo` actor runs cargo at runtime. Build scripts need a C linker. + # The `cargo` actor runs cargo at runtime. Build scripts need a C linker, + # and C code for WASI needs the WASI clang. postInstall = '' - wrapProgram $out/bin/sausage --prefix PATH : ${runtimePath} + wrapProgram $out/bin/sausage --prefix PATH : ${runtimePath} \ + --set-default CC_wasm32_wasip2 ${wasiCcEnv.CC_wasm32_wasip2} \ + --set-default AR_wasm32_wasip2 ${wasiCcEnv.AR_wasm32_wasip2} ''; meta.mainProgram = "sausage"; }); @@ -84,7 +95,7 @@ # The binary the host loop runs (`nix run`), built from the same # cached dependencies as the tests. checks.package = sausage; - checks.tests = craneLib.cargoTest (common // { + checks.tests = craneLib.cargoTest (common // wasiCcEnv // { inherit cargoArtifacts; cargoExtraArgs = "--locked ${testPackages}"; # Nothing builds on the test run's target folder: don't store it. @@ -114,7 +125,35 @@ version = "0.1.0" edition = "2024" TOML - printf 'fn main() {}\n' > "$source/src/main.rs" + # C code for WASI, compiled with the packaged WASI clang. The native + # test build has no C, so it needs no C compiler for the host. + printf 'int probe_add(int a, int b) { return a + b; }\n' > "$source/src/add.c" + cat > "$source/build.rs" <<'RUST' + use std::process::Command; + fn main() { + if std::env::var("TARGET").unwrap() != "wasm32-wasip2" { + return; + } + let out = std::env::var("OUT_DIR").unwrap(); + let cc = std::env::var("CC_wasm32_wasip2").expect("CC_wasm32_wasip2"); + let ar = std::env::var("AR_wasm32_wasip2").expect("AR_wasm32_wasip2"); + let object = format!("{out}/add.o"); + assert!(Command::new(cc).args(["-c", "src/add.c", "-o", &object]).status().unwrap().success()); + assert!(Command::new(ar).args(["crs", &format!("{out}/libadd.a"), &object]).status().unwrap().success()); + println!("cargo:rustc-link-search=native={out}"); + println!("cargo:rustc-link-lib=static=add"); + } + RUST + cat > "$source/src/main.rs" <<'RUST' + #[cfg(target_os = "wasi")] + unsafe extern "C" { + fn probe_add(a: i32, b: i32) -> i32; + } + fn main() { + #[cfg(target_os = "wasi")] + assert_eq!(unsafe { probe_add(2, 3) }, 5); + } + RUST cat > "$source/tests/probe.rs" <<'RUST' #[test] fn native_test_harness_runs() { @@ -161,11 +200,11 @@ apps.default = { type = "app"; program = "${sausage}/bin/sausage"; }; apps.loop = { type = "app"; program = "${loop}/bin/sausage-loop"; }; - devShells.default = pkgs.mkShell { + devShells.default = pkgs.mkShell (wasiCcEnv // { packages = [ rust pkgs.wasmtime pkgs.wasm-tools ]; shellHook = '' export CARGO_HOME="$PWD/.cargo-home" ''; - }; + }); }); }