diff --git a/external-jet-lib-example/Cargo.toml b/external-jet-lib-example/Cargo.toml index 790f703b..ea46d44a 100644 --- a/external-jet-lib-example/Cargo.toml +++ b/external-jet-lib-example/Cargo.toml @@ -8,6 +8,10 @@ rust-version = "1.79.0" name = "external-lib-consumer" path = "src/main.rs" +[[bin]] +name = "compiler-wasm" +path = "src/compiler_wasm.rs" + [lib] crate-type = ["cdylib"] diff --git a/external-jet-lib-example/README.md b/external-jet-lib-example/README.md index b2055d7e..3abb616f 100644 --- a/external-jet-lib-example/README.md +++ b/external-jet-lib-example/README.md @@ -52,6 +52,22 @@ cargo run -p external-jet-lib-example --bin external-lib-consumer -- \ target/debug/libexternal_jet_lib_example.dylib ``` +## Wasm Host Demo + +Build wasm artifacts and run the Node host: + +```sh +rustup target add wasm32-unknown-unknown + +cargo build -p external-jet-lib-example --target wasm32-unknown-unknown --bin compiler-wasm +cargo build -p external-jet-lib-example --target wasm32-unknown-unknown --lib + +cd external-jet-lib-example/js-host +npm run run +``` + +This path demonstrates the same idea as native loading, but with wasm module imports wired in JavaScript. + ## Security Notes Loading a shared library executes arbitrary native code in the current process. Only load libraries from trusted, verified sources. diff --git a/external-jet-lib-example/js-host/package.json b/external-jet-lib-example/js-host/package.json new file mode 100644 index 00000000..c7a50ef7 --- /dev/null +++ b/external-jet-lib-example/js-host/package.json @@ -0,0 +1,8 @@ +{ + "name": "simplicityhl-wasm-host-example", + "private": true, + "type": "module", + "scripts": { + "run": "node run.mjs" + } +} diff --git a/external-jet-lib-example/js-host/run.mjs b/external-jet-lib-example/js-host/run.mjs new file mode 100644 index 00000000..ee1c1848 --- /dev/null +++ b/external-jet-lib-example/js-host/run.mjs @@ -0,0 +1,178 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const repoRoot = path.resolve(__dirname, "..", ".."); +// Default plugin wasm (the external jet library compiled as wasm). +const defaultPluginPath = path.join( + repoRoot, + "target", + "wasm32-unknown-unknown", + "debug", + "external_jet_lib_example.wasm" +); +// Default compiler wasm (the host that imports jets from the plugin). +const defaultCompilerPath = path.join( + repoRoot, + "target", + "wasm32-unknown-unknown", + "debug", + "compiler-wasm.wasm" +); + +const pluginPath = process.argv[2] ?? defaultPluginPath; +const compilerPath = process.argv[3] ?? defaultCompilerPath; + +async function instantiateWasm(filePath, imports = {}) { + const bytes = await readFile(filePath); + return WebAssembly.instantiate(bytes, imports); +} + +// Minimal wasm-bindgen imports expected by these binaries. +function wasmBindgenShimImports() { + return { + __wbindgen_placeholder__: { + __wbindgen_describe() { + // No-op for this standalone host path. + }, + __wbindgen_throw() { + throw new Error("wasm-bindgen runtime requested a throw"); + }, + }, + __wbindgen_externref_xform__: { + __wbindgen_externref_table_grow() { + return -1; + }, + __wbindgen_externref_table_set_null() { + // No-op for this standalone host path. + }, + }, + }; +} + +// 1) Load the plugin first so we can pass its exports as compiler imports. +const plugin = await instantiateWasm(pluginPath, wasmBindgenShimImports()); +const pluginExports = plugin.instance.exports; +let compilerMemory = null; + +function requireCompilerMemory() { + if (!compilerMemory) { + throw new Error("compiler memory not initialized before a plugin call"); + } + return compilerMemory; +} + +function byteBridge(pluginFn, retToByteLen = (ret) => ret) { + return (jetIndex, outPtr, cap) => { + const compiler = requireCompilerMemory(); + const capNum = Number(cap); + const pluginPtr = pluginExports.__wbindgen_malloc(capNum, 1); + try { + const ret = pluginFn(jetIndex, pluginPtr, capNum); + if (ret >= 0) { + const nBytes = Math.min(retToByteLen(ret), capNum); + if (nBytes > 0) { + const src = new Uint8Array(pluginExports.memory.buffer, pluginPtr, nBytes); + new Uint8Array(compiler.buffer).set(src, Number(outPtr)); + } + } + return ret; + } finally { + pluginExports.__wbindgen_free(pluginPtr, capNum, 1); + } + }; +} + +// `parse` moves bytes in both directions: a name from the compiler to the +// plugin, and the resulting 8-byte ExternalJet handle back again. +const parseBridge = (namePtr, nameLen, outPtr) => { + const compiler = requireCompilerMemory(); + const compilerName = new Uint8Array(compiler.buffer, namePtr, nameLen); + const pluginNamePtr = pluginExports.__wbindgen_malloc(nameLen, 1); + new Uint8Array(pluginExports.memory.buffer).set(compilerName, pluginNamePtr); + + const pluginOutPtr = pluginExports.__wbindgen_malloc(8, 8); + try { + const status = pluginExports.parse(pluginNamePtr, nameLen, pluginOutPtr); + if (status === 0) { + const jetBytes = new Uint8Array(pluginExports.memory.buffer, pluginOutPtr, 8); + new Uint8Array(compiler.buffer).set(jetBytes, outPtr); + } + return status; + } finally { + pluginExports.__wbindgen_free(pluginNamePtr, nameLen, 1); + pluginExports.__wbindgen_free(pluginOutPtr, 8, 8); + } +}; + +// 2) Instantiate compiler wasm and satisfy its plugin imports. Pointer-bearing +// entry points are bridged; the scalar ones pass straight through. +const compiler = await instantiateWasm(compilerPath, { + ...wasmBindgenShimImports(), + "simplicityhl-plugin": { + cmr: byteBridge(pluginExports.cmr), + source_ty: byteBridge(pluginExports.source_ty), + target_ty: byteBridge(pluginExports.target_ty), + display: byteBridge(pluginExports.display), + source_jet_classification: byteBridge(pluginExports.source_jet_classification), + target_jet_classification: byteBridge(pluginExports.target_jet_classification), + encode: byteBridge(pluginExports.encode, (bits) => (bits + 7) >> 3), + parse: parseBridge, + // Scalar-only ABI (integers / the pointer-free ExternalJet handle). + cost: pluginExports.cost, + is_disabled: pluginExports.is_disabled, + verify: pluginExports.verify, + }, +}); +compilerMemory = compiler.instance.exports.memory; + +const getCompilerLastError = () => { + const ptrFn = compiler.instance.exports.last_error_ptr; + const lenFn = compiler.instance.exports.last_error_len; + if (typeof ptrFn !== "function" || typeof lenFn !== "function") { + return ""; + } + + const ptr = ptrFn(); + const len = lenFn(); + if (!ptr || !len) { + return ""; + } + + const bytes = new Uint8Array(compilerMemory.buffer, ptr, len); + return new TextDecoder().decode(bytes); +}; + +// 3) Execute the compiler entrypoint that compiles `assert!(true)` via plugin jets. +const compileFn = compiler.instance.exports.compile_happyjet; +if (typeof compileFn !== "function") { + throw new Error("compiler wasm does not export compile_happyjet"); +} + +const rc = compileFn(); +if (rc !== 0) { + const err = getCompilerLastError(); + throw new Error(`compile_happyjet failed with status ${rc}${err ? `: ${err}` : ""}`); +} + +console.log("HappyJet compilation succeeded via wasm plugin linkage."); + +// 4) Exercise every bridged shim (cmr, types, encode, display, classifications) +// and validate the bytes that cross the module-memory boundary. +const probeFn = compiler.instance.exports.probe_external_jet; +if (typeof probeFn !== "function") { + throw new Error("compiler wasm does not export probe_external_jet"); +} + +const probeRc = probeFn(); +if (probeRc !== 0) { + const err = getCompilerLastError(); + throw new Error( + `probe_external_jet failed with status ${probeRc}${err ? `: ${err}` : ""}` + ); +} + +console.log("External jet ABI probe passed: all shims round-trip across separate module memories."); diff --git a/external-jet-lib-example/src/compiler_wasm.rs b/external-jet-lib-example/src/compiler_wasm.rs new file mode 100644 index 00000000..069bc5d8 --- /dev/null +++ b/external-jet-lib-example/src/compiler_wasm.rs @@ -0,0 +1,203 @@ +//! WebAssembly entrypoint that compiles a tiny program using external jets. +//! +//! This binary is intended to be compiled for `wasm32-unknown-unknown` and +//! instantiated by JavaScript with imports from the plugin wasm module under +//! the import module name `simplicityhl-plugin`. + +use simplicityhl::ast::JetHinter; +#[cfg(target_arch = "wasm32")] +use simplicityhl::jet::external::init_external_jet_lib; +use simplicityhl::jet::external::ExternalJetHinter; +use simplicityhl::jet::{SourceJetClassification, TargetJetClassification}; +use simplicityhl::simplicity::jet::Jet; +use simplicityhl::simplicity::{BitWriter, Cost}; +use simplicityhl::TemplateProgram; +use std::io::Write; +use std::sync::{Mutex, OnceLock}; + +static LAST_ERROR: OnceLock> = OnceLock::new(); + +fn last_error_store() -> &'static Mutex { + LAST_ERROR.get_or_init(|| Mutex::new(String::new())) +} + +fn set_last_error(msg: impl Into) { + if let Ok(mut err) = last_error_store().lock() { + *err = msg.into(); + } +} + +fn clear_last_error() { + set_last_error(""); +} + +/// Ensure the external jet backend is initialized for this wasm instance. +#[cfg(target_arch = "wasm32")] +fn ensure_external_backend_initialized() -> Result<(), String> { + match unsafe { init_external_jet_lib() } { + Ok(()) => Ok(()), + Err(err) => { + let msg = err.to_string(); + if msg.contains("already been initialized") { + Ok(()) + } else { + Err(msg) + } + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn ensure_external_backend_initialized() -> Result<(), String> { + Err("compiler-wasm backend initialization is only supported on wasm32".to_owned()) +} + +#[no_mangle] +pub extern "C" fn last_error_ptr() -> *const u8 { + let guard = last_error_store() + .lock() + .expect("last error mutex poisoned unexpectedly"); + guard.as_ptr() +} + +#[no_mangle] +pub extern "C" fn last_error_len() -> usize { + let guard = last_error_store() + .lock() + .expect("last error mutex poisoned unexpectedly"); + guard.len() +} + +/// Compile a program that lowers to the plugin-provided `verify` jet. +/// +/// Returns: +/// - `0` on success +/// - `1` if initializing the external jet backend fails +/// - `2` if compilation fails +#[no_mangle] +pub extern "C" fn compile_happyjet() -> i32 { + clear_last_error(); + if let Err(err) = ensure_external_backend_initialized() { + set_last_error(format!("failed to initialize external jet backend: {err}")); + return 1; + } + + let code = r#"fn main() { + assert!(true); +}"#; + + match TemplateProgram::new(code, Box::new(ExternalJetHinter::new())) { + Ok(_) => 0, + Err(e) => { + set_last_error(e.to_string()); + 2 + } + } +} + +/// Invoke every plugin entry point across the wasm module boundary and +/// validate the results against `HappyJet`'s known values. +#[no_mangle] +pub extern "C" fn probe_external_jet() -> i32 { + clear_last_error(); + if let Err(err) = ensure_external_backend_initialized() { + set_last_error(format!("failed to initialize external jet backend: {err}")); + return 1; + } + + let hinter = ExternalJetHinter::new(); + + // `parse` (bridged): a known name resolves, an unknown name does not. + let Some(jet) = hinter.parse_jet("verify") else { + set_last_error("parse_jet(\"verify\") returned None"); + return 1; + }; + if hinter.parse_jet("no_such_jet").is_some() { + set_last_error("parse_jet(\"no_such_jet\") unexpectedly returned Some"); + return 2; + } + + let low: &dyn Jet = jet.as_jet(); + + // `cmr` shim: fixed 32-byte identity. + const EXPECTED_CMR: [u8; 32] = [ + 0xcd, 0xca, 0x2a, 0x05, 0xe5, 0x2c, 0xef, 0xa5, 0x9d, 0xc7, 0xa5, 0xb0, 0xda, 0xe2, 0x20, + 0x98, 0xfb, 0x89, 0x6e, 0x39, 0x13, 0xbf, 0xdd, 0x44, 0x6b, 0x59, 0x4e, 0x1f, 0x92, 0x50, + 0x78, 0x3e, + ]; + if low.cmr().to_byte_array() != EXPECTED_CMR { + set_last_error("cmr mismatch across module boundary"); + return 3; + } + + // `source_ty` / `target_ty` shims: Polish-notation type-name bytes. + if low.source_ty().0 != b"2".as_slice() { + set_last_error("source_ty mismatch across module boundary"); + return 4; + } + if low.target_ty().0 != b"1".as_slice() { + set_last_error("target_ty mismatch across module boundary"); + return 5; + } + + // `cost` (scalar): unchanged, but validated for completeness. + if low.cost() != Cost::from_milliweight(44) { + set_last_error("cost mismatch across module boundary"); + return 6; + } + + // `display` shim: the jet's textual name. `dyn Jet: Display`. + if low.to_string() != "verify" { + set_last_error(format!("display mismatch: got {:?}", low.to_string())); + return 7; + } + + // Classification shims: heap-backed enums serialized through the shared + // wire format. + match jet.source_jet_classification() { + SourceJetClassification::Custom(types) + if types.len() == 1 && types[0] == simplicityhl::jet::bool() => {} + other => { + set_last_error(format!("source classification mismatch: {other:?}")); + return 8; + } + } + match jet.target_jet_classification() { + TargetJetClassification::Unary => {} + other => { + set_last_error(format!("target classification mismatch: {other:?}")); + return 9; + } + } + + // `is_disabled` (scalar). + if jet.is_disabled() { + set_last_error("is_disabled should be false"); + return 10; + } + + // `encode` shim: the packed jet code, replayed into a real BitWriter. + let mut encoded: Vec = Vec::new(); + let n_bits = { + let sink: &mut dyn Write = &mut encoded; + let mut writer: BitWriter<&mut dyn Write> = BitWriter::new(sink); + let Ok(n_bits) = low.encode(&mut writer) else { + set_last_error("encode failed across module boundary"); + return 11; + }; + if writer.flush_all().is_err() { + set_last_error("flushing encoded bits failed"); + return 11; + } + n_bits + }; + // `verify` encodes to the single bit `0`, i.e. one byte `0x00`. + if n_bits != 1 || encoded != [0u8] { + set_last_error(format!("encode mismatch: {n_bits} bits, bytes {encoded:?}")); + return 12; + } + + 0 +} + +fn main() {} diff --git a/external-jet-lib-example/src/lib.rs b/external-jet-lib-example/src/lib.rs index c2eba00c..7b3ab15c 100644 --- a/external-jet-lib-example/src/lib.rs +++ b/external-jet-lib-example/src/lib.rs @@ -26,13 +26,22 @@ //! //! # ABI contract //! -//! The set of symbol names and their exact signatures must match -//! `ExternalJetLib::load` in the host crate (`src/jet/external.rs`). The loader -//! transmutes each resolved address into a Rust `fn` pointer **without** checking -//! the signature, so any mismatch is undefined behaviour. These functions use the -//! default Rust ABI rather than `extern "C"`, that is sound only because the host -//! and this library are built with the same toolchain and share the exact same -//! `simplicity` / `simplicityhl` types. +//! On native targets the set of symbol names and their exact signatures must +//! match `ExternalJetDynamicLib::load` in the host crate +//! (`src/jet/external/dynamic.rs`). The loader transmutes each resolved address +//! into a Rust `fn` pointer **without** checking the signature, so any mismatch +//! is undefined behaviour. These functions use the default Rust ABI rather than +//! `extern "C"`; that is sound only because the host and this library are built +//! with the same toolchain and share the exact same `simplicity` / `simplicityhl` +//! types. +//! +//! On `wasm32` the compiler and this plugin are separate modules with separate +//! linear memories, so any Rust value that carries a pointer (a `String`, a +//! `TypeName`, a classification, ...) cannot be shared directly: the pointer +//! would be read against the wrong memory. Each such entry point is therefore +//! compiled as an `extern "C"` shim with an explicit `(index, out_ptr, cap) -> +//! i32` signature that serialises the value into a caller-owned buffer. These +//! shims must match the imports declared in `src/jet/external/wasm.rs`. //! //! # Safety //! @@ -42,11 +51,15 @@ use std::io::Write; use simplicityhl::{ - jet::{JetHL, SourceJetClassification, TargetJetClassification}, - simplicity::{ - jet::{type_name::TypeName, Jet}, - BitWriter, Cmr, Cost, - }, + jet::JetHL, + simplicity::{jet::Jet, BitWriter, Cost}, +}; +// Types named only in the native (default-ABI) exports; the wasm32 shims use +// the shared byte serialisation instead, so these would be unused there. +#[cfg(not(target_arch = "wasm32"))] +use simplicityhl::{ + jet::{SourceJetClassification, TargetJetClassification}, + simplicity::{jet::type_name::TypeName, Cmr}, }; use crate::jet::{ExternalJet, HappyJet}; @@ -54,7 +67,29 @@ use crate::jet::{ExternalJet, HappyJet}; /// Jet definitions ([`HappyJet`]) and their [`Jet`]/[`JetHL`] implementations. pub mod jet; +/// Copy `bytes` into the caller-owned buffer described by `out_ptr`/`cap` (which +/// live in *this* plugin module's memory) and report the value's full length. +/// +/// This is the write half of the wasm ptr/len/out ABI: because the compiler and +/// the plugin have separate linear memories, the host bridge, not the compiler, +/// hands us a buffer in our own memory. We copy at most `cap` bytes and always +/// return the total length, so a caller that probed with a small buffer can grow +/// it and call again. +/// +/// # Safety +/// +/// `out_ptr` must be either null or valid for writes of `cap` bytes. +#[cfg(target_arch = "wasm32")] +unsafe fn write_out(bytes: &[u8], out_ptr: *mut u8, cap: usize) -> i32 { + let n = bytes.len().min(cap); + if !out_ptr.is_null() && n > 0 { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr, n); + } + bytes.len() as i32 +} + /// Exports [`HappyJet::cmr`]: the jet's Commitment Merkle Root (its identity). +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn cmr(jet: ExternalJet) -> Cmr { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -62,7 +97,16 @@ pub fn cmr(jet: ExternalJet) -> Cmr { jet.cmr() } +/// wasm32 shim for [`cmr`]: writes the 32-byte CMR into the caller's buffer. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn cmr(index: u64, out_ptr: *mut u8, cap: usize) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + write_out(&jet.cmr().to_byte_array(), out_ptr, cap) +} + /// Exports [`HappyJet::source_ty`]: the jet's Simplicity source (input) type. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn source_ty(jet: ExternalJet) -> TypeName { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -70,7 +114,16 @@ pub fn source_ty(jet: ExternalJet) -> TypeName { jet.source_ty() } +/// wasm32 shim for [`source_ty`]: writes the type-name bytes into the buffer. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn source_ty(index: u64, out_ptr: *mut u8, cap: usize) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + write_out(jet.source_ty().0, out_ptr, cap) +} + /// Exports [`HappyJet::target_ty`]: the jet's Simplicity target (output) type. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn target_ty(jet: ExternalJet) -> TypeName { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -78,6 +131,14 @@ pub fn target_ty(jet: ExternalJet) -> TypeName { jet.target_ty() } +/// wasm32 shim for [`target_ty`]: writes the type-name bytes into the buffer. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn target_ty(index: u64, out_ptr: *mut u8, cap: usize) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + write_out(jet.target_ty().0, out_ptr, cap) +} + /// Exports [`HappyJet::encode`]: serialises the jet into a program's bit stream. /// /// The host passes its own [`BitWriter`] (the bit-level framing the Simplicity @@ -85,6 +146,7 @@ pub fn target_ty(jet: ExternalJet) -> TypeName { /// delegates to [`HappyJet::encode`]. The signature must match the /// `ExternalJetLib::encode` field in the host crate exactly — see the /// module-level note on the ABI contract. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn encode(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -92,6 +154,35 @@ pub fn encode(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::R jet.encode(w) } +/// wasm32 shim for [`encode`]. +/// +/// The compiler owns the real [`BitWriter`] (in its memory), which cannot be +/// shared here, so instead we serialise into a local byte buffer, copy the +/// packed bits into the caller's buffer, and return the **bit** count. The +/// compiler replays those bits into its writer. Bytes are MSB-first, exactly as +/// [`BitWriter`] packs them. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn encode(index: u64, out_ptr: *mut u8, cap: usize) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + + let mut bytes: Vec = Vec::new(); + let n_bits; + { + let sink: &mut dyn Write = &mut bytes; + let mut w: BitWriter<&mut dyn Write> = BitWriter::new(sink); + jet.encode(&mut w).expect("encoding to a vec never fails"); + n_bits = w.n_total_written(); + w.flush_all().expect("flushing a vec never fails"); + } + + let n = bytes.len().min(cap); + if !out_ptr.is_null() && n > 0 { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr, n); + } + n_bits as i32 +} + /// Exports [`HappyJet::cost`]: the jet's execution cost (in milliweight units). #[no_mangle] pub fn cost(jet: ExternalJet) -> Cost { @@ -106,11 +197,28 @@ pub fn cost(jet: ExternalJet) -> Cost { /// how the host turns the identifier written after `jet::` into a handle. On /// success the resulting [`HappyJet`] is reduced to its [`ExternalJet`] index for /// return across the boundary. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn parse(s: &str) -> Result { HappyJet::parse(s).map(|jet| ExternalJet { index: jet.index() }) } + +/// wasm32 shim for the current compiler import signature of `parse`. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn parse(name_ptr: *const u8, name_len: usize, out: *mut ExternalJet) -> i32 { + let bytes = std::slice::from_raw_parts(name_ptr, name_len); + let Ok(name) = std::str::from_utf8(bytes) else { + return 1; + }; + let Ok(jet) = HappyJet::parse(name) else { + return 1; + }; + std::ptr::write(out, ExternalJet { index: jet.index() }); + 0 +} /// Exports the [`Display`](std::fmt::Display) name of the jet. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn display(jet: ExternalJet) -> String { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -118,8 +226,17 @@ pub fn display(jet: ExternalJet) -> String { jet.to_string() } +/// wasm32 shim for [`display`]: writes the UTF-8 name into the caller's buffer. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn display(index: u64, out_ptr: *mut u8, cap: usize) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + write_out(jet.to_string().as_bytes(), out_ptr, cap) +} + /// Exports [`JetHL::source_jet_classification`]: how the compiler splits the /// source type into high-level argument types. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn source_jet_classification(jet: ExternalJet) -> SourceJetClassification { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -127,8 +244,30 @@ pub fn source_jet_classification(jet: ExternalJet) -> SourceJetClassification { jet.source_jet_classification() } +/// wasm32 shim for [`source_jet_classification`]. +/// +/// The classification carries heap-allocated `AliasedType`s, so it is flattened +/// with the shared [`serialize_source_jet_classification`] wire format before +/// being copied into the caller's buffer. +/// +/// [`serialize_source_jet_classification`]: simplicityhl::jet::external::serialize_source_jet_classification +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn source_jet_classification( + index: u64, + out_ptr: *mut u8, + cap: usize, +) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + let bytes = simplicityhl::jet::external::serialize_source_jet_classification( + &jet.source_jet_classification(), + ); + write_out(&bytes, out_ptr, cap) +} + /// Exports [`JetHL::target_jet_classification`]: the high-level return type of /// the jet. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn target_jet_classification(jet: ExternalJet) -> TargetJetClassification { let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); @@ -136,6 +275,22 @@ pub fn target_jet_classification(jet: ExternalJet) -> TargetJetClassification { jet.target_jet_classification() } +/// wasm32 shim for [`target_jet_classification`], mirroring +/// [`source_jet_classification`]'s serialisation across the memory boundary. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn target_jet_classification( + index: u64, + out_ptr: *mut u8, + cap: usize, +) -> i32 { + let jet = HappyJet::from_index(index).expect("invalid jet index"); + let bytes = simplicityhl::jet::external::serialize_target_jet_classification( + &jet.target_jet_classification(), + ); + write_out(&bytes, out_ptr, cap) +} + /// Exports [`JetHL::is_disabled`]: whether the jet may be named directly in /// SimplicityHL source. #[no_mangle] @@ -165,6 +320,7 @@ pub fn verify() -> ExternalJet { /// [`HappyJet`] and re-box it as a [`JetHL`], re-attaching the high-level /// behaviour. It returns [`None`] if the jet does not belong to this library. /// This mirrors the `conjure` method of the built-in jet hinters. +#[cfg(not(target_arch = "wasm32"))] #[no_mangle] pub fn conjure(jet: &dyn Jet) -> Option> { jet.as_any() diff --git a/src/jet/external.rs b/src/jet/external.rs deleted file mode 100644 index d49c7a82..00000000 --- a/src/jet/external.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::sync::OnceLock; -use std::{io::Write, path::Path}; - -use simplicity::{ - jet::{type_name::TypeName, Jet}, - BitIter, BitWriter, Cmr, Cost, -}; - -use crate::ast::JetHinter; -use crate::jet::dynlib::Library; -use crate::jet::{JetHL, SourceJetClassification, TargetJetClassification}; - -static EXTERNAL_JET_LIB: OnceLock = OnceLock::new(); - -/// Load the external jet library from the specified shared object file path -/// -/// # Safety -/// -/// The caller must ensure that the loaded library exports each of the -/// symbols listed below with signatures matching the corresponding -/// fields of [`ExternalJetLib`]. Calling a function through a -/// mismatched signature is undefined behavior. -pub unsafe fn init_external_jet_lib(path: &str) -> Result<(), Box> { - let library = unsafe { Library::load(Path::new(path))? }; - let api = unsafe { ExternalJetLib::load(library)? }; - - if EXTERNAL_JET_LIB.set(api).is_err() { - return Err("Failed to set external jet lib, it may have already been initialized".into()); - } - - Ok(()) -} - -fn external_jet_lib() -> &'static ExternalJetLib { - EXTERNAL_JET_LIB - .get() - .expect("External jet lib is not initialized. Please call init_external_jet_lib first.") -} - -/// Symbol table loaded from an external jet shared library. -/// -/// Each field is a function pointer resolved from a `#[no_mangle]` export of -/// the same name in the library. -pub struct ExternalJetLib { - cmr: fn(jet: ExternalJet) -> Cmr, - source_ty: fn(jet: ExternalJet) -> TypeName, - target_ty: fn(jet: ExternalJet) -> TypeName, - encode: fn(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result, - cost: fn(jet: ExternalJet) -> Cost, - parse: fn(s: &str) -> Result, - display: fn(jet: ExternalJet) -> String, - - source_jet_classification: fn(jet: ExternalJet) -> SourceJetClassification, - target_jet_classification: fn(jet: ExternalJet) -> TargetJetClassification, - is_disabled: fn(jet: ExternalJet) -> bool, - - verify: fn() -> ExternalJet, - conjure: fn(jet: &dyn Jet) -> Option>, - - // Keep the library loaded, symbols above are only valid while it lives. - _library: Library, -} - -impl ExternalJetLib { - /// Resolve all required symbols from `library`. - /// - /// # Safety - /// - /// The caller must ensure that the loaded library exports each of the - /// symbols listed below with signatures matching the corresponding - /// fields of [`ExternalJetLib`]. Calling a function through a - /// mismatched signature is undefined behavior. - unsafe fn load(library: Library) -> Result> { - let cmr = library.symbol("cmr")?; - let source_ty = library.symbol("source_ty")?; - let target_ty = library.symbol("target_ty")?; - let encode = library.symbol("encode")?; - let cost = library.symbol("cost")?; - let parse = library.symbol("parse")?; - let display = library.symbol("display")?; - let source_jet_classification = library.symbol("source_jet_classification")?; - let target_jet_classification = library.symbol("target_jet_classification")?; - let is_disabled = library.symbol("is_disabled")?; - let verify = library.symbol("verify")?; - let conjure = library.symbol("conjure")?; - - Ok(Self { - cmr, - source_ty, - target_ty, - encode, - cost, - parse, - display, - source_jet_classification, - target_jet_classification, - is_disabled, - verify, - conjure, - _library: library, - }) - } -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] -pub struct ExternalJet { - pub index: u64, -} - -impl ExternalJet { - pub fn new(index: u64) -> Self { - Self { index } - } -} - -impl Jet for ExternalJet { - fn cmr(&self) -> Cmr { - let container = external_jet_lib(); - (container.cmr)(*self) - } - - fn source_ty(&self) -> TypeName { - let container = external_jet_lib(); - (container.source_ty)(*self) - } - - fn target_ty(&self) -> TypeName { - let container = external_jet_lib(); - (container.target_ty)(*self) - } - - fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { - let container = external_jet_lib(); - (container.encode)(*self, w) - } - - fn decode>( - _bits: &mut BitIter, - ) -> Result - where - Self: Sized, - { - unimplemented!("Decoding is not implemented for ExternalJet for now") - } - - fn cost(&self) -> Cost { - let container = external_jet_lib(); - (container.cost)(*self) - } - - fn parse(s: &str) -> Result - where - Self: Sized, - { - let container = external_jet_lib(); - (container.parse)(s) - } -} - -impl std::fmt::Display for ExternalJet { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let container = external_jet_lib(); - let display_str = (container.display)(*self); - write!(f, "{}", display_str) - } -} - -impl JetHL for ExternalJet { - fn source_jet_classification(&self) -> SourceJetClassification { - let container = external_jet_lib(); - (container.source_jet_classification)(*self) - } - - fn target_jet_classification(&self) -> TargetJetClassification { - let container = external_jet_lib(); - (container.target_jet_classification)(*self) - } - - fn is_disabled(&self) -> bool { - let container = external_jet_lib(); - (container.is_disabled)(*self) - } - - fn clone_box(&self) -> Box { - Box::new(*self) - } - - fn as_jet(&self) -> &dyn Jet { - self - } -} - -#[derive(Clone, Debug, Default)] -pub struct ExternalJetHinter; - -impl ExternalJetHinter { - pub fn new() -> Self { - Self - } -} - -impl JetHinter for ExternalJetHinter { - fn parse_jet(&self, name: &str) -> Option> { - let container = external_jet_lib(); - match (container.parse)(name) { - Ok(jet) => Some(Box::new(jet)), - Err(_) => None, - } - } - - fn construct_verify(&self) -> Box { - let container = external_jet_lib(); - let jet = (container.verify)(); - Box::new(jet) - } - - fn clone_box(&self) -> Box { - Box::new(ExternalJetHinter) - } - - fn conjure(&self, jet: &dyn Jet) -> Option> { - let container = external_jet_lib(); - (container.conjure)(jet) - } -} diff --git a/src/jet/external/dynamic.rs b/src/jet/external/dynamic.rs new file mode 100644 index 00000000..bb37618c --- /dev/null +++ b/src/jet/external/dynamic.rs @@ -0,0 +1,137 @@ +use std::io::Write; + +use std::sync::OnceLock; + +use simplicity::{ + jet::{type_name::TypeName, Jet}, + BitWriter, Cmr, Cost, +}; + +use crate::jet::{ + external::{loaders::dynlib::Library, ExternalJet, ExternalJetLib}, + JetHL, SourceJetClassification, TargetJetClassification, +}; + +pub(super) static EXTERNAL_JET_DYNAMIC_LIB: OnceLock = OnceLock::new(); + +/// Symbol table loaded from an external jet dynamic library. +/// +/// Each field is a function pointer resolved from a `#[no_mangle]` export of +/// the same name in the library. +pub struct ExternalJetDynamicLib { + pub cmr: fn(jet: ExternalJet) -> Cmr, + pub source_ty: fn(jet: ExternalJet) -> TypeName, + pub target_ty: fn(jet: ExternalJet) -> TypeName, + pub encode: fn(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result, + pub cost: fn(jet: ExternalJet) -> Cost, + pub parse: fn(s: &str) -> Result, + pub display: fn(jet: ExternalJet) -> String, + + pub source_jet_classification: fn(jet: ExternalJet) -> SourceJetClassification, + pub target_jet_classification: fn(jet: ExternalJet) -> TargetJetClassification, + pub is_disabled: fn(jet: ExternalJet) -> bool, + + pub verify: fn() -> ExternalJet, + pub conjure: fn(jet: &dyn Jet) -> Option>, + + // Keep the library loaded, symbols above are only valid while it lives. + _library: Library, +} + +impl ExternalJetDynamicLib { + /// Resolve all required symbols from `library`. + /// + /// # Safety + /// + /// The caller must ensure that the loaded library exports each of the + /// symbols listed below with signatures matching the corresponding + /// fields of [`ExternalJetLib`]. Calling a function through a + /// mismatched signature is undefined behavior. + pub unsafe fn load(library: Library) -> Result> { + let cmr = library.symbol("cmr")?; + let source_ty = library.symbol("source_ty")?; + let target_ty = library.symbol("target_ty")?; + let encode = library.symbol("encode")?; + let cost = library.symbol("cost")?; + let parse = library.symbol("parse")?; + let display = library.symbol("display")?; + let source_jet_classification = library.symbol("source_jet_classification")?; + let target_jet_classification = library.symbol("target_jet_classification")?; + let is_disabled = library.symbol("is_disabled")?; + let verify = library.symbol("verify")?; + let conjure = library.symbol("conjure")?; + + Ok(Self { + cmr, + source_ty, + target_ty, + encode, + cost, + parse, + display, + source_jet_classification, + target_jet_classification, + is_disabled, + verify, + conjure, + _library: library, + }) + } +} + +impl ExternalJetLib for ExternalJetDynamicLib { + fn cmr(&self, jet: ExternalJet) -> Cmr { + (self.cmr)(jet) + } + + fn source_ty(&self, jet: ExternalJet) -> TypeName { + (self.source_ty)(jet) + } + + fn target_ty(&self, jet: ExternalJet) -> TypeName { + (self.target_ty)(jet) + } + + fn encode( + &self, + jet: ExternalJet, + w: &mut BitWriter<&mut dyn Write>, + ) -> std::io::Result { + (self.encode)(jet, w) + } + + fn cost(&self, jet: ExternalJet) -> Cost { + (self.cost)(jet) + } + + fn parse(&self, s: &str) -> Result + where + Self: Sized, + { + (self.parse)(s) + } + + fn display(&self, jet: ExternalJet) -> String { + (self.display)(jet) + } + + fn source_jet_classification(&self, jet: ExternalJet) -> SourceJetClassification { + (self.source_jet_classification)(jet) + } + + fn target_jet_classification(&self, jet: ExternalJet) -> TargetJetClassification { + (self.target_jet_classification)(jet) + } + + fn is_disabled(&self, jet: ExternalJet) -> bool { + (self.is_disabled)(jet) + } + + fn verify(&self) -> ExternalJet { + (self.verify)() + } + + fn conjure(&self, jet: &dyn Jet) -> Option> { + (self.conjure)(jet) + } +} diff --git a/src/jet/dynlib.rs b/src/jet/external/loaders/dynlib.rs similarity index 100% rename from src/jet/dynlib.rs rename to src/jet/external/loaders/dynlib.rs diff --git a/src/jet/external/loaders/mod.rs b/src/jet/external/loaders/mod.rs new file mode 100644 index 00000000..e64e3092 --- /dev/null +++ b/src/jet/external/loaders/mod.rs @@ -0,0 +1,2 @@ +#[cfg(not(target_arch = "wasm32"))] +pub mod dynlib; diff --git a/src/jet/external/mod.rs b/src/jet/external/mod.rs new file mode 100644 index 00000000..4d89d8cc --- /dev/null +++ b/src/jet/external/mod.rs @@ -0,0 +1,336 @@ +#[cfg(not(target_arch = "wasm32"))] +mod dynamic; +mod loaders; +#[cfg(target_arch = "wasm32")] +mod wasm; + +use std::io::Write; + +#[cfg(not(target_arch = "wasm32"))] +use std::path::Path; + +use simplicity::{ + jet::{type_name::TypeName, Jet}, + BitIter, BitWriter, Cmr, Cost, +}; + +use crate::ast::JetHinter; +use crate::jet::{JetHL, SourceJetClassification, TargetJetClassification}; +use crate::parse::ParseFromStr; +use crate::types::AliasedType; + +#[cfg(target_arch = "wasm32")] +use crate::jet::external::wasm::ExternalJetWasmLib; +#[cfg(not(target_arch = "wasm32"))] +use crate::jet::external::{dynamic::ExternalJetDynamicLib, loaders::dynlib::Library}; + +/// Load the external jet library. When compiled for wasm32, the library is loaded +/// from the current environment. When compiled for other targets +/// the library is loaded from the specified path. +/// +/// # Safety +/// +/// The caller must ensure that the loaded library exports each of the +/// symbols listed below with signatures matching the corresponding +/// fields of [`ExternalJetLib`]. Calling a function through a +/// mismatched signature is undefined behavior. +#[cfg(target_arch = "wasm32")] +pub unsafe fn init_external_jet_lib() -> Result<(), Box> { + let api = ExternalJetWasmLib::load(); + if wasm::EXTERNAL_JET_WASM_LIB.set(api).is_err() { + return Err("Failed to set external jet lib, it may have already been initialized".into()); + } + Ok(()) +} + +/// Load the external jet library from a dynamic library path on non-wasm targets. +/// +/// # Safety +/// +/// The caller must ensure that the loaded library exports each of the +/// symbols listed below with signatures matching the corresponding +/// fields of [`ExternalJetLib`]. Calling a function through a +/// mismatched signature is undefined behavior. +#[cfg(not(target_arch = "wasm32"))] +pub unsafe fn init_external_jet_lib(path: &str) -> Result<(), Box> { + let library = unsafe { Library::load(Path::new(path))? }; + let api = unsafe { ExternalJetDynamicLib::load(library)? }; + + if dynamic::EXTERNAL_JET_DYNAMIC_LIB.set(api).is_err() { + return Err("Failed to set external jet lib, it may have already been initialized".into()); + } + + Ok(()) +} + +fn external_jet_lib() -> &'static dyn ExternalJetLib { + #[cfg(target_arch = "wasm32")] + { + wasm::EXTERNAL_JET_WASM_LIB + .get() + .expect("External jet lib is not initialized. Please call init_external_jet_lib first.") + as &dyn ExternalJetLib + } + + #[cfg(not(target_arch = "wasm32"))] + { + dynamic::EXTERNAL_JET_DYNAMIC_LIB + .get() + .expect("External jet lib is not initialized. Please call init_external_jet_lib first.") + as &dyn ExternalJetLib + } +} + +/// External jet integration interface. +/// +/// It is used by different loadable libraries backend to connect their +/// implementations of required jets. +pub trait ExternalJetLib { + // Jet methods + fn cmr(&self, jet: ExternalJet) -> Cmr; + fn source_ty(&self, jet: ExternalJet) -> TypeName; + fn target_ty(&self, jet: ExternalJet) -> TypeName; + fn encode(&self, jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) + -> std::io::Result; + fn cost(&self, jet: ExternalJet) -> Cost; + fn parse(&self, s: &str) -> Result; + fn display(&self, jet: ExternalJet) -> String; + + // JetHL methods + fn source_jet_classification(&self, jet: ExternalJet) -> SourceJetClassification; + fn target_jet_classification(&self, jet: ExternalJet) -> TargetJetClassification; + fn is_disabled(&self, jet: ExternalJet) -> bool; + + // JetHinter methods + fn verify(&self) -> ExternalJet; + fn conjure(&self, jet: &dyn Jet) -> Option>; +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct ExternalJet { + pub index: u64, +} + +impl ExternalJet { + pub fn new(index: u64) -> Self { + Self { index } + } +} + +impl Jet for ExternalJet { + fn cmr(&self) -> Cmr { + let container = external_jet_lib(); + container.cmr(*self) + } + + fn source_ty(&self) -> TypeName { + let container = external_jet_lib(); + container.source_ty(*self) + } + + fn target_ty(&self) -> TypeName { + let container = external_jet_lib(); + container.target_ty(*self) + } + + fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { + let container = external_jet_lib(); + container.encode(*self, w) + } + + fn decode>( + _bits: &mut BitIter, + ) -> Result + where + Self: Sized, + { + unimplemented!("Decoding is not implemented for ExternalJet for now") + } + + fn cost(&self) -> Cost { + let container = external_jet_lib(); + container.cost(*self) + } + + fn parse(s: &str) -> Result + where + Self: Sized, + { + let container = external_jet_lib(); + container.parse(s) + } +} + +impl std::fmt::Display for ExternalJet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let container = external_jet_lib(); + let display_str = container.display(*self); + write!(f, "{}", display_str) + } +} + +impl JetHL for ExternalJet { + fn source_jet_classification(&self) -> SourceJetClassification { + let container = external_jet_lib(); + container.source_jet_classification(*self) + } + + fn target_jet_classification(&self) -> TargetJetClassification { + let container = external_jet_lib(); + container.target_jet_classification(*self) + } + + fn is_disabled(&self) -> bool { + let container = external_jet_lib(); + container.is_disabled(*self) + } + + fn clone_box(&self) -> Box { + Box::new(*self) + } + + fn as_jet(&self) -> &dyn Jet { + self + } +} + +#[derive(Clone, Debug, Default)] +pub struct ExternalJetHinter; + +impl ExternalJetHinter { + pub fn new() -> Self { + Self + } +} + +impl JetHinter for ExternalJetHinter { + fn parse_jet(&self, name: &str) -> Option> { + let container = external_jet_lib(); + match container.parse(name) { + Ok(jet) => Some(Box::new(jet)), + Err(_) => None, + } + } + + fn construct_verify(&self) -> Box { + let container = external_jet_lib(); + let jet = container.verify(); + Box::new(jet) + } + + fn clone_box(&self) -> Box { + Box::new(ExternalJetHinter) + } + + fn conjure(&self, jet: &dyn Jet) -> Option> { + let container = external_jet_lib(); + container.conjure(jet) + } +} + +/// Serialize a [`SourceJetClassification`] into a portable byte buffer. +/// +/// Layout: +/// - 1 tag byte: `0` Unary, `1` Binary, `2` Ternary, `3` Quaternary, `4` Custom. +/// - for `Custom`: a little-endian `u32` element count, then, for each element, +/// a little-endian `u32` byte length followed by the UTF-8 [`Display`] form of +/// the [`AliasedType`]. +pub fn serialize_source_jet_classification(classification: &SourceJetClassification) -> Vec { + let mut out = Vec::new(); + match classification { + SourceJetClassification::Unary => out.push(0), + SourceJetClassification::Binary => out.push(1), + SourceJetClassification::Ternary => out.push(2), + SourceJetClassification::Quaternary => out.push(3), + SourceJetClassification::Custom(types) => { + out.push(4); + out.extend_from_slice(&(types.len() as u32).to_le_bytes()); + for ty in types { + write_aliased_type(&mut out, ty); + } + } + } + out +} + +/// Inverse of [`serialize_source_jet_classification`]. +/// +/// Returns `None` if the buffer is truncated, carries an unknown tag, or holds +/// a type string that fails to parse. +pub fn deserialize_source_jet_classification(bytes: &[u8]) -> Option { + let (&tag, mut rest) = bytes.split_first()?; + match tag { + 0 => Some(SourceJetClassification::Unary), + 1 => Some(SourceJetClassification::Binary), + 2 => Some(SourceJetClassification::Ternary), + 3 => Some(SourceJetClassification::Quaternary), + 4 => { + let count = read_u32(&mut rest)? as usize; + let mut types = Vec::with_capacity(count); + for _ in 0..count { + types.push(read_aliased_type(&mut rest)?); + } + Some(SourceJetClassification::Custom(types)) + } + _ => None, + } +} + +/// Serialize a [`TargetJetClassification`] into a portable byte buffer. +/// +/// Layout: +/// - 1 tag byte: `0` Unary, `1` Custom. +/// - for `Custom`: a little-endian `u32` byte length followed by the UTF-8 +/// [`Display`] form of the [`AliasedType`]. +pub fn serialize_target_jet_classification(classification: &TargetJetClassification) -> Vec { + let mut out = Vec::new(); + match classification { + TargetJetClassification::Unary => out.push(0), + TargetJetClassification::Custom(ty) => { + out.push(1); + write_aliased_type(&mut out, ty); + } + } + out +} + +/// Inverse of [`serialize_target_jet_classification`]. +/// +/// Returns `None` if the buffer is truncated, carries an unknown tag, or holds +/// a type string that fails to parse. +pub fn deserialize_target_jet_classification(bytes: &[u8]) -> Option { + let (&tag, mut rest) = bytes.split_first()?; + match tag { + 0 => Some(TargetJetClassification::Unary), + 1 => Some(TargetJetClassification::Custom(read_aliased_type( + &mut rest, + )?)), + _ => None, + } +} + +fn write_aliased_type(out: &mut Vec, ty: &AliasedType) { + let s = ty.to_string(); + out.extend_from_slice(&(s.len() as u32).to_le_bytes()); + out.extend_from_slice(s.as_bytes()); +} + +fn read_u32(rest: &mut &[u8]) -> Option { + if rest.len() < 4 { + return None; + } + let (head, tail) = rest.split_at(4); + *rest = tail; + Some(u32::from_le_bytes(head.try_into().ok()?)) +} + +fn read_aliased_type(rest: &mut &[u8]) -> Option { + let len = read_u32(rest)? as usize; + if rest.len() < len { + return None; + } + let (head, tail) = rest.split_at(len); + *rest = tail; + let s = std::str::from_utf8(head).ok()?; + AliasedType::parse_from_str(s).ok() +} diff --git a/src/jet/external/wasm.rs b/src/jet/external/wasm.rs new file mode 100644 index 00000000..61946c3d --- /dev/null +++ b/src/jet/external/wasm.rs @@ -0,0 +1,193 @@ +#![cfg(target_arch = "wasm32")] + +use std::collections::HashSet; +use std::io::Write; +use std::sync::{Mutex, OnceLock}; + +use simplicity::{ + jet::{type_name::TypeName, Jet}, + BitWriter, Cmr, Cost, +}; + +use crate::jet::{ + external::{ + deserialize_source_jet_classification, deserialize_target_jet_classification, ExternalJet, + ExternalJetLib, + }, + JetHL, SourceJetClassification, TargetJetClassification, +}; + +/// External jet backend for `wasm32` builds. +/// +/// All methods are imported from the `simplicityhl-plugin` wasm module, +/// which the embedding host (e.g. a browser) provides. +#[derive(Clone, Debug, Default)] +pub struct ExternalJetWasmLib; + +pub(crate) static EXTERNAL_JET_WASM_LIB: OnceLock = OnceLock::new(); + +impl ExternalJetWasmLib { + pub fn load() -> Self { + Self + } +} + +#[link(wasm_import_module = "simplicityhl-plugin")] +#[allow(improper_ctypes)] +extern "C" { + fn cmr(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + fn source_ty(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + fn target_ty(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + fn encode(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + fn display(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + fn source_jet_classification(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + fn target_jet_classification(index: u64, out_ptr: *mut u8, cap: usize) -> i32; + + fn cost(jet: ExternalJet) -> Cost; + fn parse(name_ptr: *const u8, name_len: usize, out: *mut ExternalJet) -> i32; + fn is_disabled(jet: ExternalJet) -> bool; + fn verify() -> ExternalJet; +} + +/// Call a `(index, out_ptr, cap) -> i32` plugin shim, growing the buffer as +/// needed, and return the exact bytes the plugin produced. +fn read_shim i32>(call: F) -> Option> { + const INITIAL_CAP: usize = 128; + let mut buf = vec![0u8; INITIAL_CAP]; + let needed = call(buf.as_mut_ptr(), buf.len()); + if needed < 0 { + return None; + } + let needed = needed as usize; + if needed > buf.len() { + buf = vec![0u8; needed]; + let again = call(buf.as_mut_ptr(), buf.len()); + if again < 0 || again as usize != needed { + return None; + } + } + buf.truncate(needed); + Some(buf) +} + +/// Intern plugin-provided [`TypeName`] bytes to obtain the `'static` slice that +/// [`TypeName`] requires. +/// +/// The bytes originate in the plugin's memory and are copied into ours on every +/// call, so they are not `'static` on their own. We leak a single copy per +/// distinct byte string (jets expose only a handful of type names) and reuse it +/// forever, which keeps the leak bounded to the set of type names in use. +fn intern_type_name(bytes: &[u8]) -> TypeName { + static INTERNER: OnceLock>> = OnceLock::new(); + let interner = INTERNER.get_or_init(|| Mutex::new(HashSet::new())); + let mut guard = interner.lock().expect("type-name interner poisoned"); + if let Some(existing) = guard.get(bytes) { + return TypeName(*existing); + } + let leaked: &'static [u8] = Box::leak(bytes.to_vec().into_boxed_slice()); + guard.insert(leaked); + TypeName(leaked) +} + +impl ExternalJetLib for ExternalJetWasmLib { + fn cmr(&self, jet: ExternalJet) -> Cmr { + let mut bytes = [0u8; 32]; + let status = unsafe { cmr(jet.index, bytes.as_mut_ptr(), bytes.len()) }; + assert_eq!(status, 32, "plugin cmr must return exactly 32 bytes"); + Cmr::from_byte_array(bytes) + } + + fn source_ty(&self, jet: ExternalJet) -> TypeName { + let bytes = read_shim(|ptr, cap| unsafe { source_ty(jet.index, ptr, cap) }) + .expect("plugin source_ty failed"); + intern_type_name(&bytes) + } + + fn target_ty(&self, jet: ExternalJet) -> TypeName { + let bytes = read_shim(|ptr, cap| unsafe { target_ty(jet.index, ptr, cap) }) + .expect("plugin target_ty failed"); + intern_type_name(&bytes) + } + + fn encode( + &self, + jet: ExternalJet, + w: &mut BitWriter<&mut dyn Write>, + ) -> std::io::Result { + let mut buf = [0u8; 64]; + let n_bits = unsafe { encode(jet.index, buf.as_mut_ptr(), buf.len()) }; + if n_bits < 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "external jet encode failed", + )); + } + let n_bits = n_bits as usize; + let n_bytes = (n_bits + 7) / 8; + if n_bytes > buf.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "external jet encode output exceeds buffer", + )); + } + // Replay the packed, MSB-first bit buffer into the real writer. + let full = n_bits / 8; + let rem = n_bits % 8; + for &byte in &buf[..full] { + w.write_bits_be(byte as u64, 8)?; + } + if rem > 0 { + let last = (buf[full] >> (8 - rem)) as u64; + w.write_bits_be(last, rem)?; + } + Ok(n_bits) + } + + fn cost(&self, jet: ExternalJet) -> Cost { + unsafe { cost(jet) } + } + + fn parse(&self, s: &str) -> Result { + let mut jet = ExternalJet::new(0); + let status = unsafe { parse(s.as_ptr(), s.len(), &mut jet) }; + if status == 0 { + Ok(jet) + } else { + Err(simplicity::Error::InvalidJetName(s.to_owned())) + } + } + + fn display(&self, jet: ExternalJet) -> String { + let bytes = read_shim(|ptr, cap| unsafe { display(jet.index, ptr, cap) }) + .expect("plugin display failed"); + String::from_utf8(bytes).expect("plugin display returned invalid UTF-8") + } + + fn source_jet_classification(&self, jet: ExternalJet) -> SourceJetClassification { + let bytes = read_shim(|ptr, cap| unsafe { source_jet_classification(jet.index, ptr, cap) }) + .expect("plugin source_jet_classification failed"); + deserialize_source_jet_classification(&bytes) + .expect("plugin returned malformed source jet classification") + } + + fn target_jet_classification(&self, jet: ExternalJet) -> TargetJetClassification { + let bytes = read_shim(|ptr, cap| unsafe { target_jet_classification(jet.index, ptr, cap) }) + .expect("plugin target_jet_classification failed"); + deserialize_target_jet_classification(&bytes) + .expect("plugin returned malformed target jet classification") + } + + fn is_disabled(&self, jet: ExternalJet) -> bool { + unsafe { is_disabled(jet) } + } + + fn verify(&self) -> ExternalJet { + unsafe { verify() } + } + + fn conjure(&self, jet: &dyn Jet) -> Option> { + jet.as_any() + .downcast_ref::() + .map(|jet| Box::new(*jet) as Box) + } +} diff --git a/src/jet/mod.rs b/src/jet/mod.rs index 9fb3e200..9198661a 100644 --- a/src/jet/mod.rs +++ b/src/jet/mod.rs @@ -1,6 +1,4 @@ pub mod core; -#[cfg(feature = "external-jets")] -mod dynlib; pub mod elements; #[cfg(feature = "external-jets")] pub mod external;