Compare commits

..
Author SHA1 Message Date
surma a536d873e6 cargo: compile C for wasm32-wasip2 with a WASI clang
ci / check (push) Successful in 18s
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.
2026-10-22 20:16:38 +01:00
8 changed files with 112 additions and 6 deletions
+5
View File
@@ -265,6 +265,11 @@ The reply is `{"type": "built", "wasm": "/actor/cargo/public/builds/<n>/<name>.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/`.
+3
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "cargo-c"
version = "0.1.0"
edition = "2024"
[dependencies]
sausage-actor = "0.1"
[build-dependencies]
cc = "1"
[workspace]
+5
View File
@@ -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");
}
+1
View File
@@ -0,0 +1 @@
int c_add(int a, int b) { return a + b; }
+25
View File
@@ -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);
}
}
+16
View File
@@ -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");
+45 -6
View File
@@ -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"
'';
};
});
});
}