diff --git a/turing/Cargo.toml b/turing/Cargo.toml index 9b464b2..cc29059 100644 --- a/turing/Cargo.toml +++ b/turing/Cargo.toml @@ -7,9 +7,10 @@ edition = "2024" crate-type = ["cdylib", "staticlib", "rlib"] [features] -default = ["wasm", "lua"] +default = ["wasm", "lua", "deno"] wasm = ["dep:wasmtime", "dep:wasmtime-wasi"] lua = ["dep:mlua"] +deno = ["dep:deno_core", "dep:deno_error"] # Enables registration of global-based FFI functions for all engines global_ffi = [] @@ -39,11 +40,13 @@ smallvec = "1" parking_lot = "0.12.5" rustc-hash = "2" convert_case = "0.10.0" +deno_core = {version = "0.375.0", optional = true} +deno_error = {version = "0.7", optional = true} [dev-dependencies] # for testing with wasmtime wat = { version = "1.239.0" } -wasmprinter = {version = "0.243.0" } +wasmprinter = { version = "0.243.0" } serial_test = "3.2.0" criterion = "0.8" diff --git a/turing/src/engine/deno_engine.rs b/turing/src/engine/deno_engine.rs new file mode 100644 index 0000000..6591b9e --- /dev/null +++ b/turing/src/engine/deno_engine.rs @@ -0,0 +1,576 @@ +use std::fs; +use std::marker::PhantomData; +use std::path::Path; +use std::sync::Arc; + +use anyhow::{Result, anyhow}; +use deno_core::op2; +use deno_core::{JsRuntime, OpState, RuntimeOptions, serde_v8, v8}; +use deno_error::JsErrorBox; +use parking_lot::RwLock; +use rustc_hash::FxHashMap; + +use crate::OpaquePointerKey; +use crate::engine::types::ScriptFnMetadata; +use crate::interop::params::{DataType, Param, Params}; +use crate::interop::types::ExtPointer; +use crate::{EngineDataState, ExternalFunctions}; +use slotmap::KeyData; + + +pub struct DenoEngine +where + Ext: ExternalFunctions + Send + Sync + 'static, +{ + runtime: JsRuntime, + module_name: Option, + deno_fns: FxHashMap, + deno_fn_handles: FxHashMap>, + data: Arc>, + _ext: PhantomData, +} + +// Convert a host Param into a V8 `Value` within the provided scope. +fn param_to_v8<'s>( + scope: &mut deno_core::v8::PinScope<'s, '_>, + param: Param, + data: &Arc>, +) -> Result, JsErrorBox> { + // Convert Param -> serde_json -> V8 using serde_v8 to avoid scope type mismatches + + match param { + Param::I8(i) => serde_v8::to_v8(scope, i).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::I16(i) => serde_v8::to_v8(scope, i).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::I32(i) => serde_v8::to_v8(scope, i).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::I64(i) => serde_v8::to_v8(scope, i).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::U8(u) => serde_v8::to_v8(scope, u).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::U16(u) => serde_v8::to_v8(scope, u).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::U32(u) => serde_v8::to_v8(scope, u).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::U64(u) => serde_v8::to_v8(scope, u).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::F32(f) => serde_v8::to_v8(scope, f).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::F64(f) => serde_v8::to_v8(scope, f).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::Bool(b) => serde_v8::to_v8(scope, b).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::String(s) => { + serde_v8::to_v8(scope, s).map_err(|e| JsErrorBox::generic(e.to_string())) + } + Param::Void => serde_v8::to_v8(scope, ()).map_err(|e| JsErrorBox::generic(e.to_string())), + Param::Object(ptr) => { + let mut write = data.write(); + let key = write.get_opaque_pointer(ExtPointer { ptr }); + let id = key.0.as_ffi(); + serde_v8::to_v8(scope, id).map_err(|e| JsErrorBox::generic(e.to_string())) + } + Param::Error(e) => Err(JsErrorBox::generic(e)), + } +} + +// Convert a V8 `Value` into a host `Param`. +fn v8_to_param<'s>( + scope: &mut deno_core::v8::PinScope<'s, '_>, + data: &Arc>, + value: v8::Local<'s, v8::Value>, + expect_type: Option, +) -> Param { + if let Some(expect_type) = expect_type { + return match expect_type { + DataType::Void => Param::Void, + DataType::Bool => { + if value.is_boolean() { + Param::Bool(value.boolean_value(scope)) + } else { + Param::Error("expected boolean".to_string()) + } + } + DataType::I32 => { + if value.is_int32() { + Param::I32(value.int32_value(scope).unwrap()) + } else { + Param::Error("expected int32".to_string()) + } + } + DataType::F64 => { + if value.is_number() { + Param::F64(value.number_value(scope).unwrap()) + } else { + Param::Error("expected number".to_string()) + } + } + DataType::RustString | DataType::ExtString => { + if value.is_string() { + let s = value.to_rust_string_lossy(scope); + Param::String(s) + } else { + Param::Error("expected string".to_string()) + } + } + DataType::Object => { + // expect a big integer id + if value.is_big_int() { + let id = value.to_big_int(scope).unwrap().i64_value().0 as u64; + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(id)); + + let read = data.read(); + let Some(real) = read.opaque_pointers.get(pointer_key) else { + return Param::Error(format!("Invalid opaque pointer id: {}", id)); + }; + return Param::Object(real.ptr); + } else { + Param::Error("expected object id (bigint)".to_string()) + } + } + _ => unreachable!("unsupported expected type {}", expect_type), + }; + } + + if value.is_undefined() || value.is_null() { + return Param::Void; + } + if value.is_boolean() { + return Param::Bool(value.boolean_value(scope)); + } + if value.is_int32() { + return Param::I32(value.int32_value(scope).unwrap()); + } + if value.is_uint32() { + return Param::U32(value.uint32_value(scope).unwrap()); + } + if value.is_big_int() { + return Param::I64(value.to_big_int(scope).unwrap().i64_value().0); + } + if value.is_number() { + return Param::F64(value.number_value(scope).unwrap()); + } + if value.is_string() { + let s = value.to_rust_string_lossy(scope); + return Param::String(s); + } + if value.is_object() { + // get the object's identity field + let obj = value.to_object(scope).unwrap(); + let id_key = v8::String::new(scope, "__turing_pointer_id").unwrap(); + let id_val = obj.get(scope, id_key.into()).unwrap(); + // assume it's a big integer + let id = id_val.to_big_int(scope).unwrap().i64_value().0 as u64; + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(id)); + + let read = data.read(); + let Some(real) = read.opaque_pointers.get(pointer_key) else { + return Param::Error(format!("Invalid opaque pointer id: {}", id)); + }; + return Param::Object(real.ptr); + } + + if value.is_array() { + return Param::Error("Array return types are not supported".to_string()); + } + + if value.is_function() { + return Param::Error("Function return types are not supported".to_string()); + } + + unreachable!("Does not support {value:?}") +} + +// Single dispatch op which receives a JSON array like `["fn.name", [arg0, arg1, ...]]` +/// Handles calling registered FFI functions from JS. +#[op2] +#[global] +fn turing_dispatch( + state: &mut OpState, + payload: v8::Local, +) -> Result, JsErrorBox> { + // payload expected to be [name, args] + + if !payload.is_array() { + return Err(JsErrorBox::generic("invalid payload")); + } + + let data = state.borrow::>>().clone(); + let map = state + .borrow::>() + .clone(); + + // parse array + let array = v8::Local::::try_from(payload) + .map_err(|_| JsErrorBox::generic("invalid payload"))?; + + let runtime = state.borrow_mut::(); + deno_core::scope!(scope, runtime); + // let scope = state.borrow_mut(); + + let (name, args) = { + let length = array.length(); + let name = array + .get_index(scope, 0) + .ok_or_else(|| JsErrorBox::generic("invalid payload"))? + .to_string(scope) + .ok_or_else(|| JsErrorBox::generic("invalid function name"))? + .to_rust_string_lossy(scope); + let args = (0..length) + .filter_map(|i| array.get_index(scope, i)) + .collect::>(); + + (name, args) + }; + + let metadata = map + .get(&name) + .ok_or_else(|| JsErrorBox::generic("function not found"))? + .clone(); + + let p_types = metadata.param_types.clone(); + + let params = Params::from_iter( + p_types + .iter() + .enumerate() + .map(|(i, exp_type)| { + let arg = args + .get(i) + .ok_or_else(|| JsErrorBox::generic("missing argument"))?; + let param = v8_to_param(scope, &data, *arg, Some(*exp_type)); + Ok(param) + }) + .collect::, JsErrorBox>>()?, + ); + + let ffi = params.to_ffi::(); + let ffi_arr = ffi.as_ffi_array(); + let ret = (metadata.callback)(ffi_arr); + + // convert return to JSON + let ret = ret + .into_param::() + .map_err(|e| JsErrorBox::generic("failed return value"))?; + + let j = param_to_v8(scope, ret, &data)?; + + let global = v8::Global::new(scope, j); + + Ok(global) +} + +#[doc = r""] +#[doc = r" An extension for use with the Deno JS runtime."] +#[doc = r" To use it, provide it as an argument when instantiating your runtime:"] +#[doc = r""] +#[doc = r" ```rust,ignore"] +#[doc = r" use deno_core::{ JsRuntime, RuntimeOptions };"] +#[doc = r""] +#[doc = concat!("let mut extensions = vec![",stringify!(turing),"::init()];")] +#[doc = r" let mut js_runtime = JsRuntime::new(RuntimeOptions {"] +#[doc = r" extensions,"] +#[doc = r" ..Default::default()"] +#[doc = r" });"] +#[doc = r" ```"] +#[doc = r""] +#[allow(non_camel_case_types)] +pub struct turing_op { + _phantom: ::std::marker::PhantomData, +} + +impl turing_op { + fn ext() -> deno_core::Extension { + #[allow(unused_imports)] + use deno_core::Op; + deno_core::Extension { + name: ::std::stringify!(turing), + deps: &[], + js_files: { + const JS: &[deno_core::ExtensionFileSource] = &deno_core::include_js_files!(turing); + ::std::borrow::Cow::Borrowed(JS) + }, + esm_files: { + const JS: &[deno_core::ExtensionFileSource] = &deno_core::include_js_files!(turing); + ::std::borrow::Cow::Borrowed(JS) + }, + lazy_loaded_esm_files: { + const JS: &[deno_core::ExtensionFileSource] = + &deno_core::include_lazy_loaded_js_files!(turing); + ::std::borrow::Cow::Borrowed(JS) + }, + esm_entry_point: { + const V: ::std::option::Option<&'static ::std::primitive::str> = + deno_core::or!(, ::std::option::Option::None); + V + }, + ops: ::std::borrow::Cow::Owned(vec![{ turing_dispatch::() }]), + objects: ::std::borrow::Cow::Borrowed(&[]), + external_references: ::std::borrow::Cow::Borrowed(&[]), + global_template_middleware: ::std::option::Option::None, + global_object_middleware: ::std::option::Option::None, + op_state_fn: ::std::option::Option::None, + needs_lazy_init: false, + middleware_fn: ::std::option::Option::None, + enabled: true, + } + } + #[inline(always)] + #[allow(unused_variables)] + fn with_ops_fn(ext: &mut deno_core::Extension) { + deno_core::extension!(!__ops__ ext __eot__); + } + #[inline(always)] + #[allow(unused_variables)] + fn with_middleware(ext: &mut deno_core::Extension) {} + + #[inline(always)] + #[allow(unused_variables)] + #[allow(clippy::redundant_closure_call)] + fn with_customizer(ext: &mut deno_core::Extension) {} + + #[doc = r" Initialize this extension for runtime or snapshot creation."] + #[doc = r""] + #[doc = r" # Returns"] + #[doc = r" an Extension object that can be used during instantiation of a JsRuntime"] + #[allow(dead_code)] + pub fn init() -> deno_core::Extension { + let mut ext = Self::ext(); + Self::with_ops_fn(&mut ext); + deno_core::extension!(!__config__ ext); + Self::with_middleware(&mut ext); + Self::with_customizer(&mut ext); + ext + } + #[doc = r" Initialize this extension for runtime or snapshot creation."] + #[doc = r""] + #[doc = r" If this method is used, you must later call `JsRuntime::lazy_init_extensions`"] + #[doc = r" with the result of this extension's `args` method."] + #[doc = r""] + #[doc = r" # Returns"] + #[doc = r" an Extension object that can be used during instantiation of a JsRuntime"] + #[allow(dead_code)] + pub fn lazy_init() -> deno_core::Extension { + let mut ext = Self::ext(); + Self::with_ops_fn(&mut ext); + ext.needs_lazy_init = true; + Self::with_middleware(&mut ext); + Self::with_customizer(&mut ext); + ext + } + #[doc = r" Create an `ExtensionArguments` value which must be passed to"] + #[doc = r" `JsRuntime::lazy_init_extensions`."] + #[allow(dead_code, unused_mut)] + pub fn args() -> deno_core::ExtensionArguments { + deno_core::extension!(!__config__ args); + deno_core::ExtensionArguments { + name: ::std::stringify!(turing), + op_state_fn: ::std::option::Option::None, + } + } +} + +impl DenoEngine +where + Ext: ExternalFunctions + Send + Sync + 'static, +{ + pub fn new( + js_functions: &FxHashMap, + data: Arc>, + ) -> Result { + // Register a single dispatch op generated by the `#[op]` macro. + + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![turing_op::::init()], + module_loader: None, + ..Default::default() + }); + + // Inject a small helper once to minimize per-call overhead. + // `__turing_call(name, argsArray)` will look up the function and call it. + let helper = r#"globalThis.__turing_call = function(name, args) { const fn = globalThis[name]; if (typeof fn !== 'function') throw new Error('function not found'); return fn.apply(null, args); };"#; + runtime + .execute_script("__turing_helper", helper) + .map_err(|e| anyhow!(e.to_string()))?; + + // Pre-cache function handles for faster calls. + let mut fn_handles: FxHashMap> = FxHashMap::default(); + { + let context = runtime.main_context(); + deno_core::scope!(scope, &mut runtime); + + for name in js_functions.keys() { + let ctx = v8::Local::new(scope, &context); + let g = ctx.global(scope); + let Some(key) = v8::String::new(scope, name.as_str()) else { + continue; + }; + let Some(val) = g.get(scope, key.into()) else { + continue; + }; + if !val.is_function() { + continue; + } + let Ok(func) = v8::Local::::try_from(val) else { + continue; + }; + fn_handles.insert(name.clone(), v8::Global::new(scope, func)); + } + } + + Ok(Self { + deno_fn_handles: fn_handles, + runtime, + module_name: None, + deno_fns: js_functions.clone(), + data, + _ext: PhantomData, + }) + } + + pub fn load_script(&mut self, path: &Path) -> Result<()> { + let script = fs::read_to_string(path)?; + + let mname = path.to_string_lossy().to_string(); + self.runtime + .execute_script(mname.clone(), script) + .map_err(|e| anyhow!(e.to_string()))?; + self.module_name = Some(mname); + + Ok(()) + } + + pub fn call_fn( + &mut self, + name: &str, + params: Params, + ret_type: crate::interop::params::DataType, + data: Arc>, + ) -> crate::interop::params::Param { + // Basic implementation: serialize params to JSON and invoke the global JS + // function by name. For now the return value is not converted in full + // generality — we return `Void` on success and `Error(...)` on failure. + + // If we have a cached function handle, call it directly using V8 locals + if let Some(func_global) = self.deno_fn_handles.get(name).cloned() { + return self.quick_call(ret_type, &data, params, &func_global); + } + // Fallback: stringify args and run the helper (older path) + let json_args = params + .into_iter() + .map(|p| p.to_serde(&data)) + .collect::, _>>(); + + let args_literal = match json_args { + Ok(vec) => serde_json::to_string(&serde_json::Value::Array(vec)) + .unwrap_or_else(|_| "[]".to_string()), + Err(e) => return Param::Error(format!("argument conversion error: {}", e)), + }; + + let call_code = format!( + "__turing_call({}, {});", + serde_json::to_string(&name).unwrap(), + args_literal + ); + + let script_name = format!("turing_call:{}", name); + match self.runtime.execute_script(script_name, call_code) { + Ok(global_val) => { + // convert return value directly from V8 + deno_core::scope!(scope, self.runtime); + let local = v8::Local::new(scope, &global_val); + + let param = v8_to_param(scope, &data, local, Some(ret_type)); + + if ret_type == DataType::Object { + match param { + Param::I64(i) => { + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(i as u64)); + let real = data + .read() + .opaque_pointers + .get(pointer_key) + .copied() + .unwrap_or_default(); + return Param::Object(real.ptr); + } + Param::U64(u) => { + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(u)); + let real = data + .read() + .opaque_pointers + .get(pointer_key) + .copied() + .unwrap_or_default(); + return Param::Object(real.ptr); + } + _ => { + return Param::Error("expected object id (number) from JS".to_string()); + } + } + } + + param + } + Err(e) => Param::Error(e.to_string()), + } + } + + fn quick_call( + &mut self, + ret_type: DataType, + data: &Arc>, + args_vec: Params, + func_global: &v8::Global, + ) -> Param { + deno_core::scope!(scope, self.runtime); + // let context = self.runtime.main_context(); + // let isolate = self.runtime.v8_isolate(); + // deno_core::v8::scope!(let scope, isolate); + // let context = v8::Local::new(scope, &context); + // let scope = &mut ContextScope::new(scope, context); + + // enter scope and convert Params -> V8 locals + let v8_args: Vec> = match args_vec + .into_iter() + .map(|p| match param_to_v8(scope, p, data) { + Ok(l) => Ok(l), + Err(e) => Err(format!("argument conversion error: {}", e)), + }) + .collect::>() + { + Ok(v) => v, + Err(e) => return Param::Error(e), + }; + + let local_func = v8::Local::new(scope, func_global); + let recv = v8::undefined(scope).into(); + let result = local_func.call(scope, recv, &v8_args); + let result = match result { + Some(r) => r, + None => return Param::Error("JS call threw".to_string()), + }; + + // convert V8 value -> Param directly + let param = v8_to_param(scope, data, result, None); + + // If the caller expects an object, interpret numeric return as opaque id + if ret_type == DataType::Object { + match param { + Param::I64(i) => { + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(i as u64)); + let real = data + .read() + .opaque_pointers + .get(pointer_key) + .copied() + .unwrap_or_default(); + return Param::Object(real.ptr); + } + Param::U64(u) => { + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(u)); + let real = data + .read() + .opaque_pointers + .get(pointer_key) + .copied() + .unwrap_or_default(); + return Param::Object(real.ptr); + } + _ => return Param::Error("expected object id (number) from JS".to_string()), + } + } + + param + } +} diff --git a/turing/src/engine/deno_engine/conversion.rs b/turing/src/engine/deno_engine/conversion.rs new file mode 100644 index 0000000..1593790 --- /dev/null +++ b/turing/src/engine/deno_engine/conversion.rs @@ -0,0 +1,55 @@ +use deno_core::{FromV8, ToV8, op2, v8::BigInt}; + +use crate::interop::params::Param; + +pub struct TuringFunctionDispatch(String, Vec); + +impl<'a> ToV8<'a> for Param { + type Error = std::convert::Infallible; + + fn to_v8<'i>( + self, + scope: &mut deno_core::v8::PinScope<'a, 'i>, + ) -> Result, >::Error> { + match self { + Param::String(s) => s.to_v8(scope).map_err(|e| e.into()), + Param::I8(i) => i.to_v8(scope).map_err(|e| e.into()), + Param::I16(i) => i.to_v8(scope).map_err(|e| e.into()), + Param::I32(i) => i.to_v8(scope).map_err(|e| e.into()), + Param::I64(i) => Ok(BigInt::new_from_i64(scope, i).cast()), + Param::U8(u) => u.to_v8(scope).map_err(|e| e.into()), + Param::U16(u) => u.to_v8(scope).map_err(|e| e.into()), + Param::U32(u) => u.to_v8(scope).map_err(|e| e.into()), + Param::U64(u) => Ok(BigInt::new_from_u64(scope, u).cast()), + Param::F32(f) => f.to_v8(scope).map_err(|e| e.into()), + Param::F64(f) => Ok(deno_core::v8::Number::new(scope, f as f64).cast()), + Param::Bool(b) => b.to_v8(scope).map_err(|e| e.into()), + Param::Object(o) => { + todo!() + }, + Param::Error(_) => todo!(), + Param::Void => todo!(), + } + } +} +impl<'a> FromV8<'a> for Param { + type Error = std::convert::Infallible; + + fn from_v8<'i>( + scope: &mut deno_core::v8::PinScope<'a, 'i>, + value: deno_core::v8::Local<'a, deno_core::v8::Value>, + ) -> Result>::Error> { + if value.is_string() { + let s = String::from_v8(scope, value).unwrap(); + Ok(Param::String(s)) + } else if value.is_big_int() { + let bi = deno_core::v8::Local::::try_from(value).unwrap(); + let u = bi.u64_value().0; + Ok(Param::U64(u)) + } else { + unimplemented!() + } + } + + +} diff --git a/turing/src/engine/mod.rs b/turing/src/engine/mod.rs index aa38fce..fc90443 100644 --- a/turing/src/engine/mod.rs +++ b/turing/src/engine/mod.rs @@ -13,6 +13,9 @@ pub mod lua_engine; #[cfg(feature = "wasm")] pub mod wasm_engine; +#[cfg(feature = "deno")] +pub mod deno_engine; + pub mod types; pub enum Engine @@ -23,6 +26,8 @@ where Wasm(wasm_engine::WasmInterpreter), #[cfg(feature = "lua")] Lua(lua_engine::LuaInterpreter), + #[cfg(feature = "deno")] + Deno(deno_engine::DenoEngine), } impl Engine @@ -41,6 +46,8 @@ where Engine::Wasm(engine) => engine.call_fn(name, params, ret_type, data), #[cfg(feature = "lua")] Engine::Lua(engine) => engine.call_fn(name, params, ret_type, data), + #[cfg(feature = "deno")] + Engine::Deno(engine) => engine.call_fn(name, params, ret_type, data), _ => Param::Error("No code engine is active".to_string()), } } diff --git a/turing/src/interop/params.rs b/turing/src/interop/params.rs index 27c4be2..83cb7c8 100644 --- a/turing/src/interop/params.rs +++ b/turing/src/interop/params.rs @@ -1,16 +1,16 @@ +use crate::interop::types::ExtString; +use crate::{EngineDataState, ExternalFunctions, OpaquePointerKey}; +use anyhow::{Result, anyhow}; +use num_enum::TryFromPrimitive; use parking_lot::RwLock; +use slotmap::KeyData; use smallvec::SmallVec; use std::ffi::{CStr, CString, c_char, c_void}; use std::fmt::Display; use std::marker::PhantomData; use std::mem; -use std::sync::{Arc}; -use anyhow::{anyhow, Result}; -use num_enum::TryFromPrimitive; -use slotmap::KeyData; -use crate::{ExternalFunctions, OpaquePointerKey, EngineDataState}; -use crate::interop::types::ExtString; - +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; #[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, TryFromPrimitive)] @@ -73,19 +73,19 @@ impl DataType { matches!( self, DataType::I8 - | DataType::I16 - | DataType::I32 - | DataType::I64 - | DataType::U8 - | DataType::U16 - | DataType::U32 - | DataType::U64 - | DataType::F32 - | DataType::F64 - | DataType::Bool - | DataType::RustString - | DataType::ExtString - | DataType::Object + | DataType::I16 + | DataType::I32 + | DataType::I64 + | DataType::U8 + | DataType::U16 + | DataType::U32 + | DataType::U64 + | DataType::F32 + | DataType::F64 + | DataType::Bool + | DataType::RustString + | DataType::ExtString + | DataType::Object ) } @@ -93,20 +93,20 @@ impl DataType { matches!( self, DataType::I8 - | DataType::I16 - | DataType::I32 - | DataType::I64 - | DataType::U8 - | DataType::U16 - | DataType::U32 - | DataType::U64 - | DataType::F32 - | DataType::F64 - | DataType::Bool - | DataType::RustString - | DataType::ExtString - | DataType::Object - | DataType::Void + | DataType::I16 + | DataType::I32 + | DataType::I64 + | DataType::U8 + | DataType::U16 + | DataType::U32 + | DataType::U64 + | DataType::F32 + | DataType::F64 + | DataType::Bool + | DataType::RustString + | DataType::ExtString + | DataType::Object + | DataType::Void ) } @@ -129,14 +129,19 @@ impl DataType { DataType::F32 => Ok(wasmtime::ValType::F32), DataType::F64 => Ok(wasmtime::ValType::F64), - _ => Err(anyhow!("Invalid wasm value type: {}", self)) + _ => Err(anyhow!("Invalid wasm value type: {}", self)), } } #[cfg(feature = "wasm")] - pub fn to_wasm_val_param(&self, val: &wasmtime::Val, caller: &mut wasmtime::Caller<'_, wasmtime_wasi::p1::WasiP1Ctx>, data: &Arc>) -> Result { - use wasmtime::Val; + pub fn to_wasm_val_param( + &self, + val: &wasmtime::Val, + caller: &mut wasmtime::Caller<'_, wasmtime_wasi::p1::WasiP1Ctx>, + data: &Arc>, + ) -> Result { use crate::engine::wasm_engine::get_wasm_string; + use wasmtime::Val; match (self, val) { (DataType::I8, Val::I32(i)) => Ok(Param::I8(*i as i8)), @@ -151,62 +156,74 @@ impl DataType { (DataType::F64, Val::F64(f)) => Ok(Param::F64(f64::from_bits(*f))), (DataType::Bool, Val::I32(b)) => Ok(Param::Bool(*b != 0)), (DataType::RustString | DataType::ExtString, Val::I32(ptr)) => { - let ptr = *ptr as u32; let Some(memory) = caller.get_export("memory").and_then(|e| e.into_memory()) else { - return Err(anyhow!("wasm does not export memory")) + return Err(anyhow!("wasm does not export memory")); }; let st = get_wasm_string(ptr, memory.data(&caller)); Ok(Param::String(st)) } (DataType::Object, Val::I64(pointer_id)) => { - let pointer_key = - OpaquePointerKey::from(KeyData::from_ffi(*pointer_id as u64)); + let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(*pointer_id as u64)); if let Some(true_pointer) = data.read().opaque_pointers.get(pointer_key) { Ok(Param::Object(**true_pointer)) } else { - Err(anyhow!("opaque pointer does not correspond to a real pointer")) + Err(anyhow!( + "opaque pointer does not correspond to a real pointer" + )) } - } - _ => Err(anyhow!("Mismatched parameter type")) + _ => Err(anyhow!("Mismatched parameter type")), } } #[cfg(feature = "lua")] - pub fn to_lua_val_param(&self, val: &mlua::Value, data: &Arc>) -> mlua::Result { + pub fn to_lua_val_param( + &self, + val: &mlua::Value, + data: &Arc>, + ) -> mlua::Result { match (self, val) { - (DataType::I8, mlua::Value::Integer(i)) => Ok(Param::I8(*i as i8)), + (DataType::I8, mlua::Value::Integer(i)) => Ok(Param::I8(*i as i8)), (DataType::I16, mlua::Value::Integer(i)) => Ok(Param::I16(*i as i16)), (DataType::I32, mlua::Value::Integer(i)) => Ok(Param::I32(*i as i32)), (DataType::I64, mlua::Value::Integer(i)) => Ok(Param::I64(*i)), - (DataType::U8, mlua::Value::Integer(u)) => Ok(Param::U8(*u as u8)), + (DataType::U8, mlua::Value::Integer(u)) => Ok(Param::U8(*u as u8)), (DataType::U16, mlua::Value::Integer(u)) => Ok(Param::U16(*u as u16)), (DataType::U32, mlua::Value::Integer(u)) => Ok(Param::U32(*u as u32)), (DataType::U64, mlua::Value::Integer(u)) => Ok(Param::U64(*u as u64)), (DataType::F32, mlua::Value::Number(f)) => Ok(Param::F32(*f as f32)), (DataType::F64, mlua::Value::Number(f)) => Ok(Param::F64(*f)), (DataType::Bool, mlua::Value::Boolean(b)) => Ok(Param::Bool(*b)), - (DataType::RustString | DataType::ExtString, mlua::Value::String(s)) => Ok(Param::String(s.to_string_lossy())), + (DataType::RustString | DataType::ExtString, mlua::Value::String(s)) => { + Ok(Param::String(s.to_string_lossy())) + } (DataType::Object, mlua::Value::Table(t)) => { let key = t.raw_get::("opaqu")?; let key = match key { mlua::Value::Integer(i) => i as u64, - _ => return Err(mlua::Error::RuntimeError("Incorrect type for opaque handle".to_string())) + _ => { + return Err(mlua::Error::RuntimeError( + "Incorrect type for opaque handle".to_string(), + )); + } }; let pointer_key = OpaquePointerKey::from(KeyData::from_ffi(key)); if let Some(true_pointer) = data.read().opaque_pointers.get(pointer_key) { Ok(Param::Object(**true_pointer)) } else { - Err(mlua::Error::RuntimeError("opaque pointer does not correspond to a real pointer".to_string())) + Err(mlua::Error::RuntimeError( + "opaque pointer does not correspond to a real pointer".to_string(), + )) } } - _ => Err(mlua::Error::RuntimeError(format!("Mismatched parameter type: {self} with {val:?}"))) + _ => Err(mlua::Error::RuntimeError(format!( + "Mismatched parameter type: {self} with {val:?}" + ))), } } - } #[derive(Debug, Clone, PartialEq)] @@ -228,9 +245,7 @@ pub enum Param { Void, } - impl Param { - /// Constructs a Param from a Wasmtime Val and type id. #[cfg(feature = "wasm")] pub fn from_wasm_type_val( @@ -241,7 +256,7 @@ impl Param { caller: &wasmtime::Store, ) -> Self { use crate::engine::wasm_engine::get_wasm_string; - + match typ { DataType::I8 => Param::I8(val.unwrap_i32() as i8), DataType::I16 => Param::I16(val.unwrap_i32() as i16), @@ -256,7 +271,6 @@ impl Param { DataType::Bool => Param::Bool(val.unwrap_i32() != 0), // allocated externally, we copy the string DataType::ExtString => { - let ptr = val.unwrap_i32() as u32; let st = get_wasm_string(ptr, memory.data(caller)); Param::String(st) @@ -266,7 +280,8 @@ impl Param { let op = val.unwrap_i64() as u64; let key = OpaquePointerKey::from(KeyData::from_ffi(op)); - let real = data.read() + let real = data + .read() .opaque_pointers .get(key) .copied() @@ -288,7 +303,7 @@ impl Param { typ: DataType, val: mlua::Value, data: &Arc>, - lua: &mlua::Lua + lua: &mlua::Lua, ) -> Self { match typ { DataType::I8 => Param::I8(val.as_integer().unwrap() as i8), @@ -310,7 +325,8 @@ impl Param { let op = table.get("opaqu").unwrap(); let key = OpaquePointerKey::from(KeyData::from_ffi(op)); - let real = data.read() + let real = data + .read() .opaque_pointers .get(key) .copied() @@ -323,14 +339,61 @@ impl Param { } } - pub fn to_rs_param(self) -> FfiParam { self.to_param_inner(DataType::RustString, DataType::RustError) } pub fn to_ext_param(self) -> FfiParam { self.to_param_inner(DataType::ExtString, DataType::ExtError) } - + + pub fn to_serde( + self, + data: &Arc>, + ) -> Result { + Ok(match self { + Param::I8(i) => serde_json::Value::from(i), + Param::I16(i) => serde_json::Value::from(i), + Param::I32(i) => serde_json::Value::from(i), + Param::I64(i) => serde_json::Value::from(i), + Param::U8(u) => serde_json::Value::from(u), + Param::U16(u) => serde_json::Value::from(u), + Param::U32(u) => serde_json::Value::from(u), + Param::U64(u) => serde_json::Value::from(u), + Param::F32(f) => serde_json::Value::from(f), + Param::F64(f) => serde_json::Value::from(f), + Param::Bool(b) => serde_json::Value::from(b), + Param::String(s) => serde_json::Value::from(s), + Param::Void => serde_json::Value::Null, + Param::Object(ptr) => { + let mut s = data.write(); + let key = s.get_opaque_pointer(ptr.into()); + serde_json::Value::from(key.0.as_ffi()) + } + Param::Error(e) => return Err(anyhow!("{}", e)), + }) + } + + pub fn from_serde(value: serde_json::Value) -> Self { + match value { + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Param::I64(i) + } else if let Some(u) = n.as_u64() { + Param::U64(u) + } else if let Some(f) = n.as_f64() { + Param::F64(f) + } else { + Param::Error("Invalid number".to_string()) + } + } + serde_json::Value::String(s) => Param::String(s), + serde_json::Value::Bool(b) => Param::Bool(b), + serde_json::Value::Null => Param::Void, + _ => Param::Error("Unsupported return type".to_string()), + } + } + + #[rustfmt::skip] fn to_param_inner(self, str_type: DataType, err_type: DataType) -> FfiParam { match self { @@ -356,7 +419,6 @@ impl Param { pub fn to_result(self) -> Result { T::from_param(self) } - } pub trait FromParam: Sized { @@ -367,7 +429,7 @@ macro_rules! deref_param { match $param { Param::$case(v) => Ok(v), Param::Error(e) => Err(anyhow!("{}", e)), - _ => Err(anyhow!("Incorrect data type")) + _ => Err(anyhow!("Incorrect data type")), } }; ( $tp:ty => $case:tt ) => { @@ -376,7 +438,7 @@ macro_rules! deref_param { deref_param!(param, $case) } } - } + }; } deref_param! { i8 => I8 } deref_param! { i16 => I16 } @@ -395,7 +457,7 @@ impl FromParam for () { match param { Param::Void => Ok(()), Param::Error(e) => Err(anyhow!("{}", e)), - _ => Err(anyhow!("Incorrect data type")) + _ => Err(anyhow!("Incorrect data type")), } } } @@ -441,14 +503,19 @@ impl Params { /// Converts the Params into a vector of Wasmtime Val types for function calling. #[cfg(feature = "wasm")] - pub fn to_wasm_args(self, data: &Arc>) -> Result> { + pub fn to_wasm_args( + self, + data: &Arc>, + ) -> Result> { // Acquire a single write lock for the duration of conversion to avoid // repeated locking/unlocking when pushing strings or registering objects. use wasmtime::Val; let mut s = data.write(); - let vals = self.params.into_iter().map(|p| - match p { + + self.params + .into_iter() + .map(|p| match p { Param::I8(i) => Ok(Val::I32(i as i32)), Param::I16(i) => Ok(Val::I32(i as i32)), Param::I32(i) => Ok(Val::I32(i)), @@ -475,21 +542,23 @@ impl Params { Val::I64(op.0.as_ffi() as i64) }) } - Param::Error(st) => { - Err(anyhow!("{st}")) - } + Param::Error(st) => Err(anyhow!("{st}")), _ => unreachable!("Void shouldn't ever be added as an arg"), - } - ).collect(); - - vals + }) + .collect() } #[cfg(feature = "lua")] - pub fn to_lua_args(self, lua: &mlua::Lua, data: &Arc>) -> Result { + pub fn to_lua_args( + self, + lua: &mlua::Lua, + data: &Arc>, + ) -> Result { let mut s = data.write(); - let vals = self.params.into_iter().map(|p| - match p { + let vals = self + .params + .into_iter() + .map(|p| match p { Param::I8(i) => Ok(mlua::Value::Integer(i as i64)), Param::I16(i) => Ok(mlua::Value::Integer(i as i64)), Param::I32(i) => Ok(mlua::Value::Integer(i as i64)), @@ -512,21 +581,51 @@ impl Params { mlua::Value::Integer(op.0.as_ffi() as i64) }) } - Param::Error(st) => { - Err(anyhow!("{st}")) - } - _ => unreachable!("Void shouldn't ever be added as an arg") - } - ).collect::>>()?; + Param::Error(st) => Err(anyhow!("{st}")), + _ => unreachable!("Void shouldn't ever be added as an arg"), + }) + .collect::>>()?; Ok(mlua::MultiValue::from_vec(vals)) } - pub fn to_ffi(self) -> FfiParams where Ext: ExternalFunctions { + pub fn to_ffi(self) -> FfiParams + where + Ext: ExternalFunctions, + { FfiParams::from_params(self.params) } } +impl Deref for Params { + type Target = SmallVec<[Param; 4]>; + + fn deref(&self) -> &Self::Target { + &self.params + } +} + +impl DerefMut for Params { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.params + } +} + +impl IntoIterator for Params { + type Item = Param; + type IntoIter = smallvec::IntoIter<[Param; 4]>; + + fn into_iter(self) -> Self::IntoIter { + self.params.into_iter() + } +} + +impl FromIterator for Params { + fn from_iter>(iter: T) -> Self { + Self { params: iter.into_iter().collect() } + } +} + /// C repr of ffi data #[repr(C)] pub union RawParam { @@ -563,8 +662,10 @@ pub struct FfiParams { marker: PhantomData, } - -impl Drop for FfiParams where Ext: ExternalFunctions { +impl Drop for FfiParams +where + Ext: ExternalFunctions, +{ fn drop(&mut self) { if self.params.is_empty() { return; @@ -583,21 +684,36 @@ impl Drop for FfiParams where Ext: ExternalFunctions { } } -impl Default for FfiParams where Ext: ExternalFunctions { +impl Default for FfiParams +where + Ext: ExternalFunctions, +{ fn default() -> Self { Self::empty() } } -impl FfiParams where Ext: ExternalFunctions { +impl FfiParams +where + Ext: ExternalFunctions, +{ pub fn empty() -> Self { - Self { params: SmallVec::new(), marker: PhantomData } + Self { + params: SmallVec::new(), + marker: PhantomData, + } } /// Creates FfiParams from a vector of Params. - pub fn from_params(params: T) -> Self where T: IntoIterator { + pub fn from_params(params: T) -> Self + where + T: IntoIterator, + { let ffi_params = params.into_iter().map(|p| p.to_rs_param()).collect(); - Self { params: ffi_params, marker: PhantomData } + Self { + params: ffi_params, + marker: PhantomData, + } } /// Creates FfiParams from an FfiParamArray with 'static lifetime. @@ -606,14 +722,15 @@ impl FfiParams where Ext: ExternalFunctions { return Ok(Self::default()); } unsafe { - let raw_vec = - std::ptr::slice_from_raw_parts_mut(array.ptr as *mut FfiParam, array.count as usize); + let raw_vec = std::ptr::slice_from_raw_parts_mut( + array.ptr as *mut FfiParam, + array.count as usize, + ); let raw_vec = Box::from_raw(raw_vec); // take ownership of the raw_vec let owned = raw_vec.into_vec(); - Ok(Self { params: SmallVec::from_vec(owned), marker: PhantomData, @@ -641,7 +758,7 @@ impl FfiParams where Ext: ExternalFunctions { } /// Leaks the FfiParams into an FfiParamArray with 'static lifetime. - /// Caller is responsible for freeing the memory. + /// Caller is responsible for freeing the memory. /// Freeing is possible by converting back via FfiParams::from_ffi_array and dropping the FfiParams. pub fn leak(mut self) -> FfiParamArray<'static> { let boxed_slice = mem::take(&mut self.params).into_boxed_slice(); @@ -689,7 +806,8 @@ impl<'a> FfiParamArray<'a> { std::ptr::slice_from_raw_parts(self.ptr as *mut FfiParam, self.count as usize); let slice = &*raw_slice; - let result = slice.iter() + let result = slice + .iter() .map(|p| p.as_param::()) .collect::>()?; Ok(Params { params: result }) @@ -769,7 +887,6 @@ impl FfiParam { DataType::Void => Param::Void, }) } - } impl From for FfiParam { @@ -777,4 +894,3 @@ impl From for FfiParam { value.to_rs_param() } } - diff --git a/turing/src/lib.rs b/turing/src/lib.rs index 220fae6..6e2933e 100644 --- a/turing/src/lib.rs +++ b/turing/src/lib.rs @@ -152,6 +152,15 @@ impl Turing { lua_interpreter.load_script(source)?; self.engine = Some(Engine::Lua(lua_interpreter)); } + #[cfg(feature = "deno")] + "js" | "ts" => { + let mut deno_engine = engine::deno_engine::DenoEngine::new( + &self.script_fns, + Arc::clone(&self.data), + )?; + deno_engine.load_script(source)?; + self.engine = Some(Engine::Deno(deno_engine)); + } _ => { return Err(anyhow!( "Unknown script extension: '{extension:?}' must be .wasm or .lua" diff --git a/turing/tests/deno_integration_test.rs b/turing/tests/deno_integration_test.rs new file mode 100644 index 0000000..77d8c19 --- /dev/null +++ b/turing/tests/deno_integration_test.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; +use parking_lot::RwLock; +use rustc_hash::FxHashMap; +use turing::{Turing, TuringSetup}; +use turing::engine::types::ScriptFnMetadata; +use turing::interop::params::{DataType, Params, Param}; + +#[test] +fn deno_basic_call() { + // This test only checks that loading and calling a deno script compiles and runs + // in minimal fashion. It will skip if the deno feature is not enabled. + #[cfg(not(feature = "deno"))] + { + eprintln!("deno feature not enabled; skipping test"); + return; + } + + // build a Turing instance with a minimal dummy `ExternalFunctions` impl + struct TestExt; + impl turing::ExternalFunctions for TestExt { + fn abort(_error_type: String, _error: String) -> ! { panic!("abort") } + fn log_info(_msg: impl ToString) {} + fn log_warn(_msg: impl ToString) {} + fn log_debug(_msg: impl ToString) {} + fn log_critical(_msg: impl ToString) {} + fn free_string(_ptr: *const std::os::raw::c_char) {} + } + + let setup: TuringSetup = Turing::new(); + let mut t = setup.build().unwrap(); + + // load the test deno script + let script_path = std::path::Path::new("tests/deno_test.js"); + // create engine directly + let data = Arc::new(RwLock::new(turing::EngineDataState::default())); + let script_fns: FxHashMap = FxHashMap::default(); + + // load script via the public Turing API and call functions + #[cfg(feature = "deno")] + { + t.load_script(script_path.to_string_lossy(), &["deno"]).unwrap(); + + // call add(2,3) via Turing API + let mut params = Params::of_size(2); + params.push(Param::I32(2)); + params.push(Param::I32(3)); + let ret = t.call_fn("add", params, DataType::I32); + match ret { + Param::I32(v) => assert_eq!(v, 5), + Param::I64(v) => assert_eq!(v as i32, 5), + _ => panic!("unexpected return type: {:?}", ret), + } + + // call makeOpaque(42) — the JS helper returns a numeric id in this test + let mut p2 = Params::of_size(1); + p2.push(Param::I64(42)); + let ret2 = t.call_fn("makeOpaque", p2, DataType::I64); + match ret2 { + Param::I64(v) => assert_eq!(v, 42), + Param::U64(u) => assert_eq!(u as i64, 42), + _ => panic!("expected numeric id from makeOpaque, got {:?}", ret2), + } + } +} diff --git a/turing/tests/deno_test.js b/turing/tests/deno_test.js new file mode 100644 index 0000000..367ff88 --- /dev/null +++ b/turing/tests/deno_test.js @@ -0,0 +1,13 @@ +// Minimal Deno test script used by Rust integration test +// Expose a simple function that returns a number and an object id +function add(a, b) { + return a + b; +} + +function makeOpaque(id) { + // return numeric id representing opaque pointer + return id; +} + +globalThis.add = add; +globalThis.makeOpaque = makeOpaque; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index d1fbe34..db08637 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,11 +1,11 @@ -use std::{env, fs}; +use serde::Deserialize; use std::path::Path; use std::process::Command; -use serde::Deserialize; +use std::{env, fs}; #[derive(Deserialize)] struct CargoToml { - package: Package + package: Package, } #[derive(Deserialize)] @@ -22,79 +22,76 @@ fn main() { }); match task.as_str() { - "win-build" | "w" => build_windows(), + "build" => { + build(None); + } + "win-build" | "w" => build(Some("x86_64-pc-windows-gnu")), "test-run" | "t" => test_run(), unknown => { eprintln!("Unknown task: {}", unknown); std::process::exit(1); } } - - } -fn compile_package(target: &str, crate_name: &str, mode: &str) { - +fn compile_package(target: Option<&str>, crate_name: &str, mode: &str) { let cargo_bin = env::var("CARGO").unwrap_or("cargo".to_string()); let mut status = Command::new(cargo_bin); if mode == "--debug" { - status.args(["build", "--target", target, "-p", crate_name]); + status.args(["build", "-p", crate_name]); } else { - status.args(["build", mode, "--target", target, "-p", crate_name]); + status.args(["build", mode, "-p", crate_name]); } - let status = status.status() - .expect("Failed to build Turing"); + if let Some(t) = target { + status.args(["--target", t]); + } + // Ensure V8 is built monolithically for shared-library compatibility + status.env("V8_FROM_SOURCE", "1"); + status.env("PRINT_GN_ARGS", "1"); + status.env( + "GN_ARGS", + "v8_monolithic=true v8_monolithic_for_shared_library=true", + ); + let status = status.status().expect("Failed to build Turing"); if !status.success() { eprintln!("Failed to compile {} crate", crate_name); std::process::exit(1); } - } -fn build_windows() { - let target = "x86_64-pc-windows-gnu"; +fn build(target: Option<&str>) { let crate_name = "turing"; - compile_package( - target, - crate_name, - "--release" - ); + compile_package(target, crate_name, "--release"); let raw_cargo = fs::read_to_string(format!("{}/Cargo.toml", crate_name)) .expect("Failed to read Cargo.toml"); - let cargo: CargoToml = toml::from_str(&raw_cargo) - .expect("Failed to parse Cargo.toml"); + let cargo: CargoToml = toml::from_str(&raw_cargo).expect("Failed to parse Cargo.toml"); let version = cargo.package.version; let lib_name = cargo.package.name; - let built = format!("target/{}/release/{}.dll", target, lib_name); + let built = format!("target/{}/release/{}.dll", target.unwrap_or(&env::var("TARGET").unwrap()), lib_name); let output = Path::new("dist").join(format!("{}-{}.dll", lib_name, version)); fs::create_dir_all("dist").expect("Failed to create dist directory"); - fs::copy(&built, &output) - .unwrap_or_else(|e| panic!("Failed to copy DLL: {}", e)); + fs::copy(&built, &output).unwrap_or_else(|e| panic!("Failed to copy DLL: {}", e)); println!("Windows dll generated in dist"); - } fn test_run() { - compile_package( - "wasm32-wasip1", - "wasm_tests", - "--debug" - ); + compile_package(Some("wasm32-wasip1"), "wasm_tests", "--debug"); let _ = fs::remove_file("tests/wasm/wasm_tests.wasm"); fs::copy( "target/wasm32-wasip1/debug/wasm_tests.wasm", - "tests/wasm/wasm_tests.wasm" - ).unwrap_or_else(|e| panic!("Failed to copy wasm file for testing: {}", e)); + "tests/wasm/wasm_tests.wasm", + ) + .unwrap_or_else(|e| panic!("Failed to copy wasm file for testing: {}", e)); println!("Copied wasm test script to tests/wasm, running tests..."); @@ -102,12 +99,16 @@ fn test_run() { let status = Command::new(cargo_bin) .args(["test", "-p", "turing", "--", "--nocapture"]) + .env("V8_FROM_SOURCE", "1") + .env("PRINT_GN_ARGS", "1") + .env( + "GN_ARGS", + "v8_monolithic=true v8_monolithic_for_shared_library=true", + ) .status() .expect("Failed to run tests"); if !status.success() { println!("Turing tests failed to run") } - } -