From 7266e9cb9e958cd8485d1f744ac45fc47760397d Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 12 Feb 2024 11:13:30 -0800 Subject: [PATCH 01/20] chore: update depdenencies, add dependabot --- .github/dependabot.yml | 11 +++++++++++ README.md | 2 +- crates/cli/Cargo.toml | 12 ++++++------ crates/core/Cargo.toml | 4 ++-- 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e8d486a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/README.md b/README.md index caf89f7..d74e6ce 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ declare module 'main' { export function greet(): I32; } -declare module 'extism:host' { +declare module 'extism:host/user' { interface user { myHostFunction1(ptr: I64): I64; myHostFunction2(ptr: I64): I64; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index effa4fa..839893f 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,15 +12,15 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } -wizer = "^3.0.0" +wizer = "4.0.0" structopt = "0.3" binaryen = "0.12.0" swc_atoms = "0.6.5" swc_common = "0.33.10" -swc_ecma_ast = "0.110.11" -swc_ecma_parser = "0.141.29" -wasm-encoder = "0.38.1" -wasmparser = "0.118.1" +swc_ecma_ast = "0.112" +swc_ecma_parser = "0.143" +wasm-encoder = "0.41.2" +wasmparser = "0.121.2" log = "0.4.20" tempfile = "3.8.1" -env_logger = "^0.10" +env_logger = "0.11" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 259cdd4..66899b5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -8,10 +8,10 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -extism-pdk = "1.0.0-rc1" +extism-pdk = "1.0.0" once_cell = "1.16" anyhow = { workspace = true } -quickjs-wasm-rs = "^2.0.1" +quickjs-wasm-rs = "3" [lib] crate_type = ["cdylib"] From 18ea1ac18409e2f5e21d4ae3e2fff73bdb2649ec Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 12 Feb 2024 15:51:39 -0800 Subject: [PATCH 02/20] refactor: use wagen for wasm codegen, rework shim generation --- crates/cli/Cargo.toml | 1 + crates/cli/src/main.rs | 65 +++----- crates/cli/src/shims.rs | 257 ++++++++++---------------------- crates/cli/src/ts_parser.rs | 215 +++++++++----------------- crates/core/src/globals.rs | 14 +- crates/core/src/lib.rs | 143 ++++++++++++++++-- examples/host_funcs/host.py | 2 +- examples/host_funcs/script.d.ts | 2 +- 8 files changed, 310 insertions(+), 389 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 839893f..a805a2d 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -21,6 +21,7 @@ swc_ecma_ast = "0.112" swc_ecma_parser = "0.143" wasm-encoder = "0.41.2" wasmparser = "0.121.2" +wagen = {git = "https://github.com/dylibso/wagen"} log = "0.4.20" tempfile = "3.8.1" env_logger = "0.11" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a650fd4..c4b2064 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -54,31 +54,24 @@ fn main() -> Result<()> { input_file.read_to_end(&mut user_code)?; // If we have imports, we need to inject some state needed for host function support - let names = &plugin_interface - .imports - .functions - .iter() - .map(|s| format!("'{}'", &s.name)) - .collect::>() - .join(","); - let mut contents = format!("Host.__hostFunctions = [{}].sort();\n", names) - .as_bytes() - .to_owned(); + let mut contents = Vec::new(); + for ns in &plugin_interface.imports { + let names = &ns + .functions + .iter() + .map(|s| format!("'{}'", &s.name)) + .collect::>() + .join(","); + contents + .extend_from_slice(format!("Host.__hostFunctions = [{}].sort();\n", names).as_bytes()); + } contents.append(&mut user_code); // Create a tmp dir to hold all the library objects // This can go away once we do all the wasm-merge stuff in process let tmp_dir = TempDir::new()?; let core_path = tmp_dir.path().join("core.wasm"); - let export_shim_path = tmp_dir.path().join("export-shim.wasm"); - let import_shim_path = tmp_dir.path().join("import-shim.wasm"); - let linked_shim_path = tmp_dir.path().join("linked.wasm"); - - // let tmp_dir = "/tmp/derp"; - // let core_path = PathBuf::from("/tmp/derp/core.wasm"); - // let export_shim_path = PathBuf::from("/tmp/derp/export-shim.wasm"); - // let import_shim_path = PathBuf::from("/tmp/derp/import-shim.wasm"); - // let linked_shim_path = PathBuf::from("/tmp/derp/linked.wasm"); + let shim_path = tmp_dir.path().join("shim.wasm"); // First wizen the core module let self_cmd = env::args().next().expect("Expected a command argument"); @@ -101,13 +94,11 @@ fn main() -> Result<()> { } } - // Create our shim files given our parsed TS module object - // We have a shim file for exports and one optional one for imports + // Create our shim file given our parsed TS module object generate_wasm_shims( - plugin_interface.exports, - &export_shim_path, - plugin_interface.imports, - &import_shim_path, + &shim_path, + &plugin_interface.exports, + &plugin_interface.imports, )?; let output = Command::new("wasm-merge").arg("--version").output(); @@ -115,26 +106,12 @@ fn main() -> Result<()> { bail!("Failed to execute wasm-merge. Please install binaryen and make sure wasm-merge is on your path: https://github.com/WebAssembly/binaryen"); } - // Merge the export shim with the core module + // Merge the shim with the core module let mut command = Command::new("wasm-merge") .arg(&core_path) - .arg("coremod") - .arg(&export_shim_path) - .arg("codemod") - .arg("-o") - .arg(&linked_shim_path) - .spawn()?; - let status = command.wait()?; - if !status.success() { - bail!("wasm-merge failed. Couldn't merge export shim"); - } - - // Merge the import shim with the core+export (linked) module - let mut command = Command::new("wasm-merge") - .arg(&linked_shim_path) - .arg("coremod") - .arg(&import_shim_path) - .arg("codemod") + .arg("core") + .arg(&shim_path) + .arg("shim") .arg("-o") .arg(&opts.output) .arg("--enable-reference-types") @@ -142,7 +119,7 @@ fn main() -> Result<()> { .spawn()?; let status = command.wait()?; if !status.success() { - bail!("wasm-merge failed. Couldn't merge import shim."); + bail!("wasm-merge failed. Couldn't merge shim"); } Ok(()) diff --git a/crates/cli/src/shims.rs b/crates/cli/src/shims.rs index 4420a01..979743a 100644 --- a/crates/cli/src/shims.rs +++ b/crates/cli/src/shims.rs @@ -1,192 +1,97 @@ -extern crate swc_common; -extern crate swc_ecma_parser; use anyhow::Result; -use std::fs::File; -use std::io::prelude::*; -use std::path::PathBuf; - -use wasm_encoder::{ - CodeSection, ConstExpr, ElementSection, Elements, EntityType, ExportKind, ExportSection, - Function, FunctionSection, HeapType, Instruction, TableSection, TableType, TypeSection, - ValType, -}; -use wasm_encoder::{ImportSection, Module as WasmModule}; +use std::{collections::HashMap, path::Path}; use crate::ts_parser::Interface; +use wagen::{Instr, ValType}; /// Generates the wasm shim for the exports pub fn generate_wasm_shims( - exports: Interface, - export_path: &PathBuf, - imports: Interface, - import_path: &PathBuf, + path: impl AsRef, + exports: &Interface, + imports: &[Interface], ) -> Result<()> { - let mut export_mod = WasmModule::new(); - - // Note: the order in which you set the sections - // with `export_mod.section()` is important - - // Encode the type section. - let mut types = TypeSection::new(); - // __invoke's type - let params = vec![ValType::I32]; - let results = vec![ValType::I32]; - types.function(params, results); - // Extism Export type - let params = vec![]; - let results = vec![ValType::I32]; - types.function(params, results); - export_mod.section(&types); - - //Encode the import section - let mut import_sec = ImportSection::new(); - import_sec.import("coremod", "__invoke", EntityType::Function(0)); - export_mod.section(&import_sec); - - // Encode the function section. - let mut functions = FunctionSection::new(); - - // we will have 1 thunk function per export - let type_index = 1; // these are exports () -> i32 - for _ in exports.functions.iter() { - functions.function(type_index); - } - export_mod.section(&functions); - - let mut func_index = 1; - - // Encode the export section. - let mut export_sec = ExportSection::new(); - // we need to sort them alphabetically because that is - // how the runtime maps indexes - let mut export_functions = exports.functions.clone(); - export_functions.sort_by(|a, b| a.name.cmp(&b.name)); - for i in export_functions.iter() { - export_sec.export(i.name.as_str(), ExportKind::Func, func_index); - func_index += 1; - } - export_mod.section(&export_sec); - - // Encode the code section. - let mut codes = CodeSection::new(); - let mut export_idx: i32 = 0; - - // create a single thunk per export - for _ in exports.functions.iter() { - let locals = vec![]; - let mut f = Function::new(locals); - // we will essentially call the eval function (__invoke) - f.instruction(&Instruction::I32Const(export_idx)); - f.instruction(&Instruction::Call(0)); - f.instruction(&Instruction::End); - codes.function(&f); - export_idx += 1; - } - export_mod.section(&codes); - - // Extract the encoded Wasm bytes for this module. - let wasm_bytes = export_mod.finish(); - let mut file = File::create(export_path)?; - file.write_all(wasm_bytes.as_ref())?; - - // Now do the imports - let mut import_mod = WasmModule::new(); - - // Encode the type section. - let mut types = TypeSection::new(); - - // for all other host funcs (TODO fix) - if !imports.functions.is_empty() { - let params = vec![ValType::I64]; - let results = vec![ValType::I64]; - types.function(params, results); + let mut module = wagen::Module::new(); + + let mut functions = HashMap::new(); + let __arg_start = module.import("core", "__arg_start", None, [], []); + let __arg_i32 = module.import("core", "__arg_i32", None, [ValType::I32], []); + let __arg_i64 = module.import("core", "__arg_i64", None, [ValType::I64], []); + let __arg_f32 = module.import("core", "__arg_f32", None, [ValType::F32], []); + let __arg_f64 = module.import("core", "__arg_f64", None, [ValType::F64], []); + let __invoke_i32 = module.import("core", "__invoke_i32", None, [ValType::I32], [ValType::I32]); + let __invoke_i64 = module.import("core", "__invoke_i64", None, [ValType::I32], [ValType::I64]); + let __invoke_f32 = module.import("core", "__invoke_f32", None, [ValType::I32], [ValType::F32]); + let __invoke_f64 = module.import("core", "__invoke_f64", None, [ValType::I32], [ValType::F64]); + let __invoke = module.import("core", "__invoke", None, [ValType::I32], []); + + for import in imports.iter() { + for f in import.functions.iter() { + let params: Vec<_> = f.params.iter().map(|x| x.ptype).collect(); + let results: Vec<_> = f.results.iter().map(|x| x.ptype).collect(); + let index = module.import(&import.name, &f.name, None, params, results); + functions.insert(f.name.as_str(), index.index()); + } } - // for __invokeHostFunc - let params = vec![ValType::I32, ValType::I64]; - let results = vec![ValType::I64]; - types.function(params, results); - import_mod.section(&types); - - // Encode the import section - if !imports.functions.is_empty() { - let mut import_sec = ImportSection::new(); - - for i in imports.functions.iter() { - import_sec.import( - "extism:host/user", - i.name.as_str(), - wasm_encoder::EntityType::Function(0), + for (idx, export) in exports.functions.iter().enumerate() { + let params: Vec<_> = export.params.iter().map(|x| x.ptype).collect(); + let results: Vec<_> = export.results.iter().map(|x| x.ptype).collect(); + if results.len() > 1 { + anyhow::bail!( + "Multiple return arguments are not currently supported but used in exported function {}", + export.name ); } - import_mod.section(&import_sec); - } - - // Encode the function section. - let func_type = if imports.functions.is_empty() { 0 } else { 1 }; - let mut functions = FunctionSection::new(); - functions.function(func_type); - import_mod.section(&functions); - - if !imports.functions.is_empty() { - // encode tables pointing to imports - let mut tables = TableSection::new(); - let table_type = TableType { - element_type: wasm_encoder::RefType { - nullable: true, - heap_type: HeapType::Func, - }, - minimum: imports.functions.len() as u32, - maximum: None, - }; - tables.table(table_type); - import_mod.section(&tables); - } - - // Encode the export section. - let mut export_sec = ExportSection::new(); - export_sec.export( - "__invokeHostFunc", - ExportKind::Func, - imports.functions.len() as u32, // will be the last function - ); - import_mod.section(&export_sec); - - if !imports.functions.is_empty() { - // Encode the element section. - let mut elements = ElementSection::new(); - let func_elems = Elements::Functions(&[0, 1]); - let offset = ConstExpr::i32_const(0); - elements.active(None, &offset, func_elems); - import_mod.section(&elements); - } + let func = module + .func(&export.name, params.clone(), results.clone(), []) + .export(&export.name); + let builder = func.builder(); + builder.push(Instr::Call(__arg_start.index())); + for (parami, param) in params.into_iter().enumerate() { + builder.push(Instr::LocalGet(parami as u32)); + + match param { + ValType::I32 => { + builder.push(Instr::Call(__arg_i32.index())); + } + ValType::I64 => { + builder.push(Instr::Call(__arg_i64.index())); + } + ValType::F32 => { + builder.push(Instr::Call(__arg_f32.index())); + } + ValType::F64 => { + builder.push(Instr::Call(__arg_f64.index())); + } + r => { + anyhow::bail!("Unsupported param type: {:?}", r); + } + } + } - // Encode the code section. - let mut codes = CodeSection::new(); - let locals = vec![]; - let mut f = Function::new(locals); - if imports.functions.is_empty() { - // make it a no-op - f.instruction(&Instruction::LocalGet(1)); - f.instruction(&Instruction::LocalGet(0)); - f.instruction(&Instruction::Drop); - f.instruction(&Instruction::Drop); - f.instruction(&Instruction::I64Const(-1)); - f.instruction(&Instruction::End); - } else { - // we will essentially call the eval function - // in the core module here, similar to https://github.com/extism/js-pdk/blob/eaf17366624d48219cbd97a51e85569cffd12086/crates/cli/src/main.rs#L118 - f.instruction(&Instruction::LocalGet(1)); - f.instruction(&Instruction::LocalGet(0)); - f.instruction(&Instruction::CallIndirect { ty: 0, table: 0 }); - f.instruction(&Instruction::End); + builder.push(Instr::I32Const(idx as i32)); + match results.first() { + None => { + builder.push(Instr::Call(__invoke.index())); + } + Some(ValType::I32) => { + builder.push(Instr::Call(__invoke_i32.index())); + } + Some(ValType::I64) => { + builder.push(Instr::Call(__invoke_i64.index())); + } + Some(ValType::F32) => { + builder.push(Instr::Call(__invoke_f32.index())); + } + Some(ValType::F64) => { + builder.push(Instr::Call(__invoke_f64.index())); + } + Some(r) => { + anyhow::bail!("Unsupported result type: {:?}", r); + } + } } - codes.function(&f); - import_mod.section(&codes); - - let wasm_bytes = import_mod.finish(); - let mut file = File::create(import_path)?; - file.write_all(wasm_bytes.as_ref())?; + module.validate_save(path.as_ref())?; Ok(()) } diff --git a/crates/cli/src/ts_parser.rs b/crates/cli/src/ts_parser.rs index a852c01..25789f7 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -2,6 +2,7 @@ extern crate swc_common; extern crate swc_ecma_parser; use anyhow::{bail, Context, Result}; use std::path::PathBuf; +use wasm_encoder::ValType; use swc_common::sync::Lrc; use swc_common::SourceMap; @@ -12,14 +13,14 @@ use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; #[derive(Debug, Clone)] pub struct Param { pub name: String, - pub ptype: String, + pub ptype: wasm_encoder::ValType, } impl Param { - pub fn new(name: &str, ptype: &str) -> Param { + pub fn new(name: &str, ptype: wasm_encoder::ValType) -> Param { Param { name: name.to_string(), - ptype: ptype.to_string().to_uppercase(), + ptype, } } } @@ -40,72 +41,72 @@ pub struct Interface { #[derive(Debug, Clone)] pub struct PluginInterface { pub exports: Interface, - pub imports: Interface, + pub imports: Vec, } -/// Parses the "user" part of the module which maps to the wasm imports -fn parse_user_interface(i: &Box) -> Result> { +pub fn val_type(s: &str) -> ValType { + match s.to_ascii_lowercase().as_str() { + "i32" => ValType::I32, + "i64" => ValType::I64, + "f32" => ValType::F32, + "f64" => ValType::F64, + _ => ValType::I64, // Extism handle + } +} + +/// Parses the non-main parts of the module which maps to the wasm imports +fn parse_user_interface(i: &Box) -> Result { let mut signatures = Vec::new(); let name = i.id.sym.as_str(); - match name { - "user" => { - for sig in &i.body.body { - match sig { - TsTypeElement::TsMethodSignature(t) => { - let name = t.key.as_ident().unwrap().sym.to_string(); - let params = t - .params - .iter() - .map(|p| { - let vn = p.as_ident().unwrap().id.sym.as_str(); - let typ = p.as_ident().unwrap().type_ann.clone(); - let typ = typ.unwrap(); - let typ = &typ - .type_ann - .as_ts_type_ref() - .unwrap() - .type_name - .as_ident() - .unwrap() - .sym; - let type_name = typ.as_str(); - if type_name != "I64" { - panic!("Invalid type in function `{}`, using `{}`. Interface `user` must only declare functions with type `I64`", vn, type_name); - } - - Param::new(vn, typ) - }) - .collect::>(); - let return_type = &t.type_ann.clone().context("Missing return type")?; - let return_type = &return_type + for sig in &i.body.body { + match sig { + TsTypeElement::TsMethodSignature(t) => { + let name = t.key.as_ident().unwrap().sym.to_string(); + let params = t + .params + .iter() + .map(|p| { + let vn = p.as_ident().unwrap().id.sym.as_str(); + let typ = p.as_ident().unwrap().type_ann.clone(); + let typ = typ.unwrap(); + let typ = &typ .type_ann .as_ts_type_ref() - .context("Illegal return type")? + .unwrap() .type_name .as_ident() - .context("Illegal return type")? + .unwrap() .sym; - let results = vec![Param::new("return", return_type)]; - let signature = Signature { - name, - params, - results, - }; - signatures.push(signature); - } - _ => { - log::warn!("Warning: don't know what to do with sig {:#?}", sig); - } - } + Param::new(vn, val_type(typ)) + }) + .collect::>(); + let return_type = &t.type_ann.clone().context("Missing return type")?; + let return_type = &return_type + .type_ann + .as_ts_type_ref() + .context("Illegal return type")? + .type_name + .as_ident() + .context("Illegal return type")? + .sym; + let results = vec![Param::new("return", val_type(return_type))]; + let signature = Signature { + name, + params, + results, + }; + signatures.push(signature); + } + _ => { + log::warn!("Warning: don't know what to do with sig {:#?}", sig); } - - Ok(Some(Interface { - name: name.into(), - functions: signatures, - })) } - _ => Ok(None), } + + Ok(Interface { + name: name.into(), + functions: signatures, + }) } /// Try to parse the imports @@ -116,7 +117,7 @@ fn parse_imports(tsmod: &Box) -> Result> { if let ModuleItem::Stmt(Stmt::Decl(decl)) = inter { let i = decl.as_ts_interface().unwrap(); let interface = parse_user_interface(i)?; - return Ok(interface); + return Ok(Some(interface)); } else { log::warn!("Not a module decl"); } @@ -153,7 +154,7 @@ fn parse_module_decl(tsmod: &Box) -> Result { .as_ident() .context("Illegal return type")? .sym; - let results = vec![Param::new("result", return_type)]; + let results = vec![Param::new("result", val_type(return_type))]; let signature = Signature { name, params, @@ -187,17 +188,14 @@ fn parse_module(module: Module) -> Result> { }; match name { - Some("extism:host") => { + Some("main") | None => { + interfaces.push(parse_module_decl(submod)?); + } + Some(_) => { if let Some(imports) = parse_imports(submod)? { interfaces.push(imports); } } - Some("main") => { - interfaces.push(parse_module_decl(submod)?); - } - _ => { - log::warn!("Could not parse module with name {:#?}", name); - } }; } } @@ -205,76 +203,6 @@ fn parse_module(module: Module) -> Result> { Ok(interfaces) } -fn validate_interface(plugin_interface: &PluginInterface) -> Result<()> { - let mut has_err = false; - let mut log_err = |msg: String| { - log::error!("{}", msg); - has_err = true; - }; - - for e in &plugin_interface.exports.functions { - if !e.params.is_empty() { - log_err(format!("The export {} should take no params", e.name)); - } - if e.results.len() != 1 { - log_err(format!("The export {} should return a single I32", e.name)); - } else { - let return_type = &e.results.get(0).unwrap().ptype; - if return_type != "I32" { - log_err(format!( - "The export {} should return an I32 not {}", - e.name, return_type - )); - } - } - } - - for i in &plugin_interface.imports.functions { - if i.results.is_empty() { - log_err(format!("Import function {} needs to return an I64", i.name)); - } else if i.results.len() > 1 { - log_err(format!( - "Import function {} has too many returns. We only support 1 at the moment", - i.name - )); - } else { - let result = i.results.get(0).unwrap(); - if result.ptype != "I64" { - log_err(format!( - "Import function {} needs to return an I64 but instead returns {}", - i.name, result.ptype - )); - } - } - - if i.params.is_empty() { - log_err(format!( - "Import function {} needs to accept a single I64 pointer as param", - i.name - )); - } else if i.params.len() > 1 { - log_err(format!( - "Import function {} has too many params. We only support 1 at the moment", - i.name - )); - } else { - let param = i.params.get(0).unwrap(); - if param.ptype != "I64" { - log_err(format!( - "Import function {} needs to accept a single I64 but instead takes {}", - i.name, param.ptype - )); - } - } - } - - if has_err { - bail!("Failed to validate plugin interface file"); - } - - Ok(()) -} - /// Parse the d.ts file representing the plugin interface pub fn parse_interface_file(interface_path: &PathBuf) -> Result { let cm: Lrc = Default::default(); @@ -302,16 +230,11 @@ pub fn parse_interface_file(interface_path: &PathBuf) -> Result .find(|i| i.name == "main") .context("You need to declare a 'main' module")? .to_owned(); + let imports = interfaces - .iter() - .find(|i| i.name == "user") - .map(|i| i.to_owned()) - .unwrap_or(Interface { - name: "user".into(), - functions: vec![], - }); + .into_iter() + .filter(|i| i.name != "main") + .collect(); - let plugin_interface = PluginInterface { exports, imports }; - validate_interface(&plugin_interface)?; - Ok(plugin_interface) + Ok(PluginInterface { exports, imports }) } diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index 3277093..a9ad93a 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -5,16 +5,10 @@ use extism_pdk::extism::load_input; use extism_pdk::*; use quickjs_wasm_rs::{JSContextRef, JSError, JSValue, JSValueRef}; -#[link(wasm_import_module = "codemod")] -extern "C" { - // this import will get satisified by the import shim - fn __invokeHostFunc(func_idx: u32, ptr: u64) -> u64; -} - static PRELUDE: &[u8] = include_bytes!("prelude/dist/index.js"); pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { - let module = build_module_ojbect(&context)?; + let module = build_module_object(&context)?; let console = build_console_object(&context)?; let host = build_host_object(&context)?; let var = build_var_object(&context)?; @@ -45,6 +39,7 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { Ok(()) } + fn build_console_object(context: &JSContextRef) -> anyhow::Result { let console_log_callback = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { @@ -79,7 +74,7 @@ fn build_console_object(context: &JSContextRef) -> anyhow::Result { Ok(console_object) } -fn build_module_ojbect(context: &JSContextRef) -> anyhow::Result { +fn build_module_object(context: &JSContextRef) -> anyhow::Result { let exports = context.object_value()?; let module_obj = context.object_value()?; module_obj.set_property("exports", exports)?; @@ -118,7 +113,8 @@ fn build_host_object(context: &JSContextRef) -> anyhow::Result { |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { let func_id = args.get(0).unwrap().as_i32_unchecked(); let ptr = args.get(1).unwrap().as_u32_unchecked(); - let result = unsafe { __invokeHostFunc(func_id as u32, ptr as u64) }; + crate::__arg_i64(ptr as i64); + let result = crate::__invoke_i64(func_id); //unsafe { __invokeHostFunc(func_id as u32, ptr as u64) }; Ok(JSValue::Int(result as i32)) }, )?; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index afe01e2..8b00aa3 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,15 +1,15 @@ -use extism_pdk::*; use once_cell::sync::OnceCell; -use quickjs_wasm_rs::JSContextRef; +use quickjs_wasm_rs::{JSContextRef, JSValue, JSValueRef}; use std::io; use std::io::Read; mod globals; static mut CONTEXT: OnceCell = OnceCell::new(); +static mut CALL_ARGS: Vec> = vec![]; #[export_name = "wizer.initialize"] -pub extern "C" fn init() { +extern "C" fn init() { let context = JSContextRef::default(); globals::inject_globals(&context).expect("Failed to initialize globals"); @@ -25,18 +25,122 @@ pub extern "C" fn init() { } } +fn js_context<'a>() -> &'a JSContextRef { + unsafe { + if CONTEXT.get().is_none() { + init() + } + + CONTEXT.get_unchecked() + } +} + +fn convert_js_value<'a>(context: &'a JSContextRef, v: &JSValue) -> JSValueRef<'a> { + match v { + JSValue::Undefined => context.undefined_value().unwrap(), + JSValue::Null => context.null_value().unwrap(), + JSValue::Bool(b) => context.value_from_bool(*b).unwrap(), + JSValue::Int(i) => context.value_from_i32(*i).unwrap(), + JSValue::Float(f) => context.value_from_f64(*f).unwrap(), + JSValue::String(s) => context.value_from_str(s.as_str()).unwrap(), + JSValue::Array(a) => { + let arr = context.array_value().unwrap(); + for x in a.iter() { + arr.append_property(convert_js_value(context, x)).unwrap(); + } + arr + } + JSValue::ArrayBuffer(buf) => context.array_buffer_value(buf.as_slice()).unwrap(), + JSValue::Object(x) => { + let obj = context.object_value().unwrap(); + for (k, v) in x.iter() { + obj.set_property(k.as_str(), convert_js_value(context, v)) + .unwrap(); + } + obj + } + } +} + +fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>(idx: i32, conv: F) -> T { + let call_args = unsafe { CALL_ARGS.pop() }; + let context = js_context(); + let args: Vec<_> = call_args + .unwrap() + .iter() + .map(|x| convert_js_value(context, x)) + .collect(); + let globals = context.global_object().unwrap(); + let names = export_names(&context).unwrap(); + let f = globals.get_property(names[idx as usize].as_str()).unwrap(); + let r = f.call(&context.undefined_value().unwrap(), &args).unwrap(); + conv(&context, r) +} + +#[no_mangle] +pub extern "C" fn __arg_start() { + unsafe { + CALL_ARGS.push(vec![]); + } +} + #[no_mangle] -pub unsafe extern "C" fn __invoke(func_idx: i32) -> i32 { - let context = unsafe { CONTEXT.get().unwrap() }; +pub extern "C" fn __arg_i32(arg: i32) { + unsafe { + CALL_ARGS.last_mut().unwrap().push(JSValue::Int(arg)); + } +} - let export_funcs = export_names(&context).expect("Could not parse exports"); - let func_name = export_funcs - .get(func_idx as usize) - .expect(format!("Could not find export func at index {func_idx}").as_str()); - let result = context.eval_global("script.js", format!("{}();", func_name).as_str()); +#[no_mangle] +pub extern "C" fn __arg_i64(arg: i64) { + unsafe { + CALL_ARGS + .last_mut() + .unwrap() + .push(JSValue::Float(arg as f64)); + } +} - unwrap!(result); - 0 +#[no_mangle] +pub extern "C" fn __arg_f32(arg: f32) { + unsafe { + CALL_ARGS + .last_mut() + .unwrap() + .push(JSValue::Float(arg as f64)); + } +} + +#[no_mangle] +pub extern "C" fn __arg_f64(arg: f64) { + unsafe { + CALL_ARGS.last_mut().unwrap().push(JSValue::Float(arg)); + } +} + +#[no_mangle] +pub extern "C" fn __invoke_i32(idx: i32) -> i32 { + invoke(idx, |_ctx, r| r.as_i32_unchecked()) +} + +#[no_mangle] +pub extern "C" fn __invoke_i64(idx: i32) -> i64 { + invoke(idx, |_ctx, r| r.as_f64_unchecked() as i64) +} + +#[no_mangle] +pub extern "C" fn __invoke_f64(idx: i32) -> f64 { + invoke(idx, |_ctx, r| r.as_f64_unchecked()) +} + +#[no_mangle] +pub extern "C" fn __invoke_f32(idx: i32) -> f32 { + invoke(idx, |_ctx, r| r.as_f64_unchecked() as f32) +} + +#[no_mangle] +pub extern "C" fn __invoke(idx: i32) { + invoke(idx, |_ctx, _r| ()) } fn export_names(context: &JSContextRef) -> anyhow::Result> { @@ -53,3 +157,18 @@ fn export_names(context: &JSContextRef) -> anyhow::Result> { keys.sort(); Ok(keys) } + +fn import_names(context: &JSContextRef) -> anyhow::Result> { + let global = context.global_object().unwrap(); + let module = global.get_property("module")?; + let exports = module.get_property("exports")?; + let mut properties = exports.properties()?; + let mut key = properties.next_key()?; + let mut keys: Vec = vec![]; + while key.is_some() { + keys.push(key.unwrap().as_str()?.to_string()); + key = properties.next_key()?; + } + keys.sort(); + Ok(keys) +} diff --git a/examples/host_funcs/host.py b/examples/host_funcs/host.py index 96a1e9c..6fe3497 100644 --- a/examples/host_funcs/host.py +++ b/examples/host_funcs/host.py @@ -2,7 +2,7 @@ import sys import json -set_log_file("stdout", level='info') +set_log_file("stdout", level='trace') @host_fn() def myHostFunction1(input: str) -> str: diff --git a/examples/host_funcs/script.d.ts b/examples/host_funcs/script.d.ts index cf1e9ee..5150e9c 100644 --- a/examples/host_funcs/script.d.ts +++ b/examples/host_funcs/script.d.ts @@ -2,7 +2,7 @@ declare module 'main' { export function greet(): I32; } -declare module 'extism:host' { +declare module 'extism:host/user' { interface user { myHostFunction1(ptr: I64): I64; myHostFunction2(ptr: I64): I64; From 16e500d74299827b0a72ad718f381126bc7fe6e4 Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 12 Feb 2024 19:23:53 -0800 Subject: [PATCH 03/20] cleanup: get single parameter/single output host function calls working again --- README.md | 2 +- crates/cli/Cargo.toml | 2 -- crates/cli/src/main.rs | 19 +++++++------ crates/cli/src/shims.rs | 49 +++++++++++++++++++++++++++++++-- crates/cli/src/ts_parser.rs | 13 ++++++--- crates/core/src/globals.rs | 13 ++++++--- crates/core/src/lib.rs | 31 +++++++++++---------- examples/host_funcs/script.d.ts | 2 +- 8 files changed, 93 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d74e6ce..caf89f7 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ declare module 'main' { export function greet(): I32; } -declare module 'extism:host/user' { +declare module 'extism:host' { interface user { myHostFunction1(ptr: I64): I64; myHostFunction2(ptr: I64): I64; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a805a2d..ef783b6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,8 +19,6 @@ swc_atoms = "0.6.5" swc_common = "0.33.10" swc_ecma_ast = "0.112" swc_ecma_parser = "0.143" -wasm-encoder = "0.41.2" -wasmparser = "0.121.2" wagen = {git = "https://github.com/dylibso/wagen"} log = "0.4.20" tempfile = "3.8.1" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c4b2064..d22cc00 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -55,16 +55,19 @@ fn main() -> Result<()> { // If we have imports, we need to inject some state needed for host function support let mut contents = Vec::new(); + let mut names = Vec::new(); for ns in &plugin_interface.imports { - let names = &ns - .functions - .iter() - .map(|s| format!("'{}'", &s.name)) - .collect::>() - .join(","); - contents - .extend_from_slice(format!("Host.__hostFunctions = [{}].sort();\n", names).as_bytes()); + names.extend( + ns.functions + .iter() + .map(|s| format!("'{}'", &s.name)) + .collect::>(), + ); } + names.sort(); + contents.extend_from_slice( + format!("Host.__hostFunctions = [{}].sort();\n", names.join(", ")).as_bytes(), + ); contents.append(&mut user_code); // Create a tmp dir to hold all the library objects diff --git a/crates/cli/src/shims.rs b/crates/cli/src/shims.rs index 979743a..9617185 100644 --- a/crates/cli/src/shims.rs +++ b/crates/cli/src/shims.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::{collections::HashMap, path::Path}; +use std::path::Path; use crate::ts_parser::Interface; use wagen::{Instr, ValType}; @@ -12,7 +12,6 @@ pub fn generate_wasm_shims( ) -> Result<()> { let mut module = wagen::Module::new(); - let mut functions = HashMap::new(); let __arg_start = module.import("core", "__arg_start", None, [], []); let __arg_i32 = module.import("core", "__arg_i32", None, [ValType::I32], []); let __arg_i64 = module.import("core", "__arg_i64", None, [ValType::I64], []); @@ -24,14 +23,58 @@ pub fn generate_wasm_shims( let __invoke_f64 = module.import("core", "__invoke_f64", None, [ValType::I32], [ValType::F64]); let __invoke = module.import("core", "__invoke", None, [ValType::I32], []); + let mut n_imports = 0; + for import in imports.iter() { + for _ in import.functions.iter() { + n_imports += 1; + } + } + + let import_table = module.tables().push(wagen::TableType { + element_type: wagen::RefType::FUNCREF, + minimum: n_imports, + maximum: None, + }); + + let mut import_elements = Vec::new(); + let mut import_items = vec![]; for import in imports.iter() { for f in import.functions.iter() { let params: Vec<_> = f.params.iter().map(|x| x.ptype).collect(); let results: Vec<_> = f.results.iter().map(|x| x.ptype).collect(); let index = module.import(&import.name, &f.name, None, params, results); - functions.insert(f.name.as_str(), index.index()); + import_items.push((f.name.clone(), index)); } } + import_items.sort_by_key(|x| x.0.to_string()); + + for (_f, index) in import_items { + import_elements.push(index.index()); + } + + let indirect_type = module + .types() + .push(|t| t.function([ValType::I64], [ValType::I64])); + + let invoke_host = module + .func( + "__invokeHostFunc", + [ValType::I32, ValType::I64], + [ValType::I64], + [], + ) + .export("__invokeHostFunc"); + let builder = invoke_host.builder(); + builder.push(Instr::LocalGet(1)); + builder.push(Instr::LocalGet(0)); + builder.push(Instr::CallIndirect { + ty: indirect_type, + table: import_table, + }); + module.active_element( + Some(import_table), + wagen::Elements::Functions(&import_elements), + ); for (idx, export) in exports.functions.iter().enumerate() { let params: Vec<_> = export.params.iter().map(|x| x.ptype).collect(); diff --git a/crates/cli/src/ts_parser.rs b/crates/cli/src/ts_parser.rs index 25789f7..40452ba 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -2,7 +2,7 @@ extern crate swc_common; extern crate swc_ecma_parser; use anyhow::{bail, Context, Result}; use std::path::PathBuf; -use wasm_encoder::ValType; +use wagen::ValType; use swc_common::sync::Lrc; use swc_common::SourceMap; @@ -13,11 +13,11 @@ use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; #[derive(Debug, Clone)] pub struct Param { pub name: String, - pub ptype: wasm_encoder::ValType, + pub ptype: ValType, } impl Param { - pub fn new(name: &str, ptype: wasm_encoder::ValType) -> Param { + pub fn new(name: &str, ptype: ValType) -> Param { Param { name: name.to_string(), ptype, @@ -116,7 +116,12 @@ fn parse_imports(tsmod: &Box) -> Result> { for inter in block.body { if let ModuleItem::Stmt(Stmt::Decl(decl)) = inter { let i = decl.as_ts_interface().unwrap(); - let interface = parse_user_interface(i)?; + let mut interface = parse_user_interface(i)?; + if tsmod.id.clone().str().is_some() { + interface.name = tsmod.id.clone().expect_str().value.as_str().to_string() + + "/" + + i.id.sym.as_str(); + } return Ok(Some(interface)); } else { log::warn!("Not a module decl"); diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index a9ad93a..c1ded23 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -40,6 +40,12 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { Ok(()) } +#[link(wasm_import_module = "shim")] +extern "C" { + // this import will get satisified by the import shim + fn __invokeHostFunc(func_idx: u32, ptr: u64) -> u64; +} + fn build_console_object(context: &JSContextRef) -> anyhow::Result { let console_log_callback = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { @@ -111,11 +117,10 @@ fn build_host_object(context: &JSContextRef) -> anyhow::Result { )?; let host_invoke_func = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let func_id = args.get(0).unwrap().as_i32_unchecked(); + let func_id = args.get(0).unwrap().as_u32_unchecked(); let ptr = args.get(1).unwrap().as_u32_unchecked(); - crate::__arg_i64(ptr as i64); - let result = crate::__invoke_i64(func_id); //unsafe { __invokeHostFunc(func_id as u32, ptr as u64) }; - Ok(JSValue::Int(result as i32)) + let result = unsafe { __invokeHostFunc(func_id as u32, ptr as u64) }; + Ok(JSValue::Float(result as f64)) }, )?; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 8b00aa3..2e42ad8 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -77,6 +77,22 @@ fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>(idx: i32, conv: F conv(&context, r) } +// #[no_mangle] +// pub fn __invokeHostFunc(idx: i32, i: u64) -> u64 { +// let call_args = unsafe { CALL_ARGS.pop() }; +// let context = js_context(); +// let args: Vec<_> = call_args +// .unwrap() +// .iter() +// .map(|x| convert_js_value(context, x)) +// .collect(); +// let globals = context.global_object().unwrap(); +// let names = export_names(&context).unwrap(); +// let f = globals.get_property(names[idx as usize].as_str()).unwrap(); + +// let r = f.call(&context.undefined_value().unwrap(), &args).unwrap(); +// } + #[no_mangle] pub extern "C" fn __arg_start() { unsafe { @@ -157,18 +173,3 @@ fn export_names(context: &JSContextRef) -> anyhow::Result> { keys.sort(); Ok(keys) } - -fn import_names(context: &JSContextRef) -> anyhow::Result> { - let global = context.global_object().unwrap(); - let module = global.get_property("module")?; - let exports = module.get_property("exports")?; - let mut properties = exports.properties()?; - let mut key = properties.next_key()?; - let mut keys: Vec = vec![]; - while key.is_some() { - keys.push(key.unwrap().as_str()?.to_string()); - key = properties.next_key()?; - } - keys.sort(); - Ok(keys) -} diff --git a/examples/host_funcs/script.d.ts b/examples/host_funcs/script.d.ts index 5150e9c..cf1e9ee 100644 --- a/examples/host_funcs/script.d.ts +++ b/examples/host_funcs/script.d.ts @@ -2,7 +2,7 @@ declare module 'main' { export function greet(): I32; } -declare module 'extism:host/user' { +declare module 'extism:host' { interface user { myHostFunction1(ptr: I64): I64; myHostFunction2(ptr: I64): I64; From 1068f065ec34c5b6e714b4e29a1488a7c6f8ac8c Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 09:17:18 -0800 Subject: [PATCH 04/20] checkpoint: generate __invokeHostFunc functions --- crates/cli/src/shims.rs | 43 +++++++++++++++++++++----------------- crates/core/src/globals.rs | 22 +++++++++++++++---- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/cli/src/shims.rs b/crates/cli/src/shims.rs index 9617185..2d35dcd 100644 --- a/crates/cli/src/shims.rs +++ b/crates/cli/src/shims.rs @@ -52,25 +52,30 @@ pub fn generate_wasm_shims( import_elements.push(index.index()); } - let indirect_type = module - .types() - .push(|t| t.function([ValType::I64], [ValType::I64])); - - let invoke_host = module - .func( - "__invokeHostFunc", - [ValType::I32, ValType::I64], - [ValType::I64], - [], - ) - .export("__invokeHostFunc"); - let builder = invoke_host.builder(); - builder.push(Instr::LocalGet(1)); - builder.push(Instr::LocalGet(0)); - builder.push(Instr::CallIndirect { - ty: indirect_type, - table: import_table, - }); + for p in 0..=10 { + for q in 0..=1 { + let indirect_type = module + .types() + .push(|t| t.function(vec![ValType::I64; p], vec![ValType::I64; q])); + let name = format!("__invokeHostFunc_{p}_{q}"); + let mut params = vec![ValType::I32]; + for _ in 0..p { + params.push(ValType::I64); + } + let invoke_host = module + .func(&name, params, vec![ValType::I64; q], []) + .export(&name); + let builder = invoke_host.builder(); + for i in 1..=p { + builder.push(Instr::LocalGet(i as u32)); + } + builder.push(Instr::LocalGet(0)); + builder.push(Instr::CallIndirect { + ty: indirect_type, + table: import_table, + }); + } + } module.active_element( Some(import_table), wagen::Elements::Functions(&import_elements), diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index c1ded23..10fb7ab 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -43,7 +43,8 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { #[link(wasm_import_module = "shim")] extern "C" { // this import will get satisified by the import shim - fn __invokeHostFunc(func_idx: u32, ptr: u64) -> u64; + fn __invokeHostFunc_1_1(func_idx: u32, ptr: u64) -> u64; + fn __invokeHostFunc_2_1(func_idx: u32, ptr: u64, ptr2: u64) -> u64; } fn build_console_object(context: &JSContextRef) -> anyhow::Result { @@ -118,9 +119,22 @@ fn build_host_object(context: &JSContextRef) -> anyhow::Result { let host_invoke_func = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { let func_id = args.get(0).unwrap().as_u32_unchecked(); - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let result = unsafe { __invokeHostFunc(func_id as u32, ptr as u64) }; - Ok(JSValue::Float(result as f64)) + let len = args.len() - 1; + match len { + 1 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let result = unsafe { __invokeHostFunc_1_1(func_id as u32, ptr as u64) }; + Ok(JSValue::Float(result as f64)) + } + 2 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let result = + unsafe { __invokeHostFunc_2_1(func_id as u32, ptr as u64, ptr2 as u64) }; + Ok(JSValue::Float(result as f64)) + } + n => anyhow::bail!("__invokeHostFunc with {n} parameters is not implemented"), + } }, )?; From 1e1f9d92a9db71125baf8703295bad9ffc500be5 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 12:06:51 -0800 Subject: [PATCH 05/20] feat: allow calls to more types of host functions --- crates/cli/src/main.rs | 21 +++--- crates/core/build.rs | 3 + crates/core/src/globals.rs | 97 +++++++++++++++++++++------- crates/core/src/lib.rs | 3 +- crates/core/src/prelude/src/index.js | 15 +++-- 5 files changed, 97 insertions(+), 42 deletions(-) create mode 100644 crates/core/build.rs diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d22cc00..938155c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -56,18 +56,18 @@ fn main() -> Result<()> { // If we have imports, we need to inject some state needed for host function support let mut contents = Vec::new(); let mut names = Vec::new(); + let mut sorted_names = Vec::new(); for ns in &plugin_interface.imports { - names.extend( - ns.functions - .iter() - .map(|s| format!("'{}'", &s.name)) - .collect::>(), - ); + sorted_names.extend(ns.functions.iter().map(|s| (&s.name, s.results.len()))); + } + sorted_names.sort_by_key(|x| x.0.as_str()); + + for (name, results) in sorted_names { + names.push(format!("{{ name: '{}', results: {} }}", &name, results)); } - names.sort(); - contents.extend_from_slice( - format!("Host.__hostFunctions = [{}].sort();\n", names.join(", ")).as_bytes(), - ); + + contents + .extend_from_slice(format!("Host.__hostFunctions = [{}];\n", names.join(", ")).as_bytes()); contents.append(&mut user_code); // Create a tmp dir to hold all the library objects @@ -76,6 +76,7 @@ fn main() -> Result<()> { let core_path = tmp_dir.path().join("core.wasm"); let shim_path = tmp_dir.path().join("shim.wasm"); + // std::fs::copy(concat!(env!("OUT_DIR"), "/engine.wasm"), &core_path)?; // First wizen the core module let self_cmd = env::args().next().expect("Expected a command argument"); { diff --git a/crates/core/build.rs b/crates/core/build.rs new file mode 100644 index 0000000..5e1d90e --- /dev/null +++ b/crates/core/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=src/prelude/dist/index.js"); +} diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index 10fb7ab..b7f5446 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -10,13 +10,13 @@ static PRELUDE: &[u8] = include_bytes!("prelude/dist/index.js"); pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { let module = build_module_object(&context)?; let console = build_console_object(&context)?; - let host = build_host_object(&context)?; let var = build_var_object(&context)?; let http = build_http_object(&context)?; let cfg = build_config_object(&context)?; let decoder = build_decoder(&context)?; let encoder = build_encoder(&context)?; let mem = build_memory(&context)?; + let host = build_host_object(&context)?; let global = context.global_object()?; global.set_property("console", console)?; @@ -29,6 +29,8 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { global.set_property("__decodeUtf8BufferToString", decoder)?; global.set_property("__encodeStringToUtf8Buffer", encoder)?; + inject_host_functions(context)?; + context.eval_global( "script.js", "globalThis.module = {}; globalThis.module.exports = {}", @@ -43,6 +45,10 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { #[link(wasm_import_module = "shim")] extern "C" { // this import will get satisified by the import shim + fn __invokeHostFunc_0_0(func_idx: u32); + fn __invokeHostFunc_1_0(func_idx: u32, ptr: u64); + fn __invokeHostFunc_0_1(func_idx: u32) -> u64; + fn __invokeHostFunc_2_0(func_idx: u32, ptr: u64, ptr2: u64); fn __invokeHostFunc_1_1(func_idx: u32, ptr: u64) -> u64; fn __invokeHostFunc_2_1(func_idx: u32, ptr: u64, ptr2: u64) -> u64; } @@ -116,38 +122,79 @@ fn build_host_object(context: &JSContextRef) -> anyhow::Result { Ok(JSValue::Bool(true)) }, )?; - let host_invoke_func = context.wrap_callback( - |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let func_id = args.get(0).unwrap().as_u32_unchecked(); - let len = args.len() - 1; - match len { - 1 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let result = unsafe { __invokeHostFunc_1_1(func_id as u32, ptr as u64) }; - Ok(JSValue::Float(result as f64)) - } - 2 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let result = - unsafe { __invokeHostFunc_2_1(func_id as u32, ptr as u64, ptr2 as u64) }; - Ok(JSValue::Float(result as f64)) - } - n => anyhow::bail!("__invokeHostFunc with {n} parameters is not implemented"), - } - }, - )?; - let host_object = context.object_value()?; host_object.set_property("inputBytes", host_input_bytes)?; host_object.set_property("inputString", host_input_string)?; host_object.set_property("outputBytes", host_output_bytes)?; host_object.set_property("outputString", host_output_string)?; - host_object.set_property("invokeFunc", host_invoke_func)?; - Ok(host_object) } +pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { + let global = context.global_object()?; + if global + .get_property("Host")? + .get_property("invokeHost")? + .is_null_or_undefined() + { + let host_invoke_func = context.wrap_callback( + |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { + let func_id = args.get(0).unwrap().as_u32_unchecked(); + let len = args.len() - 1; + match len { + 0 => { + let result = unsafe { __invokeHostFunc_0_1(func_id as u32) }; + Ok(JSValue::Float(result as f64)) + } + 1 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let result = unsafe { __invokeHostFunc_1_1(func_id as u32, ptr as u64) }; + Ok(JSValue::Float(result as f64)) + } + 2 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let result = unsafe { + __invokeHostFunc_2_1(func_id as u32, ptr as u64, ptr2 as u64) + }; + Ok(JSValue::Float(result as f64)) + } + n => anyhow::bail!("__invokeHostFunc with {n} parameters is not implemented"), + } + }, + )?; + let host_invoke_func0 = context.wrap_callback( + |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { + let func_id = args.get(0).unwrap().as_u32_unchecked(); + let len = args.len() - 1; + match len { + 0 => { + unsafe { __invokeHostFunc_0_0(func_id as u32) }; + } + 1 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + unsafe { __invokeHostFunc_1_0(func_id as u32, ptr as u64) }; + } + 2 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + unsafe { __invokeHostFunc_2_0(func_id as u32, ptr as u64, ptr2 as u64) }; + } + n => anyhow::bail!("__invokeHostFunc0 with {n} parameters is not implemented"), + } + + Ok(JSValue::Undefined) + }, + )?; + + let host_object = context.global_object()?.get_property("Host")?; + host_object.set_property("invokeFunc", host_invoke_func)?; + host_object.set_property("invokeFunc0", host_invoke_func0)?; + } + + Ok(()) +} + fn build_var_object(context: &JSContextRef) -> anyhow::Result { let var_set = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2e42ad8..505eae9 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -31,7 +31,8 @@ fn js_context<'a>() -> &'a JSContextRef { init() } - CONTEXT.get_unchecked() + let context = CONTEXT.get_unchecked(); + context } } diff --git a/crates/core/src/prelude/src/index.js b/crates/core/src/prelude/src/index.js index 0e6db1c..e9c7eca 100644 --- a/crates/core/src/prelude/src/index.js +++ b/crates/core/src/prelude/src/index.js @@ -115,14 +115,17 @@ Memory.find = (offset) => { Host.getFunctions = () => { const funcs = {} let funcIdx = 0 - const createInvoke = (funcIdx) => { - return (ptr) => { - console.log(`name and func ${funcIdx} ptr ${ptr}`) - return Host.invokeFunc(funcIdx, ptr) + const createInvoke = (funcIdx, results) => { + return (...args) => { + if (results == 0) { + return Host.invokeFunc0(funcIdx, ...args) + } else { + return Host.invokeFunc(funcIdx, ...args) + } } } - Host.__hostFunctions.forEach(name => { - funcs[name] = createInvoke(funcIdx++) + Host.__hostFunctions.forEach((x) => { + funcs[x.name] = createInvoke(funcIdx++, x.results) }) return funcs } From fec607bf6b630ce416571dd91e05486f85b37f55 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 13:38:43 -0800 Subject: [PATCH 06/20] refactor: more permissive typescript typechecking --- crates/cli/src/ts_parser.rs | 116 ++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/crates/cli/src/ts_parser.rs b/crates/cli/src/ts_parser.rs index 40452ba..f3aff3a 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -6,7 +6,9 @@ use wagen::ValType; use swc_common::sync::Lrc; use swc_common::SourceMap; -use swc_ecma_ast::{Decl, Module, ModuleDecl, Stmt, TsInterfaceDecl, TsModuleDecl}; +use swc_ecma_ast::{ + Decl, Module, ModuleDecl, Stmt, TsInterfaceDecl, TsKeywordTypeKind, TsModuleDecl, TsType, +}; use swc_ecma_ast::{ModuleItem, TsTypeElement}; use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; @@ -54,6 +56,46 @@ pub fn val_type(s: &str) -> ValType { } } +pub fn param_type(params: &mut Vec, vn: &str, t: &TsType) -> Result<()> { + let typ = if let Some(t) = t.as_ts_type_ref() { + t.type_name + .as_ident() + .context("Illegal param type")? + .sym + .as_str() + } else { + "i64" + }; + params.push(Param::new(vn, val_type(typ))); + Ok(()) +} + +pub fn result_type(results: &mut Vec, return_type: &TsType) -> Result<()> { + let return_type = if let Some(return_type) = return_type.as_ts_type_ref() { + Some( + return_type + .type_name + .as_ident() + .context("Illegal return type")? + .sym + .as_str(), + ) + } else if let Some(t) = return_type.as_ts_keyword_type() { + match t.kind { + TsKeywordTypeKind::TsVoidKeyword + | TsKeywordTypeKind::TsUndefinedKeyword + | TsKeywordTypeKind::TsNullKeyword => None, + _ => Some("i64"), + } + } else { + Some("i64") + }; + if let Some(r) = return_type { + results.push(Param::new("result", val_type(r))); + } + Ok(()) +} + /// Parses the non-main parts of the module which maps to the wasm imports fn parse_user_interface(i: &Box) -> Result { let mut signatures = Vec::new(); @@ -62,34 +104,18 @@ fn parse_user_interface(i: &Box) -> Result { match sig { TsTypeElement::TsMethodSignature(t) => { let name = t.key.as_ident().unwrap().sym.to_string(); - let params = t - .params - .iter() - .map(|p| { - let vn = p.as_ident().unwrap().id.sym.as_str(); - let typ = p.as_ident().unwrap().type_ann.clone(); - let typ = typ.unwrap(); - let typ = &typ - .type_ann - .as_ts_type_ref() - .unwrap() - .type_name - .as_ident() - .unwrap() - .sym; - Param::new(vn, val_type(typ)) - }) - .collect::>(); - let return_type = &t.type_ann.clone().context("Missing return type")?; - let return_type = &return_type - .type_ann - .as_ts_type_ref() - .context("Illegal return type")? - .type_name - .as_ident() - .context("Illegal return type")? - .sym; - let results = vec![Param::new("return", val_type(return_type))]; + let mut params = vec![]; + let mut results = vec![]; + + for p in t.params.iter() { + let vn = p.as_ident().unwrap().id.sym.as_str(); + let typ = p.as_ident().unwrap().type_ann.clone(); + let t = typ.unwrap().type_ann; + param_type(&mut params, &vn, &t)?; + } + if let Some(return_type) = &t.type_ann { + result_type(&mut results, &return_type.type_ann)?; + } let signature = Signature { name, params, @@ -144,22 +170,22 @@ fn parse_module_decl(tsmod: &Box) -> Result { if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) = decl { if let Some(fndecl) = e.decl.as_fn_decl() { let name = fndecl.ident.sym.as_str().to_string(); - let params = vec![]; // TODO ignoring params for now - let return_type = &fndecl - .function - .clone() - .return_type - .context("Missing return type")? - .clone(); - let return_type = &return_type - .type_ann - .as_ts_type_ref() - .context("Illegal return type")? - .type_name - .as_ident() - .context("Illegal return type")? - .sym; - let results = vec![Param::new("result", val_type(return_type))]; + let mut params = vec![]; + let mut results = vec![]; + if let Some(return_type) = fndecl.function.clone().return_type.clone() { + result_type(&mut results, &return_type.type_ann)?; + } + + for param in fndecl.function.params.iter() { + let name = param.pat.clone().expect_ident().id.sym.as_str().to_string(); + let p = param.pat.clone().expect_ident(); + match p.type_ann { + None => params.push(Param::new(&name, val_type("i64"))), + Some(ann) => { + param_type(&mut params, &name, &ann.type_ann)?; + } + } + } let signature = Signature { name, params, From ec5d7a6298ff77284bbbef7ae2476f797b6a4934 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 14:43:40 -0800 Subject: [PATCH 07/20] cleanup: use wasm-opt instead of binaryen crate --- crates/cli/Cargo.toml | 1 - crates/cli/src/main.rs | 44 ++++++++++++++++--------------------- crates/cli/src/opt.rs | 41 +++++++++++++++++----------------- crates/cli/src/options.rs | 3 +++ crates/cli/src/ts_parser.rs | 10 +++------ crates/core/src/lib.rs | 16 -------------- 6 files changed, 46 insertions(+), 69 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index ef783b6..1feac5c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -14,7 +14,6 @@ path = "src/main.rs" anyhow = { workspace = true } wizer = "4.0.0" structopt = "0.3" -binaryen = "0.12.0" swc_atoms = "0.6.5" swc_common = "0.33.10" swc_ecma_ast = "0.112" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 938155c..50bed8e 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -6,35 +6,29 @@ mod ts_parser; use crate::options::Options; use crate::ts_parser::parse_interface_file; use anyhow::{bail, Result}; -use env_logger::{Builder, Target}; use log::LevelFilter; use shims::generate_wasm_shims; use std::env; -use std::io::{Read, Write}; use std::path::PathBuf; use std::process::Stdio; -use std::{fs, process::Command}; +use std::{fs, io::Write, process::Command}; use structopt::StructOpt; use tempfile::TempDir; +const CORE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/engine.wasm")); + fn main() -> Result<()> { - let mut builder = Builder::new(); + let mut builder = env_logger::Builder::new(); builder .filter(None, LevelFilter::Info) - .target(Target::Stdout) + .target(env_logger::Target::Stdout) .init(); let opts = Options::from_args(); - let wizen = env::var("EXTISM_WIZEN"); - - if wizen.eq(&Ok("1".into())) { - let wasm: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/engine.wasm")); - opt::Optimizer::new(wasm) + if opts.core { + opt::Optimizer::new(CORE) .optimize(true) .write_optimized_wasm(opts.output)?; - - env::remove_var("EXTISM_WIZEN"); - return Ok(()); } @@ -48,10 +42,8 @@ fn main() -> Result<()> { } let plugin_interface = parse_interface_file(&interface_path)?; - // Copy in the user's js code from stdin - let mut input_file = fs::File::open(&opts.input_js)?; - let mut user_code: Vec = vec![]; - input_file.read_to_end(&mut user_code)?; + // Copy in the user's js code from the configured file + let mut user_code = fs::read(&opts.input_js)?; // If we have imports, we need to inject some state needed for host function support let mut contents = Vec::new(); @@ -76,12 +68,11 @@ fn main() -> Result<()> { let core_path = tmp_dir.path().join("core.wasm"); let shim_path = tmp_dir.path().join("shim.wasm"); - // std::fs::copy(concat!(env!("OUT_DIR"), "/engine.wasm"), &core_path)?; // First wizen the core module let self_cmd = env::args().next().expect("Expected a command argument"); { - env::set_var("EXTISM_WIZEN", "1"); let mut command = Command::new(self_cmd) + .arg("-c") .arg(&opts.input_js) .arg("-o") .arg(&core_path) @@ -105,13 +96,17 @@ fn main() -> Result<()> { &plugin_interface.imports, )?; - let output = Command::new("wasm-merge").arg("--version").output(); - if let Err(_) = output { - bail!("Failed to execute wasm-merge. Please install binaryen and make sure wasm-merge is on your path: https://github.com/WebAssembly/binaryen"); + let output = Command::new("wasm-merge") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + if output.is_err() { + bail!("Failed to detect wasm-merge. Please install binaryen and make sure wasm-merge is on your path: https://github.com/WebAssembly/binaryen"); } // Merge the shim with the core module - let mut command = Command::new("wasm-merge") + let status = Command::new("wasm-merge") .arg(&core_path) .arg("core") .arg(&shim_path) @@ -120,8 +115,7 @@ fn main() -> Result<()> { .arg(&opts.output) .arg("--enable-reference-types") .arg("--enable-bulk-memory") - .spawn()?; - let status = command.wait()?; + .status()?; if !status.success() { bail!("wasm-merge failed. Couldn't merge shim"); } diff --git a/crates/cli/src/opt.rs b/crates/cli/src/opt.rs index 82721d1..987d0d0 100644 --- a/crates/cli/src/opt.rs +++ b/crates/cli/src/opt.rs @@ -1,6 +1,8 @@ -use anyhow::{bail, Error, Result}; -use binaryen::{CodegenConfig, Module}; -use std::path::Path; +use anyhow::{Error, Result}; +use std::{ + path::Path, + process::{Command, Stdio}, +}; use wizer::Wizer; pub(crate) struct Optimizer<'a> { @@ -21,32 +23,31 @@ impl<'a> Optimizer<'a> { } pub fn write_optimized_wasm(self, dest: impl AsRef) -> Result<(), Error> { - let mut wasm = Wizer::new() + let wasm = Wizer::new() .allow_wasi(true)? .inherit_stdio(true) .wasm_bulk_memory(true) .run(self.wasm)?; - if self.optimize { - let codegen_cfg = CodegenConfig { - optimization_level: 3, // Aggressively optimize for speed. - shrink_level: 0, // Don't optimize for size at the expense of performance. - debug_info: false, - }; + std::fs::write(&dest, &wasm)?; - if let Ok(mut module) = Module::read(&wasm) { - module.optimize(&codegen_cfg); - module - .run_optimization_passes(vec!["strip"], &codegen_cfg) - .unwrap(); - wasm = module.write(); - } else { - bail!("Unable to read wasm binary for wasm-opt optimizations"); + if self.optimize { + let output = Command::new("wasm-opt") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + if output.is_err() { + anyhow::bail!("Failed to detect wasm-opt. Please install binaryen and make sure wasm-opt is on your path: https://github.com/WebAssembly/binaryen"); } + Command::new("wasm-opt") + .arg("-O3") + .arg(dest.as_ref()) + .arg("-o") + .arg(dest.as_ref()) + .status()?; } - std::fs::write(dest.as_ref(), wasm)?; - Ok(()) } } diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index 15b8532..0ef1f68 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -12,4 +12,7 @@ pub struct Options { #[structopt(short = "o", parse(from_os_str), default_value = "index.wasm")] pub output: PathBuf, + + #[structopt(short = "c")] + pub core: bool, } diff --git a/crates/cli/src/ts_parser.rs b/crates/cli/src/ts_parser.rs index f3aff3a..6003109 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -111,7 +111,7 @@ fn parse_user_interface(i: &Box) -> Result { let vn = p.as_ident().unwrap().id.sym.as_str(); let typ = p.as_ident().unwrap().type_ann.clone(); let t = typ.unwrap().type_ann; - param_type(&mut params, &vn, &t)?; + param_type(&mut params, vn, &t)?; } if let Some(return_type) = &t.type_ann { result_type(&mut results, &return_type.type_ann)?; @@ -212,11 +212,7 @@ fn parse_module(module: Module) -> Result> { let mut interfaces = Vec::new(); for statement in &module.body { if let ModuleItem::Stmt(Stmt::Decl(Decl::TsModule(submod))) = statement { - let name = if let Some(name) = submod.id.as_str() { - Some(name.value.as_str()) - } else { - None - }; + let name = submod.id.as_str().map(|name| name.value.as_str()); match name { Some("main") | None => { @@ -237,7 +233,7 @@ fn parse_module(module: Module) -> Result> { /// Parse the d.ts file representing the plugin interface pub fn parse_interface_file(interface_path: &PathBuf) -> Result { let cm: Lrc = Default::default(); - let fm = cm.load_file(&interface_path)?; + let fm = cm.load_file(interface_path)?; let lexer = Lexer::new( Syntax::Typescript(Default::default()), Default::default(), diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 505eae9..7d83ddf 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -78,22 +78,6 @@ fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>(idx: i32, conv: F conv(&context, r) } -// #[no_mangle] -// pub fn __invokeHostFunc(idx: i32, i: u64) -> u64 { -// let call_args = unsafe { CALL_ARGS.pop() }; -// let context = js_context(); -// let args: Vec<_> = call_args -// .unwrap() -// .iter() -// .map(|x| convert_js_value(context, x)) -// .collect(); -// let globals = context.global_object().unwrap(); -// let names = export_names(&context).unwrap(); -// let f = globals.get_property(names[idx as usize].as_str()).unwrap(); - -// let r = f.call(&context.undefined_value().unwrap(), &args).unwrap(); -// } - #[no_mangle] pub extern "C" fn __arg_start() { unsafe { From e369a252a263e070a20be2c07b4f3f58e58e7cfd Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 14:49:43 -0800 Subject: [PATCH 08/20] cleanup: clippy, add wasm-opt to install script/readme --- README.md | 4 +-- crates/cli/src/opt.rs | 2 +- crates/cli/src/ts_parser.rs | 13 ++++---- crates/core/src/globals.rs | 65 ++++++++++++++++++------------------- crates/core/src/lib.rs | 4 +-- install.sh | 1 + 6 files changed, 45 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index caf89f7..7a6503b 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh sh install.sh ``` -> *Note*: [Binaryen](https://github.com/WebAssembly/binaryen), specifcally the wasm-merge tool -> is required as a dependency. We will try to package this up eventually but for now it must be reachable +> *Note*: [Binaryen](https://github.com/WebAssembly/binaryen), specifcally the `wasm-merge` and `wasm-opt` tools +> are required as a dependency. We will try to package this up eventually but for now it must be reachable > on your machine. You can install on mac with `brew install binaryen` or see their [releases page](https://github.com/WebAssembly/binaryen/releases). Then run command with no args to see the help: diff --git a/crates/cli/src/opt.rs b/crates/cli/src/opt.rs index 987d0d0..a275568 100644 --- a/crates/cli/src/opt.rs +++ b/crates/cli/src/opt.rs @@ -29,7 +29,7 @@ impl<'a> Optimizer<'a> { .wasm_bulk_memory(true) .run(self.wasm)?; - std::fs::write(&dest, &wasm)?; + std::fs::write(&dest, wasm)?; if self.optimize { let output = Command::new("wasm-opt") diff --git a/crates/cli/src/ts_parser.rs b/crates/cli/src/ts_parser.rs index 6003109..5634394 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -1,7 +1,8 @@ extern crate swc_common; extern crate swc_ecma_parser; +use std::path::Path; + use anyhow::{bail, Context, Result}; -use std::path::PathBuf; use wagen::ValType; use swc_common::sync::Lrc; @@ -97,7 +98,7 @@ pub fn result_type(results: &mut Vec, return_type: &TsType) -> Result<()> } /// Parses the non-main parts of the module which maps to the wasm imports -fn parse_user_interface(i: &Box) -> Result { +fn parse_user_interface(i: &TsInterfaceDecl) -> Result { let mut signatures = Vec::new(); let name = i.id.sym.as_str(); for sig in &i.body.body { @@ -136,7 +137,7 @@ fn parse_user_interface(i: &Box) -> Result { } /// Try to parse the imports -fn parse_imports(tsmod: &Box) -> Result> { +fn parse_imports(tsmod: &TsModuleDecl) -> Result> { for block in &tsmod.body { if let Some(block) = block.clone().ts_module_block() { for inter in block.body { @@ -161,7 +162,7 @@ fn parse_imports(tsmod: &Box) -> Result> { } /// Parses the main module declaration (the extism exports) -fn parse_module_decl(tsmod: &Box) -> Result { +fn parse_module_decl(tsmod: &TsModuleDecl) -> Result { let mut signatures = Vec::new(); for block in &tsmod.body { @@ -231,9 +232,9 @@ fn parse_module(module: Module) -> Result> { } /// Parse the d.ts file representing the plugin interface -pub fn parse_interface_file(interface_path: &PathBuf) -> Result { +pub fn parse_interface_file(interface_path: impl AsRef) -> Result { let cm: Lrc = Default::default(); - let fm = cm.load_file(interface_path)?; + let fm = cm.load_file(interface_path.as_ref())?; let lexer = Lexer::new( Syntax::Typescript(Default::default()), Default::default(), diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index b7f5446..a6c4a65 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -8,15 +8,15 @@ use quickjs_wasm_rs::{JSContextRef, JSError, JSValue, JSValueRef}; static PRELUDE: &[u8] = include_bytes!("prelude/dist/index.js"); pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { - let module = build_module_object(&context)?; - let console = build_console_object(&context)?; - let var = build_var_object(&context)?; - let http = build_http_object(&context)?; - let cfg = build_config_object(&context)?; - let decoder = build_decoder(&context)?; - let encoder = build_encoder(&context)?; - let mem = build_memory(&context)?; - let host = build_host_object(&context)?; + let module = build_module_object(context)?; + let console = build_console_object(context)?; + let var = build_var_object(context)?; + let http = build_http_object(context)?; + let cfg = build_config_object(context)?; + let decoder = build_decoder(context)?; + let encoder = build_encoder(context)?; + let mem = build_memory(context)?; + let host = build_host_object(context)?; let global = context.global_object()?; global.set_property("console", console)?; @@ -56,7 +56,7 @@ extern "C" { fn build_console_object(context: &JSContextRef) -> anyhow::Result { let console_log_callback = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let stmt = args.get(0).ok_or(anyhow!("Need at least one arg"))?; + let stmt = args.first().ok_or(anyhow!("Need at least one arg"))?; let stmt = stmt.as_str()?; info!("{}", stmt); Ok(JSValue::Undefined) @@ -64,7 +64,7 @@ fn build_console_object(context: &JSContextRef) -> anyhow::Result { )?; let console_error_callback = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let stmt = args.get(0).ok_or(anyhow!("Need at least one arg"))?; + let stmt = args.first().ok_or(anyhow!("Need at least one arg"))?; let stmt = stmt.as_str()?; error!("{}", stmt); Ok(JSValue::Undefined) @@ -72,7 +72,7 @@ fn build_console_object(context: &JSContextRef) -> anyhow::Result { )?; let console_warn_callback = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let stmt = args.get(0).ok_or(anyhow!("Need at least one arg"))?; + let stmt = args.first().ok_or(anyhow!("Need at least one arg"))?; let stmt = stmt.as_str()?; warn!("{}", stmt); Ok(JSValue::Undefined) @@ -110,14 +110,14 @@ fn build_host_object(context: &JSContextRef) -> anyhow::Result { )?; let host_output_bytes = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let output = args.get(0).unwrap(); + let output = args.first().unwrap(); extism_pdk::output(output.as_bytes()?)?; Ok(JSValue::Bool(true)) }, )?; let host_output_string = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let output = args.get(0).unwrap(); + let output = args.first().unwrap(); extism_pdk::output(output.as_str()?)?; Ok(JSValue::Bool(true)) }, @@ -139,23 +139,23 @@ pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { { let host_invoke_func = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let func_id = args.get(0).unwrap().as_u32_unchecked(); + let func_id = args.first().unwrap().as_u32_unchecked(); let len = args.len() - 1; match len { 0 => { - let result = unsafe { __invokeHostFunc_0_1(func_id as u32) }; + let result = unsafe { __invokeHostFunc_0_1(func_id) }; Ok(JSValue::Float(result as f64)) } 1 => { let ptr = args.get(1).unwrap().as_u32_unchecked(); - let result = unsafe { __invokeHostFunc_1_1(func_id as u32, ptr as u64) }; + let result = unsafe { __invokeHostFunc_1_1(func_id, ptr as u64) }; Ok(JSValue::Float(result as f64)) } 2 => { let ptr = args.get(1).unwrap().as_u32_unchecked(); let ptr2 = args.get(2).unwrap().as_u32_unchecked(); let result = unsafe { - __invokeHostFunc_2_1(func_id as u32, ptr as u64, ptr2 as u64) + __invokeHostFunc_2_1(func_id, ptr as u64, ptr2 as u64) }; Ok(JSValue::Float(result as f64)) } @@ -165,20 +165,20 @@ pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { )?; let host_invoke_func0 = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let func_id = args.get(0).unwrap().as_u32_unchecked(); + let func_id = args.first().unwrap().as_u32_unchecked(); let len = args.len() - 1; match len { 0 => { - unsafe { __invokeHostFunc_0_0(func_id as u32) }; + unsafe { __invokeHostFunc_0_0(func_id) }; } 1 => { let ptr = args.get(1).unwrap().as_u32_unchecked(); - unsafe { __invokeHostFunc_1_0(func_id as u32, ptr as u64) }; + unsafe { __invokeHostFunc_1_0(func_id, ptr as u64) }; } 2 => { let ptr = args.get(1).unwrap().as_u32_unchecked(); let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - unsafe { __invokeHostFunc_2_0(func_id as u32, ptr as u64, ptr2 as u64) }; + unsafe { __invokeHostFunc_2_0(func_id, ptr as u64, ptr2 as u64) }; } n => anyhow::bail!("__invokeHostFunc0 with {n} parameters is not implemented"), } @@ -198,7 +198,7 @@ pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { fn build_var_object(context: &JSContextRef) -> anyhow::Result { let var_set = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let var_name = args.get(0).ok_or(anyhow!("Expected var_name argument"))?; + let var_name = args.first().ok_or(anyhow!("Expected var_name argument"))?; let data = args.get(1).ok_or(anyhow!("Expected data argument"))?; if data.is_str() { @@ -212,7 +212,7 @@ fn build_var_object(context: &JSContextRef) -> anyhow::Result { )?; let var_get = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let var_name = args.get(0).ok_or(anyhow!("Expected var_name argument"))?; + let var_name = args.first().ok_or(anyhow!("Expected var_name argument"))?; let data = var::get::>(var_name.as_str()?)?; match data { Some(d) => Ok(JSValue::ArrayBuffer(d)), @@ -223,7 +223,7 @@ fn build_var_object(context: &JSContextRef) -> anyhow::Result { let var_get_str = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let var_name = args.get(0).ok_or(anyhow!("Expected var_name argument"))?; + let var_name = args.first().ok_or(anyhow!("Expected var_name argument"))?; let data = var::get::(var_name.as_str()?)?; match data { Some(d) => Ok(JSValue::String(d)), @@ -243,8 +243,7 @@ fn build_var_object(context: &JSContextRef) -> anyhow::Result { fn build_http_object(context: &JSContextRef) -> anyhow::Result { let http_req = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let req = args - .get(0) + let req = args.first() .ok_or(anyhow!("Expected http request argument"))?; if !req.is_object() { @@ -315,7 +314,7 @@ fn build_http_object(context: &JSContextRef) -> anyhow::Result { fn build_config_object(context: &JSContextRef) -> anyhow::Result { let config_get = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let key = args.get(0).ok_or(anyhow!("Expected key argument"))?; + let key = args.first().ok_or(anyhow!("Expected key argument"))?; if !key.is_str() { bail!("Expected key to be a string"); } @@ -337,7 +336,7 @@ fn build_config_object(context: &JSContextRef) -> anyhow::Result { fn build_memory(context: &JSContextRef) -> anyhow::Result { let memory_from_buffer = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let data = args.get(0).ok_or(anyhow!("Expected data argument"))?; + let data = args.first().ok_or(anyhow!("Expected data argument"))?; if !data.is_array_buffer() { bail!("Expected data to be an array buffer"); } @@ -353,7 +352,7 @@ fn build_memory(context: &JSContextRef) -> anyhow::Result { )?; let memory_find = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let ptr = args.get(0).ok_or(anyhow!("Expected ptr argument"))?; + let ptr = args.first().ok_or(anyhow!("Expected ptr argument"))?; if !ptr.is_number() { bail!("Expected a pointer"); } @@ -369,7 +368,7 @@ fn build_memory(context: &JSContextRef) -> anyhow::Result { )?; let read_bytes = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let ptr = args.get(0).ok_or(anyhow!("Expected ptr argument"))?; + let ptr = args.first().ok_or(anyhow!("Expected ptr argument"))?; if !ptr.is_number() { bail!("Expected a pointer"); } @@ -389,11 +388,11 @@ fn build_memory(context: &JSContextRef) -> anyhow::Result { } fn build_decoder(context: &JSContextRef) -> anyhow::Result { - Ok(context.wrap_callback(decode_utf8_buffer_to_js_string())?) + context.wrap_callback(decode_utf8_buffer_to_js_string()) } fn build_encoder(context: &JSContextRef) -> anyhow::Result { - Ok(context.wrap_callback(encode_js_string_to_utf8_buffer())?) + context.wrap_callback(encode_js_string_to_utf8_buffer()) } fn decode_utf8_buffer_to_js_string( diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 7d83ddf..ad190be 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -72,10 +72,10 @@ fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>(idx: i32, conv: F .map(|x| convert_js_value(context, x)) .collect(); let globals = context.global_object().unwrap(); - let names = export_names(&context).unwrap(); + let names = export_names(context).unwrap(); let f = globals.get_property(names[idx as usize].as_str()).unwrap(); let r = f.call(&context.undefined_value().unwrap(), &args).unwrap(); - conv(&context, r) + conv(context, r) } #[no_mangle] diff --git a/install.sh b/install.sh index 52c58d9..7de130f 100755 --- a/install.sh +++ b/install.sh @@ -43,6 +43,7 @@ if ! which "wasm-merge" > /dev/null; then sudo mkdir /usr/local/binaryen sudo mv binaryen/bin/wasm-merge /usr/local/binaryen/wasm-merge sudo ln -s /usr/local/binaryen/wasm-merge /usr/local/bin/wasm-merge + sudo ln -s /usr/local/binaryen/wasm-opt /usr/local/bin/wasm-opt else echo "wasm-merge already installed" From 7510b73b957f90e55727ee929a9acf77cae46921 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 15:08:57 -0800 Subject: [PATCH 09/20] cleanup: implement host function shims for up to 5 arguments --- .github/workflows/ci.yml | 1 + crates/cli/src/shims.rs | 2 +- crates/core/src/globals.rs | 106 +++++++++++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5cdb56..c6590ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: curl -L https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz > binaryen.tar.gz tar xvzf binaryen.tar.gz sudo cp binaryen-version_116/bin/wasm-merge /usr/local/bin + sudo cp binaryen-version_116/bin/wasm-opt /usr/local/bin - name: Run Tests env: diff --git a/crates/cli/src/shims.rs b/crates/cli/src/shims.rs index 2d35dcd..cf96108 100644 --- a/crates/cli/src/shims.rs +++ b/crates/cli/src/shims.rs @@ -52,7 +52,7 @@ pub fn generate_wasm_shims( import_elements.push(index.index()); } - for p in 0..=10 { + for p in 0..=5 { for q in 0..=1 { let indirect_type = module .types() diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index a6c4a65..e905717 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -29,7 +29,7 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { global.set_property("__decodeUtf8BufferToString", decoder)?; global.set_property("__encodeStringToUtf8Buffer", encoder)?; - inject_host_functions(context)?; + add_host_functions(context)?; context.eval_global( "script.js", @@ -47,10 +47,23 @@ extern "C" { // this import will get satisified by the import shim fn __invokeHostFunc_0_0(func_idx: u32); fn __invokeHostFunc_1_0(func_idx: u32, ptr: u64); - fn __invokeHostFunc_0_1(func_idx: u32) -> u64; fn __invokeHostFunc_2_0(func_idx: u32, ptr: u64, ptr2: u64); + fn __invokeHostFunc_3_0(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64); + fn __invokeHostFunc_4_0(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64, ptr4: u64); + fn __invokeHostFunc_5_0(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64, ptr4: u64, ptr5: u64); + fn __invokeHostFunc_0_1(func_idx: u32) -> u64; fn __invokeHostFunc_1_1(func_idx: u32, ptr: u64) -> u64; fn __invokeHostFunc_2_1(func_idx: u32, ptr: u64, ptr2: u64) -> u64; + fn __invokeHostFunc_3_1(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64) -> u64; + fn __invokeHostFunc_4_1(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64, ptr4: u64) -> u64; + fn __invokeHostFunc_5_1( + func_idx: u32, + ptr: u64, + ptr2: u64, + ptr3: u64, + ptr4: u64, + ptr5: u64, + ) -> u64; } fn build_console_object(context: &JSContextRef) -> anyhow::Result { @@ -130,7 +143,7 @@ fn build_host_object(context: &JSContextRef) -> anyhow::Result { Ok(host_object) } -pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { +fn add_host_functions(context: &JSContextRef) -> anyhow::Result<()> { let global = context.global_object()?; if global .get_property("Host")? @@ -154,8 +167,50 @@ pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { 2 => { let ptr = args.get(1).unwrap().as_u32_unchecked(); let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let result = + unsafe { __invokeHostFunc_2_1(func_id, ptr as u64, ptr2 as u64) }; + Ok(JSValue::Float(result as f64)) + } + 3 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let result = unsafe { + __invokeHostFunc_3_1(func_id, ptr as u64, ptr2 as u64, ptr3 as u64) + }; + Ok(JSValue::Float(result as f64)) + } + 4 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let ptr4 = args.get(4).unwrap().as_u32_unchecked(); + let result = unsafe { + __invokeHostFunc_4_1( + func_id, + ptr as u64, + ptr2 as u64, + ptr3 as u64, + ptr4 as u64, + ) + }; + Ok(JSValue::Float(result as f64)) + } + 5 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let ptr4 = args.get(4).unwrap().as_u32_unchecked(); + let ptr5 = args.get(5).unwrap().as_u32_unchecked(); let result = unsafe { - __invokeHostFunc_2_1(func_id, ptr as u64, ptr2 as u64) + __invokeHostFunc_5_1( + func_id, + ptr as u64, + ptr2 as u64, + ptr3 as u64, + ptr4 as u64, + ptr5 as u64, + ) }; Ok(JSValue::Float(result as f64)) } @@ -180,6 +235,46 @@ pub fn inject_host_functions(context: &JSContextRef) -> anyhow::Result<()> { let ptr2 = args.get(2).unwrap().as_u32_unchecked(); unsafe { __invokeHostFunc_2_0(func_id, ptr as u64, ptr2 as u64) }; } + 3 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + unsafe { + __invokeHostFunc_3_0(func_id, ptr as u64, ptr2 as u64, ptr3 as u64) + }; + } + 4 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let ptr4 = args.get(4).unwrap().as_u32_unchecked(); + unsafe { + __invokeHostFunc_4_0( + func_id, + ptr as u64, + ptr2 as u64, + ptr3 as u64, + ptr4 as u64, + ) + }; + } + 5 => { + let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let ptr4 = args.get(4).unwrap().as_u32_unchecked(); + let ptr5 = args.get(5).unwrap().as_u32_unchecked(); + unsafe { + __invokeHostFunc_5_0( + func_id, + ptr as u64, + ptr2 as u64, + ptr3 as u64, + ptr4 as u64, + ptr5 as u64, + ) + }; + } n => anyhow::bail!("__invokeHostFunc0 with {n} parameters is not implemented"), } @@ -243,7 +338,8 @@ fn build_var_object(context: &JSContextRef) -> anyhow::Result { fn build_http_object(context: &JSContextRef) -> anyhow::Result { let http_req = context.wrap_callback( |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| { - let req = args.first() + let req = args + .first() .ok_or(anyhow!("Expected http request argument"))?; if !req.is_object() { From 00598bc9c2eec5191429defff1b4019cd1eec4be Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 15:18:54 -0800 Subject: [PATCH 10/20] docs: add exports example --- examples/exports/script.d.ts | 10 ++++++++++ examples/exports/script.js | 15 +++++++++++++++ examples/host_funcs/host.py | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 examples/exports/script.d.ts create mode 100644 examples/exports/script.js diff --git a/examples/exports/script.d.ts b/examples/exports/script.d.ts new file mode 100644 index 0000000..3fc6c2a --- /dev/null +++ b/examples/exports/script.d.ts @@ -0,0 +1,10 @@ +declare module 'main' { + export function add3(a: I32, b: I32, c: I32): I32; + export function appendString(a: I64, b: I64): string; +} + +declare module 'extism:host' { + interface user { + testing123(a: string): {a: string}; + } +} diff --git a/examples/exports/script.js b/examples/exports/script.js new file mode 100644 index 0000000..a95895f --- /dev/null +++ b/examples/exports/script.js @@ -0,0 +1,15 @@ +/** + * A simple example of generate non-plugin function exports + */ + +function add3(a, b, c) { + return a + b + c; +} + +function appendString(a, b) { + a = Host.find(a).readString(); + b = Host.find(b).readString(); + return Memory.fromString(a + b).offset; +} + +module.exports = { add3, appendString } diff --git a/examples/host_funcs/host.py b/examples/host_funcs/host.py index 6fe3497..96a1e9c 100644 --- a/examples/host_funcs/host.py +++ b/examples/host_funcs/host.py @@ -2,7 +2,7 @@ import sys import json -set_log_file("stdout", level='trace') +set_log_file("stdout", level='info') @host_fn() def myHostFunction1(input: str) -> str: From 1c133ff482eb6be2a68fef57c85ad04bc64628e4 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 15:20:01 -0800 Subject: [PATCH 11/20] cleanup: remove imports from exports example --- examples/exports/script.d.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/exports/script.d.ts b/examples/exports/script.d.ts index 3fc6c2a..8c5ecef 100644 --- a/examples/exports/script.d.ts +++ b/examples/exports/script.d.ts @@ -1,10 +1,5 @@ declare module 'main' { export function add3(a: I32, b: I32, c: I32): I32; - export function appendString(a: I64, b: I64): string; + export function appendString(a: string, b: string): string; } -declare module 'extism:host' { - interface user { - testing123(a: string): {a: string}; - } -} From 77935b23d7e88ea65524ef1fe938341bba229d1e Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 15:45:01 -0800 Subject: [PATCH 12/20] cleanup: add --strip flag to wasm-opt --- crates/cli/src/opt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cli/src/opt.rs b/crates/cli/src/opt.rs index a275568..3632734 100644 --- a/crates/cli/src/opt.rs +++ b/crates/cli/src/opt.rs @@ -41,6 +41,7 @@ impl<'a> Optimizer<'a> { anyhow::bail!("Failed to detect wasm-opt. Please install binaryen and make sure wasm-opt is on your path: https://github.com/WebAssembly/binaryen"); } Command::new("wasm-opt") + .arg("--strip") .arg("-O3") .arg(dest.as_ref()) .arg("-o") From 1d5560059c70a859af98d86a1fb2ebeef776a7dc Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 13 Feb 2024 16:34:07 -0800 Subject: [PATCH 13/20] cleanup: only allow wasm native types for now --- crates/cli/src/ts_parser.rs | 28 +++++++++++++--------------- examples/exports/script.d.ts | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/cli/src/ts_parser.rs b/crates/cli/src/ts_parser.rs index 5634394..241ff1a 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -47,13 +47,13 @@ pub struct PluginInterface { pub imports: Vec, } -pub fn val_type(s: &str) -> ValType { +pub fn val_type(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { - "i32" => ValType::I32, - "i64" => ValType::I64, - "f32" => ValType::F32, - "f64" => ValType::F64, - _ => ValType::I64, // Extism handle + "i32" => Ok(ValType::I32), + "i64" | "ptr" => Ok(ValType::I64), + "f32" => Ok(ValType::F32), + "f64" => Ok(ValType::F64), + _ => anyhow::bail!("Unsupported type: {}", s), // Extism handle } } @@ -65,9 +65,9 @@ pub fn param_type(params: &mut Vec, vn: &str, t: &TsType) -> Result<()> { .sym .as_str() } else { - "i64" + anyhow::bail!("Unsupported param type: {:?}", t); }; - params.push(Param::new(vn, val_type(typ))); + params.push(Param::new(vn, val_type(typ)?)); Ok(()) } @@ -83,16 +83,14 @@ pub fn result_type(results: &mut Vec, return_type: &TsType) -> Result<()> ) } else if let Some(t) = return_type.as_ts_keyword_type() { match t.kind { - TsKeywordTypeKind::TsVoidKeyword - | TsKeywordTypeKind::TsUndefinedKeyword - | TsKeywordTypeKind::TsNullKeyword => None, - _ => Some("i64"), + TsKeywordTypeKind::TsVoidKeyword => None, + _ => anyhow::bail!("Unsupported return type: {:?}", t.kind), } } else { - Some("i64") + anyhow::bail!("Unsupported return type: {:?}", return_type) }; if let Some(r) = return_type { - results.push(Param::new("result", val_type(r))); + results.push(Param::new("result", val_type(r)?)); } Ok(()) } @@ -181,7 +179,7 @@ fn parse_module_decl(tsmod: &TsModuleDecl) -> Result { let name = param.pat.clone().expect_ident().id.sym.as_str().to_string(); let p = param.pat.clone().expect_ident(); match p.type_ann { - None => params.push(Param::new(&name, val_type("i64"))), + None => params.push(Param::new(&name, val_type("i64")?)), Some(ann) => { param_type(&mut params, &name, &ann.type_ann)?; } diff --git a/examples/exports/script.d.ts b/examples/exports/script.d.ts index 8c5ecef..1790fe7 100644 --- a/examples/exports/script.d.ts +++ b/examples/exports/script.d.ts @@ -1,5 +1,5 @@ declare module 'main' { export function add3(a: I32, b: I32, c: I32): I32; - export function appendString(a: string, b: string): string; + export function appendString(a: PTR, b: PTR): PTR; } From 8f21b7a3798ee13d5c0fbf69638fb10a3d6c30c6 Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 14 Feb 2024 09:34:37 -0800 Subject: [PATCH 14/20] cleanup: optimize after merging, add more memory helper functions, minify embedded js code --- crates/cli/src/main.rs | 4 +- crates/cli/src/opt.rs | 61 ++++++++++++++++++---------- crates/core/Cargo.toml | 1 - crates/core/src/prelude/esbuild.js | 14 +++---- crates/core/src/prelude/src/index.js | 52 ++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 31 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 50bed8e..a865d53 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -27,7 +27,7 @@ fn main() -> Result<()> { let opts = Options::from_args(); if opts.core { opt::Optimizer::new(CORE) - .optimize(true) + .wizen(true) .write_optimized_wasm(opts.output)?; return Ok(()); } @@ -120,5 +120,7 @@ fn main() -> Result<()> { bail!("wasm-merge failed. Couldn't merge shim"); } + opt::optimize_wasm_file(opts.output)?; + Ok(()) } diff --git a/crates/cli/src/opt.rs b/crates/cli/src/opt.rs index 3632734..46480b7 100644 --- a/crates/cli/src/opt.rs +++ b/crates/cli/src/opt.rs @@ -6,6 +6,7 @@ use std::{ use wizer::Wizer; pub(crate) struct Optimizer<'a> { + wizen: bool, optimize: bool, wasm: &'a [u8], } @@ -15,40 +16,56 @@ impl<'a> Optimizer<'a> { Self { wasm, optimize: false, + wizen: false, } } + #[allow(unused)] pub fn optimize(self, optimize: bool) -> Self { Self { optimize, ..self } } - pub fn write_optimized_wasm(self, dest: impl AsRef) -> Result<(), Error> { - let wasm = Wizer::new() - .allow_wasi(true)? - .inherit_stdio(true) - .wasm_bulk_memory(true) - .run(self.wasm)?; + pub fn wizen(self, wizen: bool) -> Self { + Self { wizen, ..self } + } - std::fs::write(&dest, wasm)?; + pub fn write_optimized_wasm(self, dest: impl AsRef) -> Result<(), Error> { + if self.wizen { + let wasm = Wizer::new() + .allow_wasi(true)? + .inherit_stdio(true) + .wasm_bulk_memory(true) + .run(self.wasm)?; + std::fs::write(&dest, wasm)?; + } else { + std::fs::write(&dest, &self.wasm)?; + } if self.optimize { - let output = Command::new("wasm-opt") - .arg("--version") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - if output.is_err() { - anyhow::bail!("Failed to detect wasm-opt. Please install binaryen and make sure wasm-opt is on your path: https://github.com/WebAssembly/binaryen"); - } - Command::new("wasm-opt") - .arg("--strip") - .arg("-O3") - .arg(dest.as_ref()) - .arg("-o") - .arg(dest.as_ref()) - .status()?; + optimize_wasm_file(dest)?; } Ok(()) } } + +pub(crate) fn optimize_wasm_file(dest: impl AsRef) -> Result<(), Error> { + let output = Command::new("wasm-opt") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + if output.is_err() { + anyhow::bail!("Failed to detect wasm-opt. Please install binaryen and make sure wasm-opt is on your path: https://github.com/WebAssembly/binaryen"); + } + Command::new("wasm-opt") + .arg("--enable-reference-types") + .arg("--enable-bulk-memory") + .arg("--strip") + .arg("-O3") + .arg(dest.as_ref()) + .arg("-o") + .arg(dest.as_ref()) + .status()?; + Ok(()) +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 66899b5..7691154 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -15,4 +15,3 @@ quickjs-wasm-rs = "3" [lib] crate_type = ["cdylib"] - diff --git a/crates/core/src/prelude/esbuild.js b/crates/core/src/prelude/esbuild.js index 6e838d7..fbd6b5a 100644 --- a/crates/core/src/prelude/esbuild.js +++ b/crates/core/src/prelude/esbuild.js @@ -1,12 +1,12 @@ -const esbuild = require('esbuild'); +const esbuild = require("esbuild"); esbuild .build({ - entryPoints: ['src/index.js'], - outdir: 'dist', + entryPoints: ["src/index.js"], + outdir: "dist", bundle: true, sourcemap: true, - minify: false, - format: 'cjs', // needs to be CJS for now - target: ['es2020'] // don't go over es2020 because quickjs doesn't support it - }) + minify: true, + format: "cjs", // needs to be CJS for now + target: ["es2020"], // don't go over es2020 because quickjs doesn't support it + }); diff --git a/crates/core/src/prelude/src/index.js b/crates/core/src/prelude/src/index.js index e9c7eca..bc88bc6 100644 --- a/crates/core/src/prelude/src/index.js +++ b/crates/core/src/prelude/src/index.js @@ -74,6 +74,30 @@ class MemoryHandle { return new TextDecoder().decode(this.readBytes()) } + readUInt32() { + const bytes = this.readBytes(); + const arr = new Uint32Array(bytes); + return arr[0]; + } + + readUInt64() { + const bytes = this.readBytes(); + const arr = new BigUint64Array(bytes); + return arr[0]; + } + + readFloat32() { + const bytes = this.readBytes(); + const arr = new Float32Array(bytes); + return arr[0]; + } + + readUInt64() { + const bytes = this.readBytes(); + const arr = new Float64Array(bytes); + return arr[0]; + } + readBytes() { return Memory._readBytes(this.offset) } @@ -106,6 +130,34 @@ Memory.fromJsonObject = (obj) => { return new MemoryHandle(memData.offset, memData.len) } +Memory.allocUInt32 = (i) => { + const buffer = new ArrayBuffer(4); + const arr = new Uint32Array(buffer); + arr[0] = i; + return Memory.fromBuffer(buffer); +} + +Memory.allocUInt64 = (i) => { + const buffer = new ArrayBuffer(8); + const arr = new BigUint64Array(buffer); + arr[0] = i; + return Memory.fromBuffer(buffer); +} + +Memory.allocFloat32 = (i) => { + const buffer = new ArrayBuffer(4); + const arr = new Float32Array(buffer); + arr[0] = i; + return Memory.fromBuffer(buffer); +} + +Memory.allocFloat64 = (i) => { + const buffer = new ArrayBuffer(8); + const arr = new Float64Array(buffer); + arr[0] = i; + return Memory.fromBuffer(buffer); +} + Memory.find = (offset) => { // todo validate const memData = Memory._find(offset) From b05343fb2f934282acfa0e6b4e8e971a18b7b8a2 Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 14 Feb 2024 09:35:47 -0800 Subject: [PATCH 15/20] cleanup: clippy --- crates/cli/src/opt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/opt.rs b/crates/cli/src/opt.rs index 46480b7..4283683 100644 --- a/crates/cli/src/opt.rs +++ b/crates/cli/src/opt.rs @@ -38,7 +38,7 @@ impl<'a> Optimizer<'a> { .run(self.wasm)?; std::fs::write(&dest, wasm)?; } else { - std::fs::write(&dest, &self.wasm)?; + std::fs::write(&dest, self.wasm)?; } if self.optimize { From aaddb20d7a499df58201feb6996c39e0711e8579 Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 14 Feb 2024 10:29:36 -0800 Subject: [PATCH 16/20] cleanup: prefer f64 over u32 --- crates/core/src/globals.rs | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/core/src/globals.rs b/crates/core/src/globals.rs index e905717..f1eb9f7 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -160,31 +160,31 @@ fn add_host_functions(context: &JSContextRef) -> anyhow::Result<()> { Ok(JSValue::Float(result as f64)) } 1 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); let result = unsafe { __invokeHostFunc_1_1(func_id, ptr as u64) }; Ok(JSValue::Float(result as f64)) } 2 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); let result = unsafe { __invokeHostFunc_2_1(func_id, ptr as u64, ptr2 as u64) }; Ok(JSValue::Float(result as f64)) } 3 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); + let ptr3 = args.get(3).unwrap().as_f64_unchecked(); let result = unsafe { __invokeHostFunc_3_1(func_id, ptr as u64, ptr2 as u64, ptr3 as u64) }; Ok(JSValue::Float(result as f64)) } 4 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let ptr3 = args.get(3).unwrap().as_u32_unchecked(); - let ptr4 = args.get(4).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); + let ptr3 = args.get(3).unwrap().as_f64_unchecked(); + let ptr4 = args.get(4).unwrap().as_f64_unchecked(); let result = unsafe { __invokeHostFunc_4_1( func_id, @@ -197,11 +197,11 @@ fn add_host_functions(context: &JSContextRef) -> anyhow::Result<()> { Ok(JSValue::Float(result as f64)) } 5 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let ptr3 = args.get(3).unwrap().as_u32_unchecked(); - let ptr4 = args.get(4).unwrap().as_u32_unchecked(); - let ptr5 = args.get(5).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); + let ptr3 = args.get(3).unwrap().as_f64_unchecked(); + let ptr4 = args.get(4).unwrap().as_f64_unchecked(); + let ptr5 = args.get(5).unwrap().as_f64_unchecked(); let result = unsafe { __invokeHostFunc_5_1( func_id, @@ -227,27 +227,27 @@ fn add_host_functions(context: &JSContextRef) -> anyhow::Result<()> { unsafe { __invokeHostFunc_0_0(func_id) }; } 1 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); unsafe { __invokeHostFunc_1_0(func_id, ptr as u64) }; } 2 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); unsafe { __invokeHostFunc_2_0(func_id, ptr as u64, ptr2 as u64) }; } 3 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let ptr3 = args.get(3).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); + let ptr3 = args.get(3).unwrap().as_f64_unchecked(); unsafe { __invokeHostFunc_3_0(func_id, ptr as u64, ptr2 as u64, ptr3 as u64) }; } 4 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let ptr3 = args.get(3).unwrap().as_u32_unchecked(); - let ptr4 = args.get(4).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); + let ptr3 = args.get(3).unwrap().as_f64_unchecked(); + let ptr4 = args.get(4).unwrap().as_f64_unchecked(); unsafe { __invokeHostFunc_4_0( func_id, @@ -259,11 +259,11 @@ fn add_host_functions(context: &JSContextRef) -> anyhow::Result<()> { }; } 5 => { - let ptr = args.get(1).unwrap().as_u32_unchecked(); - let ptr2 = args.get(2).unwrap().as_u32_unchecked(); - let ptr3 = args.get(3).unwrap().as_u32_unchecked(); - let ptr4 = args.get(4).unwrap().as_u32_unchecked(); - let ptr5 = args.get(5).unwrap().as_u32_unchecked(); + let ptr = args.get(1).unwrap().as_f64_unchecked(); + let ptr2 = args.get(2).unwrap().as_f64_unchecked(); + let ptr3 = args.get(3).unwrap().as_f64_unchecked(); + let ptr4 = args.get(4).unwrap().as_f64_unchecked(); + let ptr5 = args.get(5).unwrap().as_f64_unchecked(); unsafe { __invokeHostFunc_5_0( func_id, From 027eaf6b695c895af539723c1684eeac16df6faf Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 16 Feb 2024 16:15:34 -0800 Subject: [PATCH 17/20] test: use virtualenv for python host --- Makefile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1c370cc..116c0ba 100644 --- a/Makefile +++ b/Makefile @@ -44,10 +44,14 @@ clean-wasi-sdk: test: compile-examples @extism call examples/simple_js.wasm greet --wasi --input="Benjamin" @extism call examples/bundled.wasm greet --wasi --input="Benjamin" - @pip install -r examples/host_funcs/requirements.txt - @python examples/host_funcs/host.py examples/host_funcs.wasm + @python3 -m venv ./.venv && \ + . ./.venv/bin/activate && \ + pip install -r examples/host_funcs/requirements.txt && \ + python3 examples/host_funcs/host.py examples/host_funcs.wasm && \ + deactivate -compile-examples: +compile-examples: cli ./target/release/extism-js examples/simple_js/script.js -i examples/simple_js/script.d.ts -o examples/simple_js.wasm cd examples/bundled && npm install && npm run build && cd ../.. ./target/release/extism-js examples/host_funcs/script.js -i examples/host_funcs/script.d.ts -o examples/host_funcs.wasm + ./target/release/extism-js examples/exports/script.js -i examples/exports/script.d.ts -o examples/exports.wasm From e3ac6780d0a752a03fa811a46286782af0973897 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 22 Feb 2024 09:50:17 -0800 Subject: [PATCH 18/20] chore: use released version of wagen --- crates/cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1feac5c..1735d19 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -18,7 +18,7 @@ swc_atoms = "0.6.5" swc_common = "0.33.10" swc_ecma_ast = "0.112" swc_ecma_parser = "0.143" -wagen = {git = "https://github.com/dylibso/wagen"} +wagen = "0.1" log = "0.4.20" tempfile = "3.8.1" env_logger = "0.11" From 087fbe8fbb1074b440d5700101a526a02d879f7d Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 22 Feb 2024 09:50:40 -0800 Subject: [PATCH 19/20] chore: use rust stable on ci --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6590ca..d512da5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: default - toolchain: 1.73.0 + toolchain: stable target: wasm32-wasi default: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 99e257e..763bcd3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: default - toolchain: 1.73.0 + toolchain: stable target: wasm32-wasi default: true @@ -86,7 +86,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: default - toolchain: 1.73.0 + toolchain: stable target: ${{ matrix.target }} default: true From 209d3ffda354dee809ce46a14bf70d4b4c92d46c Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 22 Feb 2024 10:30:22 -0800 Subject: [PATCH 20/20] fix: port change to execute pending tasks --- crates/core/src/lib.rs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ad190be..f45cc8c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -63,7 +63,10 @@ fn convert_js_value<'a>(context: &'a JSContextRef, v: &JSValue) -> JSValueRef<'a } } -fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>(idx: i32, conv: F) -> T { +fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>( + idx: i32, + conv: F, +) -> Result { let call_args = unsafe { CALL_ARGS.pop() }; let context = js_context(); let args: Vec<_> = call_args @@ -75,7 +78,10 @@ fn invoke<'a, T, F: Fn(&'a JSContextRef, JSValueRef<'a>) -> T>(idx: i32, conv: F let names = export_names(context).unwrap(); let f = globals.get_property(names[idx as usize].as_str()).unwrap(); let r = f.call(&context.undefined_value().unwrap(), &args).unwrap(); - conv(context, r) + while context.is_pending() { + context.execute_pending()?; + } + Ok(conv(context, r)) } #[no_mangle] @@ -119,29 +125,45 @@ pub extern "C" fn __arg_f64(arg: f64) { } } +macro_rules! unwrap_value { + ($d:expr, $x:expr) => { + match $x { + Ok(x) => x, + Err(e) => { + let err = format!("{:?}", e); + let mem = extism_pdk::Memory::from_bytes(&err).unwrap(); + unsafe { + extism_pdk::extism::error_set(mem.offset()); + } + $d + } + } + }; +} + #[no_mangle] pub extern "C" fn __invoke_i32(idx: i32) -> i32 { - invoke(idx, |_ctx, r| r.as_i32_unchecked()) + unwrap_value!(-1, invoke(idx, |_ctx, r| r.as_i32_unchecked())) } #[no_mangle] pub extern "C" fn __invoke_i64(idx: i32) -> i64 { - invoke(idx, |_ctx, r| r.as_f64_unchecked() as i64) + unwrap_value!(-1, invoke(idx, |_ctx, r| r.as_f64_unchecked() as i64)) } #[no_mangle] pub extern "C" fn __invoke_f64(idx: i32) -> f64 { - invoke(idx, |_ctx, r| r.as_f64_unchecked()) + unwrap_value!(-1.0, invoke(idx, |_ctx, r| r.as_f64_unchecked())) } #[no_mangle] pub extern "C" fn __invoke_f32(idx: i32) -> f32 { - invoke(idx, |_ctx, r| r.as_f64_unchecked() as f32) + unwrap_value!(-1.0, invoke(idx, |_ctx, r| r.as_f64_unchecked() as f32)) } #[no_mangle] pub extern "C" fn __invoke(idx: i32) { - invoke(idx, |_ctx, _r| ()) + unwrap_value!((), invoke(idx, |_ctx, _r| ())) } fn export_names(context: &JSContextRef) -> anyhow::Result> {