diff --git a/Cargo.lock b/Cargo.lock index 15c547f20a..fd5e712ba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4262,6 +4262,7 @@ dependencies = [ "log", "serde_json", "solidcraft-geom", + "web-time", ] [[package]] diff --git a/apps/solidcraft-web/Cargo.toml b/apps/solidcraft-web/Cargo.toml index 2339990a38..cb189f7dcc 100644 --- a/apps/solidcraft-web/Cargo.toml +++ b/apps/solidcraft-web/Cargo.toml @@ -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 } diff --git a/crates/smlib/Cargo.toml b/crates/smlib/Cargo.toml index c7f1173958..16290aa710 100644 --- a/crates/smlib/Cargo.toml +++ b/crates/smlib/Cargo.toml @@ -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"] } diff --git a/crates/smlib/build.rs b/crates/smlib/build.rs index bc9f0af6fa..98baa8bed3 100644 --- a/crates/smlib/build.rs +++ b/crates/smlib/build.rs @@ -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 { + 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, String> { let mut v: Vec = std::fs::read_dir(dir) diff --git a/crates/smlib/src/ffi.rs b/crates/smlib/src/ffi.rs index 6f4f2554be..136da360ed 100644 --- a/crates/smlib/src/ffi.rs +++ b/crates/smlib/src/ffi.rs @@ -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 = 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() + } +}