mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
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:
Generated
+1
@@ -4262,6 +4262,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user