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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{fs, io};


unsafe extern "C" {
fn _log_info(msg: *const c_char);
fn _log__info(msg: *const c_char);
fn _fetch_string() -> u32;
fn _host_strcpy(location: u32, size: u32) -> u32;
}
Expand All @@ -13,7 +13,7 @@ macro_rules! println {
( $( $tok:expr ),* ) => {
{
let s = CString::new(format!($($tok),*)).unwrap();
unsafe { _log_info(s.as_ptr()) }
unsafe { _log__info(s.as_ptr()) }
}
};
}
Expand All @@ -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);
_log__info(ptr);
}
}

Expand All @@ -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);

Expand Down
Binary file modified tests/wasm/wasm_tests.wasm
Binary file not shown.
2 changes: 1 addition & 1 deletion turing/benches/wasm_api_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn setup_turing_with_callbacks() -> Turing<DirectExt> {

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);
Expand Down
4 changes: 2 additions & 2 deletions turing/src/engine/lua_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ impl<Ext: ExternalFunctions> LuaInterpreter<Ext> {

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);
Expand All @@ -281,7 +281,7 @@ impl<Ext: ExternalFunctions> LuaInterpreter<Ext> {

let Ok(table) = api.raw_get::<Table>(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);
Expand Down
9 changes: 6 additions & 3 deletions turing/src/engine/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ 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)]
pub struct ScriptFnMetadata {
pub capability: String,
pub callback: ScriptCallback,
pub param_types: Vec<ScriptFnParameter>,
pub return_type: Vec<(DataType, String)>,
pub return_type: Vec<(DataType, DataTypeName)>,
pub doc_comment: Option<String>,
}

Expand Down Expand Up @@ -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
Expand Down
72 changes: 38 additions & 34 deletions turing/src/engine/wasm_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -293,8 +293,7 @@ pub struct WasmInterpreter<Ext: ExternalFunctions> {
script_instance: Option<Instance>,
memory: Option<Memory>,

func_cache: KeyVec<ScriptFnKey, (String, Func)>,
typed_cache: FxHashMap<ScriptFnKey, TypedFuncEntry>,
func_cache: KeyVec<ScriptFnKey, (String, Func, Option<TypedFuncEntry>)>,

fast_calls: FastCalls,
pub api_versions: FxHashMap<String, Semver>,
Expand Down Expand Up @@ -556,7 +555,6 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {
script_instance: None,
memory: None,
func_cache: Default::default(),
typed_cache: Default::default(),
fast_calls: FastCalls::default(),
api_versions: Default::default(),
_ext: PhantomData
Expand Down Expand Up @@ -609,37 +607,38 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {
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::<Result<Vec<ValType>>>()?;
let mut param_types = metadata.param_types.iter().map(|d| d.data_type).collect::<Vec<DataType>>();

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::<Result<Vec<ValType>>>()?;

// 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::<Result<Vec<ValType>>>()?
};
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::<Vec<DataType>>();


let data2 = Arc::clone(&data);

Ext::log_debug(format!("Registered wasm function: env::{internal_name} {}", ft));

linker.func_new(
"env",
internal_name.as_str(),
internal_name.clone().as_str(),
ft,
move |caller, ps, rs| {
wasm_bind_env::<Ext>(&data2, caller, &cap, ps, rs, pts.as_slice(), &callback)
wasm_bind_env::<Ext>(&data2, caller, &cap, ps, rs, param_types.as_slice(), &callback)
}
)?;

Expand All @@ -664,24 +663,20 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {
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::<f32, ()>(&mut self.store) else { continue };
Expand Down Expand Up @@ -714,15 +709,14 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {
ret_type: DataType,
data: &Arc<RwLock<EngineDataState>>,
) -> 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(Param::Error)
}

let args = params.to_wasm_args(data);
if let Err(e) = args {
Expand All @@ -740,8 +734,10 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {
_ => 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() {
Expand All @@ -755,6 +751,7 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {
};

// 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)
}

Expand Down Expand Up @@ -784,7 +781,8 @@ impl<Ext: ExternalFunctions + Send + Sync + 'static> WasmInterpreter<Ext> {

}


/// Wraps a call from wasm into the host environment, checking capability availability
/// and converting parameters and return values as needed.
fn wasm_bind_env<Ext: ExternalFunctions>(
data: &Arc<RwLock<EngineDataState>>,
mut caller: Caller<'_, WasiP1Ctx>,
Expand All @@ -794,8 +792,8 @@ fn wasm_bind_env<Ext: ExternalFunctions>(
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))
}

Expand All @@ -805,18 +803,24 @@ fn wasm_bind_env<Ext: ExternalFunctions>(
params.push(exp_typ.to_wasm_val_param(value, &mut caller, data)?)
}


let ffi_params= params.to_ffi::<Ext>();
let ffi_params_struct = ffi_params.as_ffi_array();

// Call to C#/rust's provided callback using a clone so we can still cleanup
let res = func(ffi_params_struct).into_param::<Ext>()?;

// Convert Param back to Val for return
if let Param::Error(e) = &res {
Ext::log_debug(format!("WASM host function returning: {:?}", res));
}

let Some(rv) = res.into_wasm_val(data)? else {
return Ok(())
};
rs[0] = rv;


Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion turing/src/global_ffi/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 10 additions & 1 deletion turing/src/interop/params.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::ffi::{CStr, CString, c_char, c_void};
use std::fmt::Display;
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion turing/src/interop/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading