SMLib in the browser: built with wasi-sdk for wasm32-unknown-unknown

On the web target the smlib build compiles SMLib for wasm32-wasip1 with wasi-sdk's clang
(WASI_SDK_PATH), with wasm exceptions in the exnref encoding that wasi-sdk's libc++ and Rust
use, and links wasi-sdk's libc++, libc++abi, libunwind and libc. The WASI calls libc makes are
defined in ffi.rs: stdout and stderr go to the log, the clocks are the page's (the C API times
every call), and there is no file system or environment. solidcraft-web gets an smlib
feature, off until CI has wasi-sdk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
This commit is contained in:
jyviko
2026-10-26 21:15:34 -04:00
parent bc15221fa3
commit e476aba94f
5 changed files with 186 additions and 0 deletions
Generated
+1
View File
@@ -4262,6 +4262,7 @@ dependencies = [
"log",
"serde_json",
"solidcraft-geom",
"web-time",
]
[[package]]
+3
View File
@@ -16,6 +16,9 @@ path = "src/main.rs"
[features]
default = []
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
# Link SMLib into the kernel (solidcraft-engine/smlib): needs scripts/fetch-smlib.sh and wasi-sdk in
# WASI_SDK_PATH (crates/smlib/build.rs). Off by default until CI has wasi-sdk.
smlib = ["solidcraft-engine/smlib"]
[dependencies]
solidcraft-engine = { workspace = true }
+4
View File
@@ -28,6 +28,10 @@ native = []
log = { workspace = true }
solidcraft-geom = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# The clocks SMLib's C library asks for in a browser build (src/ffi.rs).
web-time = { workspace = true }
[build-dependencies]
cc = { version = "1.2", features = ["parallel"] }
+34
View File
@@ -11,6 +11,7 @@ fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=SMLIB_REV");
println!("cargo:rerun-if-env-changed=SOLIDCRAFT_SMLIB_DIR");
println!("cargo:rerun-if-env-changed=WASI_SDK_PATH");
if std::env::var_os("CARGO_FEATURE_NATIVE").is_none() {
return;
}
@@ -42,6 +43,7 @@ fn build() -> Result<(), String> {
let api = sources(&src.join("SM_API/src"), &["StdAfx.cpp"])?;
let target = std::env::var("TARGET").unwrap_or_default();
let msvc = target.contains("msvc");
let wasi = if target == "wasm32-unknown-unknown" { Some(wasi_sdk()?) } else { None };
let base = || {
let mut b = cc::Build::new();
@@ -50,6 +52,13 @@ fn build() -> Result<(), String> {
if msvc {
b.flag("/EHsc").flag("/bigobj");
}
if let Some(sdk) = &wasi {
b.compiler(sdk.join("bin/clang++")).archiver(sdk.join("bin/llvm-ar")).target(WASI_TARGET);
b.flag(format!("--sysroot={}", sdk.join("share/wasi-sysroot").display())).flag("-fwasm-exceptions");
// The exnref encoding, as wasi-sdk's libc++ and Rust use: a module may not mix it with
// the legacy one clang still defaults to.
b.flag("-mllvm").flag("-wasm-use-legacy-eh=false");
}
b
};
let quiet = |b: &mut cc::Build| {
@@ -77,9 +86,34 @@ fn build() -> Result<(), String> {
k.files(&core).include(src.join("SMLib/inc"));
quiet(&mut k);
k.try_compile("smlib_core").map_err(|e| e.to_string())?;
// The C++ runtime and the C library SMLib needs, from wasi-sdk: the exception-handling build of
// libc++. The few WASI calls libc makes are answered by src/wasi.rs.
if let Some(sdk) = &wasi {
let lib = sdk.join("share/wasi-sysroot/lib").join(WASI_TARGET);
println!("cargo:rustc-link-search=native={}", lib.join("eh").display());
println!("cargo:rustc-link-search=native={}", lib.display());
for l in ["c++", "c++abi", "unwind", "c"] {
println!("cargo:rustc-link-lib=static={l}");
}
}
Ok(())
}
/// The C target SMLib is compiled for inside a `wasm32-unknown-unknown` build: wasi-sdk's, the
/// one with a C++ standard library.
const WASI_TARGET: &str = "wasm32-wasip1";
/// wasi-sdk, from `WASI_SDK_PATH` (github.com/WebAssembly/wasi-sdk, release 34 or later).
fn wasi_sdk() -> Result<PathBuf, String> {
let sdk =
std::env::var_os("WASI_SDK_PATH").map(PathBuf::from).ok_or("building SMLib for wasm32-unknown-unknown needs wasi-sdk: set WASI_SDK_PATH")?;
if !sdk.join("bin/clang++").is_file() {
return Err(format!("WASI_SDK_PATH={} has no bin/clang++", sdk.display()));
}
Ok(sdk)
}
/// The `.cpp` files in `dir`, sorted, without `skip`.
fn sources(dir: &Path, skip: &[&str]) -> Result<Vec<PathBuf>, String> {
let mut v: Vec<PathBuf> = std::fs::read_dir(dir)
+144
View File
@@ -259,3 +259,147 @@ mod primitive_calls {
}
pub use primitive_calls::*;
// ---- WASI calls (wasm32-unknown-unknown) -------------------------------------------------------
/// In a browser build SMLib is compiled against wasi-sdk's C library (build.rs), which calls a few
/// WASI functions. The page has no WASI host, so they are defined here, where wasm-ld finds them
/// before it would leave them as imports. There is no file system or environment: those calls fail
/// with WASI's own error codes. The clocks are the page's (the C API times every call). What
/// C writes to stdout or stderr goes to the `log` crate (target `smlib`).
#[cfg(all(feature = "native", target_arch = "wasm32", target_os = "unknown"))]
mod wasi {
const SUCCESS: i32 = 0;
const BADF: i32 = 8;
const INVAL: i32 = 28;
const NOSYS: i32 = 52;
const NOTCAPABLE: i32 = 76;
/// One `__wasi_ciovec_t`: a buffer and its length.
#[repr(C)]
struct Ciovec {
buf: *const u8,
len: usize,
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_write(fd: i32, iovs: *const Ciovec, n: usize, written: *mut usize) -> i32 {
if !(1..=2).contains(&fd) {
return BADF;
}
let mut text = Vec::new();
for i in 0..n {
// libc passes `n` valid iovecs, each a valid buffer, for the call's duration.
let v = unsafe { &*iovs.add(i) };
if !v.buf.is_null() {
text.extend_from_slice(unsafe { std::slice::from_raw_parts(v.buf, v.len) });
}
}
for line in String::from_utf8_lossy(&text).lines().filter(|l| !l.trim().is_empty()) {
log::debug!(target: "smlib", "{line}");
}
if !written.is_null() {
unsafe { *written = text.len() };
}
SUCCESS
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_environ_sizes_get(count: *mut usize, size: *mut usize) -> i32 {
if !count.is_null() && !size.is_null() {
unsafe {
*count = 0;
*size = 0;
}
}
SUCCESS
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_environ_get(_: *mut *mut u8, _: *mut u8) -> i32 {
SUCCESS
}
/// Clock 0 is the wall clock; the others (monotonic, process and thread time) all count from
/// the first call, through the page's monotonic clock.
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_clock_time_get(id: i32, _: i64, time: *mut u64) -> i32 {
static START: std::sync::OnceLock<web_time::Instant> = std::sync::OnceLock::new();
let d = match id {
0 => match web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) {
Ok(d) => d,
Err(_) => return NOSYS,
},
1..=3 => START.get_or_init(web_time::Instant::now).elapsed(),
_ => return INVAL,
};
if time.is_null() {
return INVAL;
}
unsafe { *time = u64::try_from(d.as_nanos()).unwrap_or(u64::MAX) };
SUCCESS
}
/// No preopened directories: libc stops looking at the first descriptor that is not one.
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_prestat_get(_: i32, _: *mut u8) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_prestat_dir_name(_: i32, _: *mut u8, _: usize) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_fdstat_get(_: i32, _: *mut u8) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_fdstat_set_flags(_: i32, _: i32) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_close(_: i32) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_read(_: i32, _: *const Ciovec, _: usize, _: *mut usize) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_seek(_: i32, _: i64, _: i32, _: *mut u64) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_fd_renumber(_: i32, _: i32) -> i32 {
BADF
}
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_path_open(
_: i32,
_: i32,
_: *const u8,
_: usize,
_: i32,
_: i64,
_: i64,
_: i32,
_: *mut i32,
) -> i32 {
NOTCAPABLE
}
/// C's `exit` and `abort`: the C API catches everything, so this is a kernel defect; stop the
/// module as a native build would stop the process.
#[unsafe(no_mangle)]
extern "C" fn __imported_wasi_snapshot_preview1_proc_exit(_: i32) -> ! {
std::arch::wasm32::unreachable()
}
}