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/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5cdb56..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 @@ -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/.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 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 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/Cargo.toml b/crates/cli/Cargo.toml index effa4fa..1735d19 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,15 +12,13 @@ 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" +wagen = "0.1" log = "0.4.20" tempfile = "3.8.1" -env_logger = "^0.10" +env_logger = "0.11" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a650fd4..a865d53 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) - .optimize(true) + if opts.core { + opt::Optimizer::new(CORE) + .wizen(true) .write_optimized_wasm(opts.output)?; - - env::remove_var("EXTISM_WIZEN"); - return Ok(()); } @@ -48,43 +42,37 @@ 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 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(); + let mut names = Vec::new(); + let mut sorted_names = Vec::new(); + for ns in &plugin_interface.imports { + 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)); + } + + 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 // 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"); { - env::set_var("EXTISM_WIZEN", "1"); let mut command = Command::new(self_cmd) + .arg("-c") .arg(&opts.input_js) .arg("-o") .arg(&core_path) @@ -101,49 +89,38 @@ 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(); - 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 export shim with the core module - let mut command = Command::new("wasm-merge") + // Merge the shim with the core module + let status = 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") .arg("--enable-bulk-memory") - .spawn()?; - let status = command.wait()?; + .status()?; if !status.success() { - bail!("wasm-merge failed. Couldn't merge import shim."); + 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 82721d1..4283683 100644 --- a/crates/cli/src/opt.rs +++ b/crates/cli/src/opt.rs @@ -1,9 +1,12 @@ -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> { + wizen: bool, optimize: bool, wasm: &'a [u8], } @@ -13,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 wizen(self, wizen: bool) -> Self { + Self { wizen, ..self } + } + pub fn write_optimized_wasm(self, dest: impl AsRef) -> Result<(), Error> { - let mut wasm = Wizer::new() - .allow_wasi(true)? - .inherit_stdio(true) - .wasm_bulk_memory(true) - .run(self.wasm)?; + 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 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, - }; - - 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"); - } + optimize_wasm_file(dest)?; } - std::fs::write(dest.as_ref(), wasm)?; - 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/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/shims.rs b/crates/cli/src/shims.rs index 4420a01..cf96108 100644 --- a/crates/cli/src/shims.rs +++ b/crates/cli/src/shims.rs @@ -1,192 +1,145 @@ -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::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; + let mut module = wagen::Module::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], []); + + let mut n_imports = 0; + for import in imports.iter() { + for _ in import.functions.iter() { + n_imports += 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; + 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); + import_items.push((f.name.clone(), index)); + } } - 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(); + import_items.sort_by_key(|x| x.0.to_string()); - // 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); + for (_f, index) in import_items { + import_elements.push(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 p in 0..=5 { + 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, + }); } - 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 + module.active_element( + Some(import_table), + wagen::Elements::Functions(&import_elements), ); - 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); - } + 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 + ); + } + 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..241ff1a 100644 --- a/crates/cli/src/ts_parser.rs +++ b/crates/cli/src/ts_parser.rs @@ -1,25 +1,29 @@ 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; 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}; #[derive(Debug, Clone)] pub struct Param { pub name: String, - pub ptype: String, + pub ptype: ValType, } impl Param { - pub fn new(name: &str, ptype: &str) -> Param { + pub fn new(name: &str, ptype: ValType) -> Param { Param { name: name.to_string(), - ptype: ptype.to_string().to_uppercase(), + ptype, } } } @@ -40,83 +44,110 @@ pub struct Interface { #[derive(Debug, Clone)] pub struct PluginInterface { pub exports: Interface, - pub imports: Interface, + pub imports: Vec, +} + +pub fn val_type(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "i32" => Ok(ValType::I32), + "i64" | "ptr" => Ok(ValType::I64), + "f32" => Ok(ValType::F32), + "f64" => Ok(ValType::F64), + _ => anyhow::bail!("Unsupported type: {}", s), // Extism handle + } +} + +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 { + anyhow::bail!("Unsupported param type: {:?}", t); + }; + params.push(Param::new(vn, val_type(typ)?)); + Ok(()) } -/// Parses the "user" part of the module which maps to the wasm imports -fn parse_user_interface(i: &Box) -> Result> { +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 => None, + _ => anyhow::bail!("Unsupported return type: {:?}", t.kind), + } + } else { + anyhow::bail!("Unsupported return type: {:?}", return_type) + }; + 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: &TsInterfaceDecl) -> 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); - } + for sig in &i.body.body { + match sig { + TsTypeElement::TsMethodSignature(t) => { + let name = t.key.as_ident().unwrap().sym.to_string(); + let mut params = vec![]; + let mut results = vec![]; - Param::new(vn, 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", return_type)]; - let signature = Signature { - name, - params, - results, - }; - signatures.push(signature); - } - _ => { - log::warn!("Warning: don't know what to do with sig {:#?}", sig); - } + 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, + 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 -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 { if let ModuleItem::Stmt(Stmt::Decl(decl)) = inter { let i = decl.as_ts_interface().unwrap(); - let interface = parse_user_interface(i)?; - return Ok(interface); + 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"); } @@ -129,7 +160,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 { @@ -138,22 +169,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", 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, @@ -180,24 +211,17 @@ 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("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,80 +229,10 @@ 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 { +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(), @@ -302,16 +256,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/Cargo.toml b/crates/core/Cargo.toml index 259cdd4..7691154 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -8,11 +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"] - 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 3277093..f1eb9f7 100644 --- a/crates/core/src/globals.rs +++ b/crates/core/src/globals.rs @@ -5,24 +5,18 @@ 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 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 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)?; @@ -35,6 +29,8 @@ pub fn inject_globals(context: &JSContextRef) -> anyhow::Result<()> { global.set_property("__decodeUtf8BufferToString", decoder)?; global.set_property("__encodeStringToUtf8Buffer", encoder)?; + add_host_functions(context)?; + context.eval_global( "script.js", "globalThis.module = {}; globalThis.module.exports = {}", @@ -45,10 +41,35 @@ 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_0_0(func_idx: u32); + fn __invokeHostFunc_1_0(func_idx: u32, ptr: 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 { 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) @@ -56,7 +77,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) @@ -64,7 +85,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) @@ -79,7 +100,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)?; @@ -102,41 +123,177 @@ 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)) }, )?; - let host_invoke_func = context.wrap_callback( - |_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) }; - Ok(JSValue::Int(result as i32)) - }, - )?; - 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) } +fn add_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.first().unwrap().as_u32_unchecked(); + let len = args.len() - 1; + match len { + 0 => { + let result = unsafe { __invokeHostFunc_0_1(func_id) }; + Ok(JSValue::Float(result as f64)) + } + 1 => { + 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_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_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_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, + 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_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, + ptr as u64, + ptr2 as u64, + ptr3 as u64, + ptr4 as u64, + ptr5 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.first().unwrap().as_u32_unchecked(); + let len = args.len() - 1; + match len { + 0 => { + unsafe { __invokeHostFunc_0_0(func_id) }; + } + 1 => { + 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_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_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_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, + ptr as u64, + ptr2 as u64, + ptr3 as u64, + ptr4 as u64, + ) + }; + } + 5 => { + 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, + 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"), + } + + 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]| { - 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() { @@ -150,7 +307,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)), @@ -161,7 +318,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)), @@ -182,7 +339,7 @@ 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) + .first() .ok_or(anyhow!("Expected http request argument"))?; if !req.is_object() { @@ -253,7 +410,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"); } @@ -275,7 +432,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"); } @@ -291,7 +448,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"); } @@ -307,7 +464,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"); } @@ -327,11 +484,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 afe01e2..f45cc8c 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,145 @@ pub extern "C" fn init() { } } +fn js_context<'a>() -> &'a JSContextRef { + unsafe { + if CONTEXT.get().is_none() { + init() + } + + let context = CONTEXT.get_unchecked(); + context + } +} + +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, +) -> Result { + 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(); + while context.is_pending() { + context.execute_pending()?; + } + Ok(conv(context, r)) +} + +#[no_mangle] +pub extern "C" fn __arg_start() { + unsafe { + CALL_ARGS.push(vec![]); + } +} + +#[no_mangle] +pub extern "C" fn __arg_i32(arg: i32) { + unsafe { + CALL_ARGS.last_mut().unwrap().push(JSValue::Int(arg)); + } +} + +#[no_mangle] +pub extern "C" fn __arg_i64(arg: i64) { + unsafe { + CALL_ARGS + .last_mut() + .unwrap() + .push(JSValue::Float(arg as f64)); + } +} + +#[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)); + } +} + +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 { + unwrap_value!(-1, invoke(idx, |_ctx, r| r.as_i32_unchecked())) +} + +#[no_mangle] +pub extern "C" fn __invoke_i64(idx: i32) -> i64 { + unwrap_value!(-1, invoke(idx, |_ctx, r| r.as_f64_unchecked() as i64)) +} + #[no_mangle] -pub unsafe extern "C" fn __invoke(func_idx: i32) -> i32 { - let context = unsafe { CONTEXT.get().unwrap() }; +pub extern "C" fn __invoke_f64(idx: i32) -> f64 { + unwrap_value!(-1.0, invoke(idx, |_ctx, r| r.as_f64_unchecked())) +} - 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 __invoke_f32(idx: i32) -> f32 { + unwrap_value!(-1.0, invoke(idx, |_ctx, r| r.as_f64_unchecked() as f32)) +} - unwrap!(result); - 0 +#[no_mangle] +pub extern "C" fn __invoke(idx: i32) { + unwrap_value!((), invoke(idx, |_ctx, _r| ())) } fn export_names(context: &JSContextRef) -> anyhow::Result> { 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 0e6db1c..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) @@ -115,14 +167,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 } diff --git a/examples/exports/script.d.ts b/examples/exports/script.d.ts new file mode 100644 index 0000000..1790fe7 --- /dev/null +++ b/examples/exports/script.d.ts @@ -0,0 +1,5 @@ +declare module 'main' { + export function add3(a: I32, b: I32, c: I32): I32; + export function appendString(a: PTR, b: PTR): PTR; +} + 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/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"