Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions external-jet-lib-example/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
16 changes: 16 additions & 0 deletions external-jet-lib-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 8 additions & 0 deletions external-jet-lib-example/js-host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "simplicityhl-wasm-host-example",
"private": true,
"type": "module",
"scripts": {
"run": "node run.mjs"
}
}
118 changes: 118 additions & 0 deletions external-jet-lib-example/js-host/run.mjs
Original file line number Diff line number Diff line change
@@ -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.");
69 changes: 69 additions & 0 deletions external-jet-lib-example/src/compiler_wasm.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<String>> = OnceLock::new();

fn last_error_store() -> &'static Mutex<String> {
LAST_ERROR.get_or_init(|| Mutex::new(String::new()))
}

fn set_last_error(msg: impl Into<String>) {
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() {}
29 changes: 29 additions & 0 deletions external-jet-lib-example/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternalJet, simplicityhl::simplicity::Error> {
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 {
Expand Down Expand Up @@ -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<Box<dyn JetHL>> {
jet.as_any()
.downcast_ref::<HappyJet>()
.map(|jet| Box::new(*jet) as Box<dyn JetHL>)
}

/// 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<Box<dyn JetHL>>, jet: *const dyn Jet) {
let jet = &*jet;
let value = jet
.as_any()
.downcast_ref::<HappyJet>()
.map(|jet| Box::new(*jet) as Box<dyn JetHL>);
std::ptr::write(out, value);
}
2 changes: 1 addition & 1 deletion external-jet-lib-example/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
Loading
Loading