wasm3x-sys 0.1.0

Raw FFI bindings to the Wasm3 WebAssembly interpreter.
Documentation
use std::env;
use std::ffi::OsStr;
use std::path::PathBuf;

/// Directory (relative to the crate root) holding the Wasm3 C sources. Wasm3 is
/// tracked as a git submodule at `crates/wasm3x-sys/wasm3`; run
/// `git submodule update --init` after cloning to populate it.
const WASM3_SOURCE: &str = "wasm3/source";

/// Directory holding our small C shim over Wasm3 internals (see `shim/wasm3x_shim.c`).
const SHIM_SOURCE: &str = "shim";

/// Only expose the public Wasm3 API (`m3_*`), our `wasm3x_*` shim, plus the
/// uppercase helper types.
const ALLOWLIST_FUNCTION: &str = "(wasm3x_|[A-Z]|m3_).*";
const ALLOWLIST_TYPE: &str = "(?:I|c_)?[Mm]3.*";

/// Wasm3 typedefs these short primitive aliases (`f32`, `u64`, ...). Blocklisting
/// them keeps the generated bindings from shadowing the Rust primitives.
const PRIMITIVES: &[&str] = &[
    "f64", "f32", "u64", "i64", "u32", "i32", "u16", "i16", "u8", "i8",
];

fn main() {
    ensure_sources_present();
    generate_bindings();
    compile_wasm3();

    println!("cargo:rerun-if-changed={WASM3_SOURCE}");
    println!("cargo:rerun-if-changed={SHIM_SOURCE}");
    println!("cargo:rerun-if-changed=build.rs");
}

/// Fails early with an actionable message if the `wasm3` submodule has not been
/// checked out (e.g. the repository was cloned without `--recursive`).
fn ensure_sources_present() {
    if !PathBuf::from(format!("{WASM3_SOURCE}/wasm3.h")).exists() {
        panic!(
            "Wasm3 sources not found at `{WASM3_SOURCE}`. The Wasm3 C library is a \
             git submodule; fetch it with:\n\n    \
             git submodule update --init crates/wasm3x-sys/wasm3\n"
        );
    }
}

/// Runs `bindgen` over `wasm3.h` and writes `$OUT_DIR/bindings.rs`.
fn generate_bindings() {
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by Cargo"));

    let mut builder = bindgen::builder()
        .header(format!("{WASM3_SOURCE}/wasm3.h"))
        .header(format!("{SHIM_SOURCE}/wasm3x_shim.h"))
        .clang_arg(format!("-I{WASM3_SOURCE}"))
        .clang_arg("-Dd_m3LogOutput=0")
        .clang_arg(format!("-Dd_m3Use32BitSlots={}", slot_bits_define()))
        .clang_arg(wasi_clang_define())
        .default_enum_style(bindgen::EnumVariation::ModuleConsts)
        .allowlist_function(ALLOWLIST_FUNCTION)
        .allowlist_type(ALLOWLIST_TYPE)
        .allowlist_var(ALLOWLIST_TYPE)
        .layout_tests(false)
        .generate_comments(false)
        .derive_debug(false);
    for primitive in PRIMITIVES {
        builder = builder.blocklist_type(primitive);
    }

    builder
        .generate()
        .expect("failed to generate Wasm3 bindings")
        .write_to_file(out_dir.join("bindings.rs"))
        .expect("failed to write Wasm3 bindings");
}

/// Compiles the Wasm3 C sources into a static `libwasm3.a`.
fn compile_wasm3() {
    let mut build = cc::Build::new();
    for entry in std::fs::read_dir(WASM3_SOURCE)
        .unwrap_or_else(|err| panic!("failed to read `{WASM3_SOURCE}`: {err}"))
    {
        let path = entry.expect("failed to read directory entry").path();
        if path.extension().and_then(OsStr::to_str) != Some("c") {
            continue;
        }
        // The `uvwasi` backend requires linking against the external `uvwasi`
        // library; we only ship the self-contained WASI backend instead.
        if path.file_name().and_then(OsStr::to_str) == Some("m3_api_uvwasi.c") {
            continue;
        }
        build.file(path);
    }
    // Our shim getter over Wasm3 internals; compiled with identical flags so the
    // `M3Global` layout it reads matches the rest of the library.
    build.file(format!("{SHIM_SOURCE}/wasm3x_shim.c"));

    build
        .include(WASM3_SOURCE)
        .define("d_m3LogOutput", Some("0"))
        .define("d_m3Use32BitSlots", Some(slot_bits_define()))
        .warnings(false)
        .extra_warnings(false);
    if cfg!(feature = "wasi") {
        build.define("d_m3HasWASI", None);
    }
    build.compile("wasm3");
}

/// `1` if the `use-32bit-slots` behaviour is desired; we default to 64-bit slots.
fn slot_bits_define() -> &'static str {
    "0"
}

fn wasi_clang_define() -> &'static str {
    if cfg!(feature = "wasi") {
        "-Dd_m3HasWASI"
    } else {
        "-DWASM3_SYS_NO_WASI"
    }
}