diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 66bfe31..13e0305 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -4,8 +4,8 @@ use std::{fs, io}; unsafe extern "C" { - fn _log_info(msg: *const c_char); - fn _fetch_string() -> u32; + fn _test_log__info(msg: *const c_char); + fn _test_fetch_string() -> u32; fn _host_strcpy(location: u32, size: u32) -> u32; } @@ -13,7 +13,7 @@ macro_rules! println { ( $( $tok:expr ),* ) => { { let s = CString::new(format!($($tok),*)).unwrap(); - unsafe { _log_info(s.as_ptr()) } + unsafe { _test_log__info(s.as_ptr()) } } }; } @@ -24,7 +24,7 @@ extern "C" fn on_load() { let s = CString::new("log info from wasm!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!").unwrap(); let ptr = s.into_raw(); - _log_info(ptr); + _test_log__info(ptr); } } @@ -33,7 +33,7 @@ extern "C" fn file_access_test() { let current_path = Path::new(env!("CARGO_MANIFEST_DIR")); let readme = current_path.parent().unwrap().join("README.md"); - let bytes = fs::read(readme).unwrap(); + let bytes = fs::read(readme).expect("Failed to read README.md"); let content = String::from_utf8(bytes); @@ -57,7 +57,7 @@ extern "C" fn test_stdin_fail() { #[unsafe(no_mangle)] extern "C" fn test_string_fetch() { - let sz = unsafe { _fetch_string() }; + let sz = unsafe { _test_fetch_string() }; let mut turing_str = vec![0; sz as usize]; unsafe { _host_strcpy(turing_str.as_mut_ptr() as u32, sz) }; let turing_str = unsafe { CStr::from_ptr(turing_str.as_ptr() as *const c_char) }; @@ -65,3 +65,8 @@ extern "C" fn test_string_fetch() { println!("Received string from host: '{}'", string) } + +#[unsafe(no_mangle)] +extern "C" fn test_panic() { + panic!("This is a panic from within wasm!"); +} \ No newline at end of file diff --git a/tests/wasm/wasm_tests.wasm b/tests/wasm/wasm_tests.wasm index 6a06a95..42676b5 100755 Binary files a/tests/wasm/wasm_tests.wasm and b/tests/wasm/wasm_tests.wasm differ diff --git a/turing/benches/wasm_api_bench.rs b/turing/benches/wasm_api_bench.rs index 8b035f3..8df5367 100644 --- a/turing/benches/wasm_api_bench.rs +++ b/turing/benches/wasm_api_bench.rs @@ -46,7 +46,7 @@ fn setup_turing_with_callbacks() -> Turing { let mut meta = ScriptFnMetadata::new("test".to_owned(), log_info_wasm, None); let _ = meta.add_param_type(DataType::RustString, "msg"); - turing.add_function("log_info", meta).unwrap(); + turing.add_function("log::info", meta).unwrap(); let mut meta = ScriptFnMetadata::new("test".to_owned(), fetch_string, None); let _ = meta.add_return_type(DataType::ExtString); diff --git a/turing/src/engine/lua_engine.rs b/turing/src/engine/lua_engine.rs index 3943e3b..7ea98c2 100644 --- a/turing/src/engine/lua_engine.rs +++ b/turing/src/engine/lua_engine.rs @@ -272,7 +272,7 @@ impl LuaInterpreter { fn bind_lua(&self, api: &Table, lua: &Lua) -> Result<()> { for (name, metadata) in self.lua_fns.iter() { - if name.contains(".") { + if ScriptFnMetadata::is_instance_method(name) { let parts: Vec<&str> = name.splitn(2, ".").collect(); let cname = parts[0].to_case(Case::Pascal); let fname = parts[1].to_case(Case::Snake); @@ -281,7 +281,7 @@ impl LuaInterpreter { let Ok(table) = api.raw_get::(cname.as_str()) else { return Err(anyhow!("table['{cname}'] is not a table")) }; self.generate_function(lua, &table, fname.as_str(), metadata)?; - } else if name.contains(":") { + } else if ScriptFnMetadata::is_static_method(name) { let parts: Vec<&str> = name.splitn(2, ":").collect(); let cname = parts[0].to_case(Case::Pascal); let fname = parts[1].to_case(Case::Snake); diff --git a/turing/src/engine/types.rs b/turing/src/engine/types.rs index 0f8b3a9..1b127d5 100644 --- a/turing/src/engine/types.rs +++ b/turing/src/engine/types.rs @@ -4,11 +4,14 @@ use convert_case::{Case, Casing}; pub type ScriptCallback = extern "C" fn(FfiParamArray) -> FfiParam; +// Represents the name of a type used in parameter or return type lists +pub type DataTypeName = String; + #[derive(Clone)] pub struct ScriptFnParameter { pub name: String, pub data_type: DataType, - pub data_type_name: String, + pub data_type_name: DataTypeName, } #[derive(Clone)] @@ -16,7 +19,7 @@ pub struct ScriptFnMetadata { pub capability: String, pub callback: ScriptCallback, pub param_types: Vec, - pub return_type: Vec<(DataType, String)>, + pub return_type: Vec<(DataType, DataTypeName)>, pub doc_comment: Option, } @@ -101,7 +104,7 @@ impl ScriptFnMetadata { /// Determines if function is a static method pub fn is_static_method(fn_name: &str) -> bool { - fn_name.contains("::") + !Self::is_instance_method(fn_name) && fn_name.contains("::") } /// Converts function name to internal representation diff --git a/turing/src/engine/wasm_engine.rs b/turing/src/engine/wasm_engine.rs index cbe3a0c..e34b722 100644 --- a/turing/src/engine/wasm_engine.rs +++ b/turing/src/engine/wasm_engine.rs @@ -5,7 +5,7 @@ use std::path::Path; use std::sync::Arc; use std::task::Poll; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow, bail}; use convert_case::{Case, Casing}; use parking_lot::RwLock; use rustc_hash::FxHashMap; @@ -159,7 +159,7 @@ impl Param { DataType::RustError | DataType::ExtError => { let ptr = val.unwrap_i32() as u32; let st = get_wasm_string(ptr, memory.data(caller)); - Param::Error(st) + Param::Error(format!("WASM Error: {}", st)) } DataType::Void => Param::Void, @@ -201,14 +201,14 @@ impl Param { s.str_cache.push_back(st); Val::I32(l as i32) } + Param::Error(er) => { + return Err(anyhow!("Error executing host function: {}", er)); + } Param::Object(pointer) => { let pointer = ExtPointer::from(pointer); let opaque = s.get_opaque_pointer(pointer); Val::I64(opaque.0.as_ffi() as i64) } - Param::Error(er) => { - return Err(anyhow!("Error executing C# function: {}", er)); - } Param::Void => return Ok(None), Param::Vec2(v) => enqueue!(v; 2), Param::Vec3(v) => enqueue!(v; 3), @@ -293,8 +293,7 @@ pub struct WasmInterpreter { script_instance: Option, memory: Option, - func_cache: KeyVec, - typed_cache: FxHashMap, + func_cache: KeyVec)>, fast_calls: FastCalls, pub api_versions: FxHashMap, @@ -331,64 +330,64 @@ enum TypedFuncEntry { } impl TypedFuncEntry { - fn invoke(&self, store: &mut Store, args: Params) -> Result { + fn invoke(&self, store: &mut Store, args: Params) -> Result { match self { - TypedFuncEntry::NoParamsVoid(t) => t.call(store, ()).map(|_| Param::Void).map_err(|e| e.to_string()), - TypedFuncEntry::NoParamsI32(t) => t.call(store, ()).map(Param::I32).map_err(|e| e.to_string()), - TypedFuncEntry::NoParamsI64(t) => t.call(store, ()).map(Param::I64).map_err(|e| e.to_string()), - TypedFuncEntry::NoParamsF32(t) => t.call(store, ()).map(Param::F32).map_err(|e| e.to_string()), - TypedFuncEntry::NoParamsF64(t) => t.call(store, ()).map(Param::F64).map_err(|e| e.to_string()), + TypedFuncEntry::NoParamsVoid(t) => t.call(store, ()).map(|_| Param::Void), + TypedFuncEntry::NoParamsI32(t) => t.call(store, ()).map(Param::I32), + TypedFuncEntry::NoParamsI64(t) => t.call(store, ()).map(Param::I64), + TypedFuncEntry::NoParamsF32(t) => t.call(store, ()).map(Param::F32), + TypedFuncEntry::NoParamsF64(t) => t.call(store, ()).map(Param::F64), TypedFuncEntry::I32ToI32(t) => { - if args.len() != 1 { return Err("Arg mismatch".to_string()) } + if args.len() != 1 { bail!("Arg mismatch") } let a0 = match &args[0] { Param::I32(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; - t.call(store, a0).map(Param::I32).map_err(|e| e.to_string()) + t.call(store, a0).map(Param::I32) } TypedFuncEntry::I64ToI64(t) => { - if args.len() != 1 { return Err("Arg mismatch".to_string()) } + if args.len() != 1 { bail!("Arg mismatch") } let a0 = match &args[0] { Param::I64(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; - t.call(store, a0).map(Param::I64).map_err(|e| e.to_string()) + t.call(store, a0).map(Param::I64) } TypedFuncEntry::F32ToF32(t) => { - if args.len() != 1 { return Err("Arg mismatch".to_string()) } + if args.len() != 1 { bail!("Arg mismatch") } let a0 = match &args[0] { Param::F32(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; - t.call(store, a0).map(Param::F32).map_err(|e| e.to_string()) + t.call(store, a0).map(Param::F32) } TypedFuncEntry::F32ToVoid(typed_func) => { - if args.len() != 1 { return Err("Arg mismatch".to_string()) } + if args.len() != 1 { bail!("Arg mismatch") } let a0 = match &args[0] { Param::F32(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; - typed_func.call(store, a0).map(|_| Param::Void).map_err(|e| e.to_string()) + typed_func.call(store, a0).map(|_| Param::Void) }, TypedFuncEntry::F64ToF64(t) => { - if args.len() != 1 { return Err("Arg mismatch".to_string()) } + if args.len() != 1 { bail!("Arg mismatch") } let a0 = match &args[0] { Param::F64(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; - t.call(store, a0).map(Param::F64).map_err(|e| e.to_string()) + t.call(store, a0).map(Param::F64) } TypedFuncEntry::I32I32ToI32(t) => { - if args.len() != 2 { return Err("Arg mismatch".to_string()) } + if args.len() != 2 { bail!("Arg mismatch") } let a0 = match &args[0] { Param::I32(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; let a1 = match &args[1] { Param::I32(v) => *v, - _ => return Err("Arg conversion".to_string()), + _ => bail!("Arg conversion"), }; - t.call(store, (a0, a1)).map(Param::I32).map_err(|e| e.to_string()) + t.call(store, (a0, a1)).map(Param::I32) } } @@ -556,7 +555,6 @@ impl WasmInterpreter { script_instance: None, memory: None, func_cache: Default::default(), - typed_cache: Default::default(), fast_calls: FastCalls::default(), api_versions: Default::default(), _ext: PhantomData @@ -609,26 +607,27 @@ impl WasmInterpreter { for (name, metadata) in wasm_fns.iter() { // Convert from `ClassName::functionName` to `_class_name_function_name` - let mut internal_name = name.replace("::", "__").replace(".", "__").to_case(Case::Snake); - internal_name.insert(0, '_'); + let internal_name = metadata.as_internal_name(name); - let mut p_types = metadata.param_types.iter().map(|d| d.data_type.to_val_type()).collect::>>()?; + let mut param_types = metadata.param_types.iter().map(|d| d.data_type).collect::>(); if ScriptFnMetadata::is_instance_method(name) { // instance methods get an extra first parameter for the instance pointer - p_types.insert(0, DataType::Object.to_val_type().unwrap()); + param_types.insert(0, DataType::Object); } + let param_wasm_types = param_types.iter().map(|d| d.to_val_type()).collect::>>()?; + // if the only return type is void, we treat it as no return types let r_types = if metadata.return_type.len() == 1 && metadata.return_type.first().cloned().map(|r| r.0) == Some(DataType::Void) { Vec::new() } else { metadata.return_type.iter().map(|d| d.0.to_val_type()).collect::>>()? }; - let ft = FuncType::new(engine, p_types, r_types); + let ft = FuncType::new(engine, param_wasm_types, r_types); let cap = metadata.capability.clone(); let callback = metadata.callback; - let pts = metadata.param_types.iter().map(|d| d.data_type).collect::>(); + let data2 = Arc::clone(&data); @@ -636,10 +635,15 @@ impl WasmInterpreter { linker.func_new( "env", - internal_name.as_str(), + internal_name.clone().as_str(), ft, move |caller, ps, rs| { - wasm_bind_env::(&data2, caller, &cap, ps, rs, pts.as_slice(), &callback) + wasm_bind_env::(&data2, caller, &cap, ps, rs, param_types.as_slice(), &callback) + // we handle Error returns specially by logging them + // since wasmtime doesn't propagate them with their messages + .inspect_err(|e| { + Ext::log_debug(format!("WASM function {internal_name} threw error: {e}")); + }) } )?; @@ -664,24 +668,20 @@ impl WasmInterpreter { self.memory = Some(memory); // clear any previous function cache and cache exports lazily self.func_cache.clear(); - self.typed_cache.clear(); // Pre-create typed wrappers for exported functions where possible to avoid first-call overhead. // Try a small set of common signatures and cache the TypedFunc if creation succeeds. for export in module.exports() { + let name = export.name(); let Some(func) = instance.get_func(&mut self.store, name) else { continue }; - // get or insert into func cache - let key = self.func_cache.key_of(|x| x.0 == name).unwrap_or_else(||{ - self.func_cache.push((name.to_string(), func)) - }); - - - if let Some(entry) = TypedFuncEntry::from_func(&mut self.store, func) { - self.typed_cache.insert(key, entry); + // ensure no duplicates + if self.func_cache.key_of(|x| x.0 == name).is_some() { + return Err(anyhow!("Duplicate exported function name in wasm module: {}", name)); } + self.func_cache.push((name.to_string(), func, TypedFuncEntry::from_func(&mut self.store, func))); if name == "on_update" { let Ok(f) = func.typed::(&mut self.store) else { continue }; @@ -714,19 +714,19 @@ impl WasmInterpreter { ret_type: DataType, data: &Arc>, ) -> Param { - // Fast-path: typed cache (common signatures). Falls back to dynamic call below. - if let Some(entry) = self.typed_cache.get(&cache_key) { - return entry.invoke(&mut self.store, params).unwrap_or_else(Param::Error) - } - // Try cache first to avoid repeated name lookup and Val boxing/unboxing. // This shouldn't be necessary as all exported functions are indexed on load - let (_, f) = self.func_cache.get(&cache_key); - + let (f_name, f, typed) = self.func_cache.get(&cache_key); + + // Fast-path: typed cache (common signatures). Falls back to dynamic call below. + if let Some(typed) = typed { + return typed.invoke(&mut self.store, params) + .unwrap_or_else(|e| Param::Error(format!("Error calling wasm function typed: {e}"))); + } let args = params.to_wasm_args(data); if let Err(e) = args { - return Param::Error(format!("{e}")) + return Param::Error(format!("Params error: {e}")) } let args = args.unwrap(); @@ -740,8 +740,10 @@ impl WasmInterpreter { _ => SmallVec::from_buf([Val::I32(0)]), }; + // this are errors raised by wasm execution + // e.g. stack overflow, out of bounds memory access, etc. if let Err(e) = f.call(&mut self.store, &args, &mut res) { - return Param::Error(e.to_string()); + return Param::Error(format!("Error calling wasm function: {}\n{}", f_name, e)); } // Return void quickly if res.is_empty() { @@ -755,6 +757,7 @@ impl WasmInterpreter { }; // convert Val to Param + // if an error is returned from wasm, convert to Param::Error Param::from_wasm_type_val(ret_type, rt, data, memory, &self.store) } @@ -784,7 +787,8 @@ impl WasmInterpreter { } - +/// Wraps a call from wasm into the host environment, checking capability availability +/// and converting parameters and return values as needed. fn wasm_bind_env( data: &Arc>, mut caller: Caller<'_, WasiP1Ctx>, @@ -794,8 +798,8 @@ fn wasm_bind_env( p: &[DataType], func: &ScriptCallback, ) -> Result<()> { - if !data.read().active_capabilities.contains(cap) { + Ext::log_critical(format!("Attempted to call mod capability '{}' which is not currently loaded", cap)); return Err(anyhow!("Mod capability '{}' is not currently loaded", cap)) } @@ -805,6 +809,7 @@ fn wasm_bind_env( params.push(exp_typ.to_wasm_val_param(value, &mut caller, data)?) } + let ffi_params= params.to_ffi::(); let ffi_params_struct = ffi_params.as_ffi_array(); @@ -812,11 +817,15 @@ fn wasm_bind_env( let res = func(ffi_params_struct).into_param::()?; // Convert Param back to Val for return + // TODO: Add mechanism for providing error messages to caller + + let Some(rv) = res.into_wasm_val(data)? else { return Ok(()) }; rs[0] = rv; + Ok(()) } diff --git a/turing/src/global_ffi/ffi.rs b/turing/src/global_ffi/ffi.rs index e6e590d..9f4eb5d 100644 --- a/turing/src/global_ffi/ffi.rs +++ b/turing/src/global_ffi/ffi.rs @@ -229,7 +229,7 @@ unsafe extern "C" fn turing_script_load(turing: *mut TuringInstance, source: *co }; if let Err(e) = turing.load_script(source, &capabilities) { - Param::Error(format!("{}\n{}", e, e.backtrace())) + Param::Error(format!("Error loading script: {}\n{}", e, e.backtrace())) } else { Param::Void }.to_rs_param() diff --git a/turing/src/interop/params.rs b/turing/src/interop/params.rs index ad892ae..309e24e 100644 --- a/turing/src/interop/params.rs +++ b/turing/src/interop/params.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::ffi::{CStr, CString, c_char, c_void}; use std::fmt::Display; @@ -12,7 +13,7 @@ use crate::interop::types::ExtString; #[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, TryFromPrimitive)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, TryFromPrimitive, Serialize, Deserialize)] pub enum DataType { I8 = 1, I16 = 2, @@ -508,6 +509,14 @@ impl<'a> FfiParamArray<'a> { pub fn as_slice(&'a self) -> &'a [FfiParam] { unsafe { std::slice::from_raw_parts(self.ptr, self.count as usize) } } + + pub fn len(&self) -> u32 { + self.count + } + + pub fn is_empty(&self) -> bool { + self.count == 0 || self.ptr.is_null() + } } impl FfiParam { diff --git a/turing/src/interop/types.rs b/turing/src/interop/types.rs index 290c60a..7f64396 100644 --- a/turing/src/interop/types.rs +++ b/turing/src/interop/types.rs @@ -5,9 +5,11 @@ use std::hash::Hash; use std::marker::PhantomData; use std::ops::Deref; use std::ptr; +use serde::{Deserialize, Serialize}; + use crate::ExternalFunctions; -#[derive(Default, Eq, Clone, Copy)] +#[derive(Debug, Default, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Semver { pub major: u32, pub minor: u16, diff --git a/turing/src/spec_gen/generator.rs b/turing/src/spec_gen/generator.rs index 6052af6..098dd10 100644 --- a/turing/src/spec_gen/generator.rs +++ b/turing/src/spec_gen/generator.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fs; use std::path::Path; use rustc_hash::FxHashMap; -use crate::engine::types::ScriptFnMetadata; +use crate::{engine::types::ScriptFnMetadata, spec_gen::json_generator}; use anyhow::{Result, anyhow}; use convert_case::{Case, Casing}; @@ -35,6 +35,12 @@ pub fn generate_specs( fs::write(path, contents)?; } + let json = json_generator::generate_specs_json(metadata, api_versions)?; + + let json_path = output_directory.join("specs.json"); + let json_contents = serde_json::to_string_pretty(&json)?; + fs::write(json_path, json_contents)?; + Ok(()) } @@ -65,7 +71,7 @@ fn generate_spec(api: &str, ver: Semver, metadata: &FxHashMap>(); let class_name = names[0].to_case(Case::Pascal); @@ -74,9 +80,9 @@ fn generate_spec(api: &str, ver: Semver, metadata: &FxHashMap, func_name: &str, ty: FnType) -> String { + fn generate_signature(&self, func_name: &str, ty: FnType, binding: &str) -> String { let mut out = String::new(); - let binding = "_".to_string() - + &if let Some(cn) = class_name { cn.to_case(Case::Snake) + "__" } else { String::new() } - + &func_name.to_case(Case::Snake); if matches!(ty, FnType::Function) { out += "::"; @@ -149,7 +152,7 @@ impl ScriptFnMetadata { out += self.return_type.first().map_or("void", |v| &v.1); out += " : "; - out += &binding; + out += binding; out } diff --git a/turing/src/spec_gen/json_generator.rs b/turing/src/spec_gen/json_generator.rs new file mode 100644 index 0000000..3a8a5d1 --- /dev/null +++ b/turing/src/spec_gen/json_generator.rs @@ -0,0 +1,99 @@ +use convert_case::{Case, Casing}; +use rustc_hash::FxHashMap; +use serde::Serialize; + +use crate::{engine::types::{DataTypeName, ScriptFnMetadata}, interop::{params::DataType, types::Semver}}; +use anyhow::Result; + +#[derive(Debug, Serialize)] +pub struct SpecClass { + pub is_opaque: bool, + pub capability: String, + + pub functions: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SpecMethod { + pub name: String, + pub internal_name: String, + pub doc_comment: Option, + + pub return_type: DataType, + pub return_type_name: Option, + pub param_types: Vec, + + pub is_instance_method: bool, + pub is_static_method: bool, +} + +#[derive(Debug, Serialize)] +pub struct SpecParam { + pub name: String, + pub data_type_name: DataTypeName, + pub data_type: DataType, +} + +#[derive(Debug, Serialize)] +pub struct SpecMap { + pub specs: FxHashMap, + pub api_versions: FxHashMap, +} + +pub fn generate_specs_json( + metadata: &FxHashMap, + api_versions: &FxHashMap, +) -> Result { + let mut specs = FxHashMap::default(); + + for (name, data) in metadata { + let class_name; + let func_name; + let mut is_opaque = false; + + if ScriptFnMetadata::is_instance_method(name) { // methods + let names = name.splitn(2, ".").collect::>(); + class_name = names[0].to_case(Case::Pascal); + func_name = names[1].to_case(Case::Snake); + is_opaque = true; + } else if ScriptFnMetadata::is_static_method(name) { // functions + let names = name.splitn(2, "::").collect::>(); + class_name = names[0].to_case(Case::Pascal); + func_name = names[1].to_case(Case::Snake); + } else { // globals + class_name = "Global".to_string(); + func_name = name.to_case(Case::Snake); + } + + let spec_class = specs.entry(class_name.clone()).or_insert(SpecClass { + is_opaque: false, + functions: Vec::new(), + capability: data.capability.clone(), + }); + + if name.contains(".") { + spec_class.is_opaque |= is_opaque; + } + + let is_instance_method = ScriptFnMetadata::is_instance_method(name); + let is_static_method = !is_instance_method && ScriptFnMetadata::is_static_method(name); + spec_class.functions.push(SpecMethod { + name: func_name, + internal_name: data.as_internal_name(name), + doc_comment: data.doc_comment.clone(), + + is_instance_method, + is_static_method, + + return_type: data.return_type.first().map_or(DataType::Void, |v| v.0), + return_type_name: data.return_type.first().map(|v| v.1.clone()), + param_types: data.param_types.iter().map(|p| SpecParam { + name: p.name.clone(), + data_type_name: p.data_type_name.clone(), + data_type: p.data_type, + }).collect(), + }); + } + + Ok(SpecMap { specs, api_versions: api_versions.clone() }) +} diff --git a/turing/src/spec_gen/mod.rs b/turing/src/spec_gen/mod.rs index 0ddc909..4b6806a 100644 --- a/turing/src/spec_gen/mod.rs +++ b/turing/src/spec_gen/mod.rs @@ -1 +1,2 @@ -pub mod generator; \ No newline at end of file +pub mod generator; +pub mod json_generator; \ No newline at end of file diff --git a/turing/src/tests.rs b/turing/src/tests.rs index 8619cda..14eab77 100644 --- a/turing/src/tests.rs +++ b/turing/src/tests.rs @@ -61,25 +61,25 @@ extern "C" fn fetch_string(_params: FfiParamArray) -> FfiParam { Param::String("this is a host provided string!".to_string()).to_ext_param() } +extern "C" fn log_info_panic(_params: FfiParamArray) -> FfiParam { + panic!("Host panic from log_info_panic"); +} + fn common_setup_direct(source: &str) -> Result> { let mut turing = Turing::new(); - let mut metadata = ScriptFnMetadata::new( - "test".to_owned(), - log_info_wasm, - None, - ); + let mut metadata = ScriptFnMetadata::new("test".to_owned(), log_info_wasm, None); metadata.add_param_type(DataType::RustString, "msg")?; - turing.add_function("log.info", metadata)?; + turing.add_function("log::info", metadata)?; - let mut metadata = ScriptFnMetadata::new( - "test".to_owned(), - fetch_string, - None, - ); + let mut metadata = ScriptFnMetadata::new("test".to_owned(), fetch_string, None); metadata.add_return_type(DataType::ExtString)?; turing.add_function("fetch_string", metadata)?; + let mut metadata = ScriptFnMetadata::new("test".to_owned(), log_info_panic, None); + metadata.add_param_type(DataType::RustString, "msg")?; + turing.add_function("do_panic", metadata)?; + let mut turing = turing.build()?; setup_test_script(&mut turing, source)?; @@ -168,3 +168,42 @@ pub fn test_lua_string_fetch() -> Result<()> { println!("Received message from lua: '{res}'"); Ok(()) } + +#[test] +pub fn test_wasm_panic() -> Result<()> { + let mut turing = common_setup_direct(WASM_SCRIPT)?; + + let res = turing + .call_fn_by_name("test_panic", Params::new(), DataType::Void) + .to_result::<()>(); + + assert!(res.is_err()); + + let err = res.unwrap_err().to_string(); + assert!(err.contains("panic") || err.contains("unreachable") || err.contains("trap")); + Ok(()) +} + +/// Tests that a panic in a host function called from WASM is properly propagated +/// to the caller when using the DirectExt external functions. +#[test] +#[ignore] +pub fn test_host_wasm_host_panic() -> Result<()> { + let mut turing = common_setup_direct(WASM_SCRIPT)?; + + // loading the WASM will call `on_load` which calls the host `log::info` and should panic + let caps = vec!["test"]; + turing.load_script(WASM_SCRIPT, &caps)?; + + let result = turing.call_fn_by_name("test_panic", Params::new(), DataType::Void); + + let Param::Error(err) = result else { + panic!("Expected error from panic, got: {:#?}", result); + }; + + assert!( + err.contains("Host panic from log_info_panic"), + "Error did not contain expected panic message, got:\n{err}" + ); + Ok(()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ff385c3..bb690ac 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -84,6 +84,18 @@ fn build_windows() { fs::copy(&built, &output) .unwrap_or_else(|e| panic!("Failed to copy DLL: {}.dll {}", lib_name, e)); + // copy to BEAT_SABER_DIR/Libs/Native/turing_rs.dll if BEAT_SABER_DIR is set + if let Ok(beat_saber_dir) = env::var("BEAT_SABER_DIR") { + let dest_dir = Path::new(&beat_saber_dir).join("Libs").join("Native"); + fs::create_dir_all(&dest_dir).expect("Failed to create Beat Saber Libs/Native directory"); + let dest_path = dest_dir.join(format!("{lib_name}.dll")); + + + fs::copy(&built, &dest_path) + .unwrap_or_else(|e| panic!("Failed to copy DLL to Beat Saber directory: {} {}", dest_path.display(), e)); + println!("Copied dll to Beat Saber directory: {}", dest_path.display()); + } + println!("Windows dll generated in dist"); }