From 43ec289bf79b447d7f804d75bc94192b376a9c4e Mon Sep 17 00:00:00 2001 From: ivanlele Date: Fri, 26 Jun 2026 18:56:58 +0300 Subject: [PATCH 1/2] make the external jet loader interchangable --- src/jet/external.rs | 225 ----------------------- src/jet/external/dynamic.rs | 138 ++++++++++++++ src/jet/{ => external/loaders}/dynlib.rs | 1 + src/jet/external/loaders/mod.rs | 1 + src/jet/external/mod.rs | 189 +++++++++++++++++++ src/jet/mod.rs | 2 - 6 files changed, 329 insertions(+), 227 deletions(-) delete mode 100644 src/jet/external.rs create mode 100644 src/jet/external/dynamic.rs rename src/jet/{ => external/loaders}/dynlib.rs (99%) create mode 100644 src/jet/external/loaders/mod.rs create mode 100644 src/jet/external/mod.rs 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..ba4d1f6d --- /dev/null +++ b/src/jet/external/dynamic.rs @@ -0,0 +1,138 @@ +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. +#[derive(Clone)] +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 99% rename from src/jet/dynlib.rs rename to src/jet/external/loaders/dynlib.rs index 553ca409..be9d215e 100644 --- a/src/jet/dynlib.rs +++ b/src/jet/external/loaders/dynlib.rs @@ -19,6 +19,7 @@ impl std::error::Error for Error {} /// Handle to a dynamically loaded shared library. /// /// The library is unloaded when this value is dropped. +#[derive(Clone)] pub struct Library { handle: *mut c_void, } diff --git a/src/jet/external/loaders/mod.rs b/src/jet/external/loaders/mod.rs new file mode 100644 index 00000000..c23115ad --- /dev/null +++ b/src/jet/external/loaders/mod.rs @@ -0,0 +1 @@ +pub mod dynlib; diff --git a/src/jet/external/mod.rs b/src/jet/external/mod.rs new file mode 100644 index 00000000..398bfcf6 --- /dev/null +++ b/src/jet/external/mod.rs @@ -0,0 +1,189 @@ +mod dynamic; +mod loaders; + +use std::{io::Write, path::Path, rc::Rc}; + +use simplicity::{ + jet::{type_name::TypeName, Jet}, + BitIter, BitWriter, Cmr, Cost, +}; + +use crate::jet::{ + external::dynamic::ExternalJetDynamicLib, JetHL, SourceJetClassification, + TargetJetClassification, +}; +use crate::{ast::JetHinter, jet::external::loaders::dynlib::Library}; + +/// 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 { 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() -> Rc { + let lib = dynamic::EXTERNAL_JET_DYNAMIC_LIB + .get() + .expect("External jet lib is not initialized. Please call init_external_jet_lib first."); + + Rc::new(lib.clone()) +} + +/// 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) + } +} 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; From 76d9f4ae0a4908c8919fa17a300b355c70ec947d Mon Sep 17 00:00:00 2001 From: ivanlele Date: Tue, 30 Jun 2026 14:15:49 +0300 Subject: [PATCH 2/2] Add wasm external jet backend --- external-jet-lib-example/Cargo.toml | 4 + external-jet-lib-example/README.md | 16 +++ external-jet-lib-example/js-host/package.json | 8 ++ external-jet-lib-example/js-host/run.mjs | 118 ++++++++++++++++++ external-jet-lib-example/src/compiler_wasm.rs | 69 ++++++++++ external-jet-lib-example/src/lib.rs | 29 +++++ external-jet-lib-example/src/main.rs | 2 +- src/jet/external/loaders/mod.rs | 1 + src/jet/external/mod.rs | 62 ++++++--- src/jet/external/wasm.rs | 107 ++++++++++++++++ 10 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 external-jet-lib-example/js-host/package.json create mode 100644 external-jet-lib-example/js-host/run.mjs create mode 100644 external-jet-lib-example/src/compiler_wasm.rs create mode 100644 src/jet/external/wasm.rs 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..566edbd2 --- /dev/null +++ b/external-jet-lib-example/js-host/run.mjs @@ -0,0 +1,118 @@ +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; +const parseBridge = (namePtr, nameLen, outPtr) => { + if (!compilerMemory) { + throw new Error("compiler memory not initialized before parse call"); + } + + const compilerName = new Uint8Array(compilerMemory.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); + const status = pluginExports.parse(pluginNamePtr, nameLen, pluginOutPtr); + if (status === 0) { + const jetBytes = new Uint8Array(pluginExports.memory.buffer, pluginOutPtr, 8); + new Uint8Array(compilerMemory.buffer).set(jetBytes, outPtr); + } + + return status; +}; + +// 2) Instantiate compiler wasm and satisfy its plugin imports. +const compiler = await instantiateWasm(compilerPath, { + ...wasmBindgenShimImports(), + "simplicityhl-plugin": { + ...pluginExports, + parse: parseBridge, + }, +}); +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."); 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..78038468 --- /dev/null +++ b/external-jet-lib-example/src/compiler_wasm.rs @@ -0,0 +1,69 @@ +//! 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::{jet::external::ExternalJetHinter, TemplateProgram}; +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(""); +} + +#[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 unsafe { simplicityhl::jet::external::init_external_jet_lib(None) }.is_err() { + set_last_error("failed to initialize external jet backend"); + 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 + } + } +} + +fn main() {} diff --git a/external-jet-lib-example/src/lib.rs b/external-jet-lib-example/src/lib.rs index c2eba00c..6c9e1b8d 100644 --- a/external-jet-lib-example/src/lib.rs +++ b/external-jet-lib-example/src/lib.rs @@ -106,10 +106,26 @@ 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. #[no_mangle] pub fn display(jet: ExternalJet) -> String { @@ -165,9 +181,22 @@ 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() .downcast_ref::() .map(|jet| Box::new(*jet) as Box) } + +/// wasm32 shim for the current compiler import signature of `conjure`. +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub unsafe extern "C" fn conjure(out: *mut Option>, jet: *const dyn Jet) { + let jet = &*jet; + let value = jet + .as_any() + .downcast_ref::() + .map(|jet| Box::new(*jet) as Box); + std::ptr::write(out, value); +} diff --git a/external-jet-lib-example/src/main.rs b/external-jet-lib-example/src/main.rs index c4389271..f1d1944b 100644 --- a/external-jet-lib-example/src/main.rs +++ b/external-jet-lib-example/src/main.rs @@ -33,7 +33,7 @@ fn main() { // the process-global `ExternalJetLib` table. This must happen exactly once // and before any program that uses external jets is compiled. unsafe { - simplicityhl::jet::external::init_external_jet_lib(&lib_path) + simplicityhl::jet::external::init_external_jet_lib(Some(&lib_path)) .expect("failed to initialize external jet lib"); } diff --git a/src/jet/external/loaders/mod.rs b/src/jet/external/loaders/mod.rs index c23115ad..e64e3092 100644 --- a/src/jet/external/loaders/mod.rs +++ b/src/jet/external/loaders/mod.rs @@ -1 +1,2 @@ +#[cfg(not(target_arch = "wasm32"))] pub mod dynlib; diff --git a/src/jet/external/mod.rs b/src/jet/external/mod.rs index 398bfcf6..9420a3ba 100644 --- a/src/jet/external/mod.rs +++ b/src/jet/external/mod.rs @@ -1,20 +1,30 @@ +#[cfg(not(target_arch = "wasm32"))] mod dynamic; mod loaders; +#[cfg(target_arch = "wasm32")] +mod wasm; -use std::{io::Write, path::Path, rc::Rc}; +use std::{io::Write, rc::Rc}; + +#[cfg(not(target_arch = "wasm32"))] +use std::path::Path; use simplicity::{ jet::{type_name::TypeName, Jet}, BitIter, BitWriter, Cmr, Cost, }; -use crate::jet::{ - external::dynamic::ExternalJetDynamicLib, JetHL, SourceJetClassification, - TargetJetClassification, -}; -use crate::{ast::JetHinter, jet::external::loaders::dynlib::Library}; +use crate::ast::JetHinter; +use crate::jet::{JetHL, SourceJetClassification, TargetJetClassification}; -/// Load the external jet library from the specified shared object file path +#[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 /// @@ -22,18 +32,42 @@ use crate::{ast::JetHinter, jet::external::loaders::dynlib::Library}; /// 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 { 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()); +#[allow(unused_variables)] +pub unsafe fn init_external_jet_lib(path: Option<&str>) -> Result<(), Box> { + #[cfg(target_arch = "wasm32")] + { + 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(), + ); + } + return Ok(()); } - Ok(()) + #[cfg(not(target_arch = "wasm32"))] + { + let path = path.ok_or("Path must be provided for non-wasm targets")?; + 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() -> Rc { + #[cfg(target_arch = "wasm32")] + let lib = wasm::EXTERNAL_JET_WASM_LIB + .get() + .expect("External jet lib is not initialized. Please call init_external_jet_lib first."); + + #[cfg(not(target_arch = "wasm32"))] let lib = dynamic::EXTERNAL_JET_DYNAMIC_LIB .get() .expect("External jet lib is not initialized. Please call init_external_jet_lib first."); diff --git a/src/jet/external/wasm.rs b/src/jet/external/wasm.rs new file mode 100644 index 00000000..ccb1c0ca --- /dev/null +++ b/src/jet/external/wasm.rs @@ -0,0 +1,107 @@ +#![cfg(target_arch = "wasm32")] + +use std::io::Write; +use std::sync::OnceLock; + +use simplicity::{ + jet::{type_name::TypeName, Jet}, + BitWriter, Cmr, Cost, +}; + +use crate::jet::{ + external::{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(jet: ExternalJet) -> Cmr; + fn source_ty(jet: ExternalJet) -> TypeName; + fn target_ty(jet: ExternalJet) -> TypeName; + fn encode(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result; + fn cost(jet: ExternalJet) -> Cost; + fn parse(name_ptr: *const u8, name_len: usize, out: *mut ExternalJet) -> i32; + fn display(jet: ExternalJet) -> String; + fn source_jet_classification(jet: ExternalJet) -> SourceJetClassification; + fn target_jet_classification(jet: ExternalJet) -> TargetJetClassification; + fn is_disabled(jet: ExternalJet) -> bool; + fn verify() -> ExternalJet; +} + +impl ExternalJetLib for ExternalJetWasmLib { + fn cmr(&self, jet: ExternalJet) -> Cmr { + unsafe { cmr(jet) } + } + + fn source_ty(&self, jet: ExternalJet) -> TypeName { + unsafe { source_ty(jet) } + } + + fn target_ty(&self, jet: ExternalJet) -> TypeName { + unsafe { target_ty(jet) } + } + + fn encode( + &self, + jet: ExternalJet, + w: &mut BitWriter<&mut dyn Write>, + ) -> std::io::Result { + unsafe { encode(jet, w) } + } + + 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 { + unsafe { display(jet) } + } + + fn source_jet_classification(&self, jet: ExternalJet) -> SourceJetClassification { + unsafe { source_jet_classification(jet) } + } + + fn target_jet_classification(&self, jet: ExternalJet) -> TargetJetClassification { + unsafe { target_jet_classification(jet) } + } + + 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) + } +}