diff options
Diffstat (limited to 'scripts/src')
| -rw-r--r-- | scripts/src/lib.rs | 318 | ||||
| -rw-r--r-- | scripts/src/libs/allocate.rs | 76 | ||||
| -rw-r--r-- | scripts/src/libs/buffer.rs | 182 | ||||
| -rw-r--r-- | scripts/src/libs/ctype.rs | 85 | ||||
| -rw-r--r-- | scripts/src/libs/math.rs | 398 | ||||
| -rw-r--r-- | scripts/src/libs/mod.rs | 19 | ||||
| -rw-r--r-- | scripts/src/libs/system.rs | 154 | ||||
| -rw-r--r-- | scripts/src/vtable.rs | 10 | ||||
| -rw-r--r-- | scripts/src/wasm.rs | 170 |
9 files changed, 1412 insertions, 0 deletions
diff --git a/scripts/src/lib.rs b/scripts/src/lib.rs new file mode 100644 index 0000000..214001c --- /dev/null +++ b/scripts/src/lib.rs @@ -0,0 +1,318 @@ +use std::{collections::HashMap, path::Path}; + +pub use vtable::VTableScript; +use wasmtime::{Engine, IntoFunc, Linker, Module, Store}; + +pub use wasm::InvalidWasmScript; +use wasm::{WasmScript, WasmScriptState}; + +mod libs; +mod vtable; +mod wasm; + +/// A sturcture to contain and use any needed scripts. +/// +/// # Performance +/// +/// V-Table scripts are faster than WebAssembly scripts. WebAssembly scripting +/// is provided to make hot-reloading possible. Either way, scripts should be +/// reasonably fast. +/// +/// A single long WebAssembly script is typically faster than lots of small +/// scripts. Try to keep the number of scripts small. No more than twenty +/// scripts should be enabled at once. +#[derive(Default)] +pub struct ScriptManager { + script_map: HashMap<Box<str>, ScriptId>, + + vtable_scripts: Vec<Script<VTableScript>>, + wasm_scripts: Vec<Script<WasmScript>>, + + relink_every_frame: bool, + wasm_engine: Engine, + wasm_imports: Linker<WasmScriptState>, +} + +impl ScriptManager { + /// Creates a new, empty script manager + pub fn new() -> Self { + let wasm_engine = Engine::default(); + let mut wasm_imports = Linker::new(&wasm_engine); + wasm_imports.allow_shadowing(true); + + let mut this = Self { + script_map: HashMap::new(), + vtable_scripts: Vec::new(), + wasm_scripts: Vec::new(), + relink_every_frame: false, + wasm_imports, + wasm_engine, + }; + libs::add_alligator_library(&mut this); + + this + } + + /// If set to `true`, then every frame, the scripts will be relinked with + /// it's dependencies on each call. + /// + /// The main reason to do this would be to ensure that the state is reset + /// each frame. However, the link is slow, so it's not recommended to use + /// this setting for releasing a game. This is only recommended for + /// debugging. + pub fn set_relink_every_frame(&mut self, setting: bool) { + self.relink_every_frame = setting; + } + + /// Returns `true` if the scripts are set to be relinked each frame. See + /// [`set_relink_every_frame`] for more details. + pub fn relink_every_frame(&self) -> bool { + self.relink_every_frame + } + + /// Adds a script to this manager. + /// + /// By default, all scripts are disabled. To enable it, call + /// [`enable_script`]. + pub fn add_vtable_script(&mut self, name: Box<str>, script: VTableScript) -> ScriptId { + let idx = self.vtable_scripts.len(); + self.vtable_scripts.push(Script::new(script)); + let id = ScriptId { + kind: ScriptKind::VTable, + idx, + }; + self.script_map.insert(name, id); + id + } + + /// Adds a WebAssembly script to this manager. + /// + /// By default, all scripts are disabled. To enable it, call + /// [`enable_script`]. + /// + /// # Errors + /// + /// This function returns an error if it could not use the given script. + pub fn add_wasm_script( + &mut self, + name: Box<str>, + path: impl AsRef<Path>, + trusted: bool, + ) -> Result<ScriptId, InvalidWasmScript> { + let idx = self.vtable_scripts.len(); + let script = WasmScript::new( + path.as_ref(), + &self.wasm_engine, + &self.wasm_imports, + trusted, + )?; + self.wasm_scripts.push(Script::new(script)); + let id = ScriptId { + kind: ScriptKind::Wasm, + idx, + }; + self.script_map.insert(name, id); + Ok(id) + } + + /// Add a WebAssembly module which can be called by other WebAssembly + /// scripts. + /// + /// If there are two libraries with the same name, only the newest module + /// is used. This feature can be used to allow libraries to be hot + /// reloaded. + /// + /// The `trusted` parameter refers to whether this library can call + /// functions which require trust. + /// + /// # Performance + /// + /// Running this function relinks all WebAssembly scripts. For best + /// performance, all WebAssembly libraries should be added before any + /// scripts are loaded. + pub fn add_wasm_library( + &mut self, + name: &str, + path: impl AsRef<Path>, + trusted: bool, + ) -> Result<(), InvalidWasmScript> { + let module = Module::from_file(&self.wasm_engine, path)?; + let mut store = Store::new(&self.wasm_engine, WasmScriptState::new(trusted)); + let instance = self.wasm_imports.instantiate(&mut store, &module)?; + self.wasm_imports.instance(store, name, instance)?; + + for script in &mut self.wasm_scripts { + if let Err(e) = script.inner.relink(&self.wasm_engine, &self.wasm_imports) { + log::error!("{e}"); + } + } + + Ok(()) + } + + /// Reloads a WebAssembly script from disk. + pub fn reload_script(&mut self, name: &str) -> Option<Result<(), InvalidWasmScript>> { + let id = self.script_map.get(name).unwrap(); + let result = match id.kind { + ScriptKind::Wasm => self + .wasm_scripts + .get_mut(id.idx) + .unwrap() + .inner + .reload(&self.wasm_engine, &self.wasm_imports), + _ => return None, + }; + Some(result) + } + + /// Adds a library to the manager + pub fn add_library_function<Params, Args>( + &mut self, + module: &str, + name: &str, + func: impl IntoFunc<WasmScriptState, Params, Args> + Clone, + ) -> Option<()> { + self.wasm_imports.func_wrap(module, name, func).ok()?; + Some(()) + } + + /// Checks if the script with the given name is enabled + pub fn is_script_enabled(&self, name: &str) -> Option<bool> { + let id = self.script_map.get(name)?; + let enabled = match id.kind { + ScriptKind::VTable => self.vtable_scripts.get(id.idx)?.is_enabled(), + ScriptKind::Wasm => self.wasm_scripts.get(id.idx)?.is_enabled(), + }; + Some(enabled) + } + + /// Checks if the script with the given name is trusted + pub fn is_script_trusted(&self, name: &str) -> Option<bool> { + let id = self.script_map.get(name)?; + let trusted = match id.kind { + ScriptKind::VTable => true, + ScriptKind::Wasm => self.wasm_scripts.get(id.idx)?.inner.is_trusted(), + }; + Some(trusted) + } + + /// This enables the script, allowing it to run each frame. + /// + /// Calling this automatically calls the `begin` function for the script. + pub fn enable_script(&mut self, name: &str) -> Option<()> { + let id = self.script_map.get(name)?; + match id.kind { + ScriptKind::VTable => { + let script = self.vtable_scripts.get_mut(id.idx)?; + if script.is_enabled() { + // avoid calling `begin` if the script is already enabled + return Some(()); + } + + script.enable(); + script.inner.begin(); + } + ScriptKind::Wasm => { + let script = self.wasm_scripts.get_mut(id.idx)?; + if script.is_enabled() { + return Some(()); + } + + script.enable(); + if let Err(e) = script.inner.begin() { + log::error!("{}", e); + } + } + }; + Some(()) + } + + /// This disables the script, preventing it from running + pub fn disable_script(&mut self, name: &str) -> Option<()> { + let id = self.script_map.get(name)?; + match id.kind { + ScriptKind::VTable => self.vtable_scripts.get_mut(id.idx)?.disable(), + ScriptKind::Wasm => self.wasm_scripts.get_mut(id.idx)?.disable(), + }; + Some(()) + } + + /// This allows the script to run functions which require trust + pub fn trust_script(&mut self, name: &str) -> Option<()> { + let id = self.script_map.get(name)?; + if id.kind == ScriptKind::Wasm { + self.wasm_scripts.get_mut(id.idx)?.inner.trust(); + } + Some(()) + } + + /// This prevents the script from running functions which require trust + pub fn untrust_script(&mut self, name: &str) -> Option<()> { + let id = self.script_map.get(name)?; + if id.kind == ScriptKind::Wasm { + self.wasm_scripts.get_mut(id.idx)?.inner.untrust(); + } + Some(()) + } + + /// Runs the `update` function for all enabled scripts + pub fn run_update_scripts(&mut self) { + for script in &mut self.vtable_scripts { + if script.is_enabled() { + script.inner.update(); + } + } + + for script in &mut self.wasm_scripts { + if script.is_enabled() { + if self.relink_every_frame { + if let Err(e) = script.inner.relink(&self.wasm_engine, &self.wasm_imports) { + log::error!("{e}"); + } + } + if let Err(e) = script.inner.update() { + log::error!("{}", e); + } + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ScriptKind { + VTable, + Wasm, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ScriptId { + kind: ScriptKind, + idx: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Script<S> { + inner: S, + enabled: bool, +} + +impl<S> Script<S> { + fn new(inner: S) -> Self { + Self { + inner, + enabled: true, + } + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn enable(&mut self) { + self.enabled = true; + } + + fn disable(&mut self) { + self.enabled = false; + } +} diff --git a/scripts/src/libs/allocate.rs b/scripts/src/libs/allocate.rs new file mode 100644 index 0000000..7f2825d --- /dev/null +++ b/scripts/src/libs/allocate.rs @@ -0,0 +1,76 @@ +use wasmtime::Caller; + +use crate::{ScriptManager, WasmScriptState}; + +use super::{ + system::{_alloc, _get_memory}, + LIBRARY_NAME, +}; + +fn allocate_zeroed(mut caller: Caller<'_, WasmScriptState>, size: u32, align: u32) -> u32 { + let Some((ptr, data)) = _alloc(&mut caller, size, align) else { + return 0; + }; + + data.fill(0); + ptr +} + +fn reallocate( + mut caller: Caller<'_, WasmScriptState>, + ptr: u32, + size: u32, + new_size: u32, + align: u32, +) -> u32 { + if new_size < size { + return ptr; + } + + let bump_pointer = &mut caller.data_mut().bump_pointer; + if *bump_pointer == ptr + size { + *bump_pointer = ptr; + let Some((new_ptr, _)) = _alloc(&mut caller, new_size, align) else { + return 4; + }; + return new_ptr; + } + + let (_, original) = _get_memory(&mut caller); + let Some(original) = original.get(ptr as usize..(ptr + size) as usize) else { + return 4; + }; + let original = original.to_vec(); + + let Some((new_ptr, data)) = _alloc(&mut caller, new_size, align) else { + return 0; + }; + + data.get_mut(..size as usize) + .unwrap() + .clone_from_slice(&original); + new_ptr +} + +fn duplicate(mut caller: Caller<'_, WasmScriptState>, ptr: u32, size: u32, align: u32) -> u32 { + let (_, original) = _get_memory(&mut caller); + let Some(original) = original.get(ptr as usize..(ptr + size) as usize) else { + return 4; + }; + let original = original.to_vec(); + + let Some((new_ptr, data)) = _alloc(&mut caller, size, align) else { + return 0; + }; + + data.clone_from_slice(&original); + new_ptr +} + +pub fn library(manager: &mut ScriptManager) -> Option<()> { + manager.add_library_function(LIBRARY_NAME, "calloc", allocate_zeroed)?; + manager.add_library_function(LIBRARY_NAME, "realloc", reallocate)?; + manager.add_library_function(LIBRARY_NAME, "memdup", duplicate)?; + + Some(()) +} diff --git a/scripts/src/libs/buffer.rs b/scripts/src/libs/buffer.rs new file mode 100644 index 0000000..b4cee77 --- /dev/null +++ b/scripts/src/libs/buffer.rs @@ -0,0 +1,182 @@ +use wasmtime::Caller; + +use crate::{ScriptManager, WasmScriptState}; + +use super::{system::_get_memory, LIBRARY_NAME}; + +fn memory_search(mut caller: Caller<'_, WasmScriptState>, ptr: u32, ch: u32, len: u32) -> u32 { + let (_, mem_ptr) = _get_memory(&mut caller); + let len = len as usize; + let ptr = ptr as usize; + let ch = ch as u8; + if (len + ptr) > mem_ptr.len() { + return 4; + } + + for i in 0..len { + if mem_ptr[ptr + i] == ch { + return (ptr + i) as u32; + } + } + + 0 +} + +fn memory_search_last(mut caller: Caller<'_, WasmScriptState>, ptr: u32, ch: u32, len: u32) -> u32 { + let (_, mem_ptr) = _get_memory(&mut caller); + let len = len as usize; + let ptr = ptr as usize; + let ch = ch as u8; + if (len + ptr) > mem_ptr.len() { + return 4; + } + + for i in 0..len { + let index = ptr + len - i - 1; + if mem_ptr[index] == ch { + return index as u32; + } + } + + 0 +} + +fn memory_compare( + mut caller: Caller<'_, WasmScriptState>, + lhs: u32, + rhs: u32, + len: u32, +) -> (u32, u32) { + let (_, mem_ptr) = _get_memory(&mut caller); + let len = len as usize; + let lhs = lhs as usize; + let rhs = rhs as usize; + if (len + lhs) > mem_ptr.len() || (len + rhs) > mem_ptr.len() { + return (4, 0); + } + + for i in 0..len { + let diff = mem_ptr[lhs + i] - mem_ptr[rhs + i]; + if diff != 0 { + return (0, diff as u32); + } + } + + (0, 0) +} + +fn memory_fill(mut caller: Caller<'_, WasmScriptState>, dest: u32, ch: u32, len: u32) -> u32 { + let (_, mem_ptr) = _get_memory(&mut caller); + let len = len as usize; + let dest = dest as usize; + let ch = ch as u8; + + if (len + dest) > mem_ptr.len() { + return 4; + } + + for i in 0..len { + mem_ptr[dest + i] = ch; + } + + 0 +} + +fn memory_copy(mut caller: Caller<'_, WasmScriptState>, dest: u32, src: u32, len: u32) -> u32 { + let (_, mem_ptr) = _get_memory(&mut caller); + let len = len as usize; + let dest = dest as usize; + let src = src as usize; + if (len + dest) > mem_ptr.len() || (len + src) > mem_ptr.len() { + return 4; + } + + // check for overlap + if (dest < src && dest + len > src) || (src < dest && src + len > dest) { + let src = mem_ptr[src..src + len].to_vec(); + let dest = &mut mem_ptr[dest..dest + len]; + dest.clone_from_slice(&src); + } else { + for i in 0..len { + mem_ptr[dest + i] = mem_ptr[src + i]; + } + } + + 0 +} + +fn memory_copy_until( + mut caller: Caller<'_, WasmScriptState>, + dest: u32, + src: u32, + len: u32, + delimiter: u32, +) -> u32 { + let (_, mem_ptr) = _get_memory(&mut caller); + let len = len as usize; + let dest = dest as usize; + let src = src as usize; + let delimiter = delimiter as u8; + if (len + dest) > mem_ptr.len() || (len + src) > mem_ptr.len() { + return 4; + } + + // check for overlap + if (dest < src && dest + len > src) || (src < dest && src + len > dest) { + let cloned_src = mem_ptr[src..src + len].to_vec(); + let dest = &mut mem_ptr[dest..dest + len]; + for i in 0..len { + let ch = cloned_src[i]; + dest[i] = ch; + if ch == delimiter { + return (src + i + 1) as u32; + } + } + } else { + for i in 0..len { + let ch = mem_ptr[src + i]; + mem_ptr[dest + i] = ch; + if ch == delimiter { + return (src + i + 1) as u32; + } + } + } + + 0 +} + +fn memory_concatenate( + mut caller: Caller<'_, WasmScriptState>, + dest: u32, + src: u32, + dest_len: u32, + src_len: u32, +) -> u32 { + let (_, mem_ptr) = _get_memory(&mut caller); + let dest = dest as usize; + let src = src as usize; + let dest_len = dest_len as usize; + let src_len = src_len as usize; + if (dest_len + src_len + dest) > mem_ptr.len() || (src_len + src) > mem_ptr.len() { + return 4; + } + + let src = mem_ptr[src..src + src_len].to_vec(); + let dest_offset = dest + dest_len; + let dest = &mut mem_ptr[dest_offset..dest_offset + src_len]; + dest.clone_from_slice(&src); + + 0 +} + +pub fn library(manager: &mut ScriptManager) -> Option<()> { + manager.add_library_function(LIBRARY_NAME, "memchr", memory_search)?; + manager.add_library_function(LIBRARY_NAME, "memrchr", memory_search_last)?; + manager.add_library_function(LIBRARY_NAME, "memcmp", memory_compare)?; + manager.add_library_function(LIBRARY_NAME, "memset", memory_fill)?; + manager.add_library_function(LIBRARY_NAME, "memcpy", memory_copy)?; + manager.add_library_function(LIBRARY_NAME, "memccpy", memory_copy_until)?; + manager.add_library_function(LIBRARY_NAME, "memcat", memory_concatenate)?; + + Some(()) +} diff --git a/scripts/src/libs/ctype.rs b/scripts/src/libs/ctype.rs new file mode 100644 index 0000000..ca4f326 --- /dev/null +++ b/scripts/src/libs/ctype.rs @@ -0,0 +1,85 @@ +use crate::{libs::LIBRARY_NAME, ScriptManager}; + +fn char_is(func: impl Fn(char) -> bool, ch: u32) -> u32 { + let Some(ch) = char::from_u32(ch) else { + return 0; + }; + + func(ch) as u32 +} + +macro_rules! ctype { + (fn $fn_name: ident => $fn: expr;) => { + fn $fn_name(ch: u32) -> u32 { + char_is($fn, ch) + } + }; + (fn $fn_name: ident => $fn: expr; $(fn $fn_name2: ident => $fn2: expr;)*) => { + fn $fn_name(ch: u32) -> u32 { + char_is($fn, ch) + } + ctype!($(fn $fn_name2 => $fn2;)*); + }; +} + +ctype!( + fn is_alphanumeric => |ch| ch.is_alphanumeric(); + fn is_alphabetic => |ch| ch.is_alphabetic(); + fn is_lowercase => |ch| ch.is_lowercase(); + fn is_uppercase => |ch| ch.is_uppercase(); + fn is_control => |ch| ch.is_control(); + fn is_whitespace => |ch| ch.is_whitespace(); + fn is_ascii_alphanumeric => |ch| ch.is_ascii_alphanumeric(); + fn is_ascii_alphabetic => |ch| ch.is_ascii_alphabetic(); + fn is_ascii_lowercase => |ch| ch.is_ascii_lowercase(); + fn is_ascii_uppercase => |ch| ch.is_ascii_lowercase(); + fn is_ascii_digit => |ch| ch.is_ascii_digit(); + fn is_ascii_hexdigit => |ch| ch.is_ascii_hexdigit(); + fn is_ascii_control => |ch| ch.is_ascii_control(); + fn is_ascii_graphical => |ch| ch.is_ascii_graphic(); + fn is_ascii_whitespace => |ch| ch.is_ascii_whitespace(); + fn is_ascii_blank => |ch| ch == ' ' || ch == '\t'; + fn is_ascii_printable => |ch| ch.is_alphanumeric() || ch.is_ascii_punctuation(); + fn is_ascii_punctuation => |ch| ch.is_ascii_punctuation(); +); + +fn to_ascii_lower(ch: u32) -> u32 { + let Some(ch) = char::from_u32(ch) else { + return ch; + }; + + ch.to_ascii_lowercase() as u32 +} + +fn to_ascii_upper(ch: u32) -> u32 { + let Some(ch) = char::from_u32(ch) else { + return ch; + }; + + ch.to_ascii_uppercase() as u32 +} + +pub fn library(manager: &mut ScriptManager) -> Option<()> { + manager.add_library_function(LIBRARY_NAME, "iswalnum", is_alphanumeric)?; + manager.add_library_function(LIBRARY_NAME, "iswalpha", is_alphabetic)?; + manager.add_library_function(LIBRARY_NAME, "iswlower", is_lowercase)?; + manager.add_library_function(LIBRARY_NAME, "iswupper", is_uppercase)?; + manager.add_library_function(LIBRARY_NAME, "iswcntrl", is_control)?; + manager.add_library_function(LIBRARY_NAME, "iswspace", is_whitespace)?; + manager.add_library_function(LIBRARY_NAME, "isalnum", is_ascii_alphanumeric)?; + manager.add_library_function(LIBRARY_NAME, "isalpha", is_ascii_alphabetic)?; + manager.add_library_function(LIBRARY_NAME, "islower", is_ascii_lowercase)?; + manager.add_library_function(LIBRARY_NAME, "isupper", is_ascii_uppercase)?; + manager.add_library_function(LIBRARY_NAME, "isdigit", is_ascii_digit)?; + manager.add_library_function(LIBRARY_NAME, "isxdigit", is_ascii_hexdigit)?; + manager.add_library_function(LIBRARY_NAME, "iscntrl", is_ascii_control)?; + manager.add_library_function(LIBRARY_NAME, "isgraph", is_ascii_graphical)?; + manager.add_library_function(LIBRARY_NAME, "isspace", is_ascii_whitespace)?; + manager.add_library_function(LIBRARY_NAME, "isblank", is_ascii_blank)?; + manager.add_library_function(LIBRARY_NAME, "isprint", is_ascii_printable)?; + manager.add_library_function(LIBRARY_NAME, "ispunct", is_ascii_punctuation)?; + manager.add_library_function(LIBRARY_NAME, "tolower", to_ascii_lower)?; + manager.add_library_function(LIBRARY_NAME, "toupper", to_ascii_upper)?; + + Some(()) +} diff --git a/scripts/src/libs/math.rs b/scripts/src/libs/math.rs new file mode 100644 index 0000000..4aae3c3 --- /dev/null +++ b/scripts/src/libs/math.rs @@ -0,0 +1,398 @@ +use crate::ScriptManager; + +use super::LIBRARY_NAME; + +fn abs_i32(n: i32) -> i32 { + n.abs() +} + +fn abs_i64(n: i64) -> i64 { + n.abs() +} + +fn mod_f32(x: f32, y: f32) -> f32 { + libm::fmodf(x, y) +} + +fn mod_f64(x: f64, y: f64) -> f64 { + libm::fmod(x, y) +} + +fn remainder_f32(x: f32, y: f32) -> f32 { + libm::remainderf(x, y) +} + +fn remainder_f64(x: f64, y: f64) -> f64 { + libm::remainder(x, y) +} + +fn mul_add_f32(x: f32, y: f32, z: f32) -> f32 { + x.mul_add(y, z) +} + +fn mul_add_f64(x: f64, y: f64, z: f64) -> f64 { + x.mul_add(y, z) +} + +fn fdim_f32(x: f32, y: f32) -> f32 { + libm::fdimf(x, y) +} + +fn fdim_f64(x: f64, y: f64) -> f64 { + libm::fdim(x, y) +} + +fn exp_f32(x: f32) -> f32 { + x.exp() +} + +fn exp_f64(x: f64) -> f64 { + x.exp() +} + +fn exp2_f32(x: f32) -> f32 { + x.exp2() +} + +fn exp2_f64(x: f64) -> f64 { + x.exp2() +} + +fn expm1_f32(x: f32) -> f32 { + x.exp_m1() +} + +fn expm1_f64(x: f64) -> f64 { + x.exp_m1() +} + +fn ln_f32(x: f32) -> f32 { + x.ln() +} + +fn ln_f64(x: f64) -> f64 { + x.ln() +} + +fn log10_f32(x: f32) -> f32 { + x.log10() +} + +fn log10_f64(x: f64) -> f64 { + x.log10() +} + +fn log2_f32(x: f32) -> f32 { + x.log2() +} + +fn log2_f64(x: f64) -> f64 { + x.log2() +} + +fn ln1p_f32(x: f32) -> f32 { + x.ln_1p() +} + +fn ln1p_f64(x: f64) -> f64 { + x.ln_1p() +} + +fn pow_f32(base: f32, exponent: f32) -> f32 { + base.powf(exponent) +} + +fn pow_f64(base: f64, exponent: f64) -> f64 { + base.powf(exponent) +} + +fn cbrt_f32(x: f32) -> f32 { + x.cbrt() +} + +fn cbrt_f64(x: f64) -> f64 { + x.cbrt() +} + +fn hypotenuse_f32(x: f32, y: f32) -> f32 { + x.hypot(y) +} + +fn hypotenuse_f64(x: f64, y: f64) -> f64 { + x.hypot(y) +} + +fn sin_f32(x: f32) -> f32 { + x.sin() +} + +fn sin_f64(x: f64) -> f64 { + x.sin() +} + +fn cos_f32(x: f32) -> f32 { + x.cos() +} + +fn cos_f64(x: f64) -> f64 { + x.cos() +} + +fn tan_f32(x: f32) -> f32 { + x.tan() +} + +fn tan_f64(x: f64) -> f64 { + x.tan() +} + +fn asin_f32(x: f32) -> f32 { + x.asin() +} + +fn asin_f64(x: f64) -> f64 { + x.asin() +} + +fn acos_f32(x: f32) -> f32 { + x.acos() +} + +fn acos_f64(x: f64) -> f64 { + x.acos() +} + +fn atan_f32(x: f32) -> f32 { + x.atan() +} + +fn atan_f64(x: f64) -> f64 { + x.atan() +} + +fn sinh_f32(x: f32) -> f32 { + x.sinh() +} + +fn sinh_f64(x: f64) -> f64 { + x.sinh() +} + +fn cosh_f32(x: f32) -> f32 { + x.cosh() +} + +fn cosh_f64(x: f64) -> f64 { + x.cosh() +} + +fn tanh_f32(x: f32) -> f32 { + x.tanh() +} + +fn tanh_f64(x: f64) -> f64 { + x.tanh() +} + +fn asinh_f32(x: f32) -> f32 { + x.asinh() +} + +fn asinh_f64(x: f64) -> f64 { + x.asinh() +} + +fn acosh_f32(x: f32) -> f32 { + x.acosh() +} + +fn acosh_f64(x: f64) -> f64 { + x.acosh() +} + +fn atanh_f32(x: f32) -> f32 { + x.atanh() +} + +fn atanh_f64(x: f64) -> f64 { + x.atanh() +} + +fn erf_f32(x: f32) -> f32 { + libm::erff(x) +} + +fn erf_f64(x: f64) -> f64 { + libm::erf(x) +} + +fn erfc_f32(x: f32) -> f32 { + libm::erfcf(x) +} + +fn erfc_f64(x: f64) -> f64 { + libm::erfc(x) +} + +fn gamma_f32(x: f32) -> f32 { + libm::tgammaf(x) +} + +fn gamma_f64(x: f64) -> f64 { + libm::tgamma(x) +} + +fn ln_gamma_f32(x: f32) -> f32 { + libm::lgammaf(x) +} + +fn ln_gamma_f64(x: f64) -> f64 { + libm::lgamma(x) +} + +fn round_f32_i32(x: f32) -> i32 { + x.round() as i32 +} + +fn round_f64_i32(x: f64) -> i32 { + x.round() as i32 +} + +fn round_f32_i64(x: f32) -> i64 { + x.round() as i64 +} + +fn round_f64_i64(x: f64) -> i64 { + x.round() as i64 +} + +fn frexp_f32(x: f32) -> (f32, i32) { + libm::frexpf(x) +} + +fn frexp_f64(x: f64) -> (f64, i32) { + libm::frexp(x) +} + +fn ldexp_f32(x: f32, exp: i32) -> f32 { + libm::ldexpf(x, exp) +} + +fn ldexp_f64(x: f64, exp: i32) -> f64 { + libm::ldexp(x, exp) +} + +fn modf_f32(x: f32) -> (f32, f32) { + libm::modff(x) +} + +fn modf_f64(x: f64) -> (f64, f64) { + libm::modf(x) +} + +fn logb_f32(x: f32) -> i32 { + libm::ilogbf(x) +} + +fn logb_f64(x: f64) -> i32 { + libm::ilogb(x) +} + +fn next_after_f32(from: f32, to: f32) -> f32 { + libm::nextafterf(from, to) +} + +fn next_after_f64(from: f64, to: f64) -> f64 { + libm::nextafter(from, to) +} + +fn copy_sign_f32(x: f32, y: f32) -> f32 { + libm::copysignf(x, y) +} + +fn copy_sign_f64(x: f64, y: f64) -> f64 { + libm::copysign(x, y) +} + +pub fn library(manager: &mut ScriptManager) -> Option<()> { + manager.add_library_function(LIBRARY_NAME, "abs", abs_i32)?; + manager.add_library_function(LIBRARY_NAME, "labs", abs_i64)?; + manager.add_library_function(LIBRARY_NAME, "modf", mod_f32)?; + manager.add_library_function(LIBRARY_NAME, "mod", mod_f64)?; + manager.add_library_function(LIBRARY_NAME, "remf", remainder_f32)?; + manager.add_library_function(LIBRARY_NAME, "rem", remainder_f64)?; + manager.add_library_function(LIBRARY_NAME, "fmaf", mul_add_f32)?; + manager.add_library_function(LIBRARY_NAME, "fma", mul_add_f64)?; + manager.add_library_function(LIBRARY_NAME, "fdimf", fdim_f32)?; + manager.add_library_function(LIBRARY_NAME, "fdim", fdim_f64)?; + manager.add_library_function(LIBRARY_NAME, "expf", exp_f32)?; + manager.add_library_function(LIBRARY_NAME, "exp", exp_f64)?; + manager.add_library_function(LIBRARY_NAME, "exp2f", exp2_f32)?; + manager.add_library_function(LIBRARY_NAME, "exp2", exp2_f64)?; + manager.add_library_function(LIBRARY_NAME, "expm1f", expm1_f32)?; + manager.add_library_function(LIBRARY_NAME, "expm1", expm1_f64)?; + manager.add_library_function(LIBRARY_NAME, "lnf", ln_f32)?; + manager.add_library_function(LIBRARY_NAME, "ln", ln_f64)?; + manager.add_library_function(LIBRARY_NAME, "log2f", log2_f32)?; + manager.add_library_function(LIBRARY_NAME, "log2", log2_f64)?; + manager.add_library_function(LIBRARY_NAME, "log10f", log10_f32)?; + manager.add_library_function(LIBRARY_NAME, "log10", log10_f64)?; + manager.add_library_function(LIBRARY_NAME, "ln1pf", ln1p_f32)?; + manager.add_library_function(LIBRARY_NAME, "ln1p", ln1p_f64)?; + manager.add_library_function(LIBRARY_NAME, "powf", pow_f32)?; + manager.add_library_function(LIBRARY_NAME, "pow", pow_f64)?; + manager.add_library_function(LIBRARY_NAME, "cbrtf", cbrt_f32)?; + manager.add_library_function(LIBRARY_NAME, "cbrt", cbrt_f64)?; + manager.add_library_function(LIBRARY_NAME, "hypotf", hypotenuse_f32)?; + manager.add_library_function(LIBRARY_NAME, "hypot", hypotenuse_f64)?; + manager.add_library_function(LIBRARY_NAME, "sinf", sin_f32)?; + manager.add_library_function(LIBRARY_NAME, "sin", sin_f64)?; + manager.add_library_function(LIBRARY_NAME, "cosf", cos_f32)?; + manager.add_library_function(LIBRARY_NAME, "cos", cos_f64)?; + manager.add_library_function(LIBRARY_NAME, "tanf", tan_f32)?; + manager.add_library_function(LIBRARY_NAME, "tan", tan_f64)?; + manager.add_library_function(LIBRARY_NAME, "asinf", asin_f32)?; + manager.add_library_function(LIBRARY_NAME, "asin", asin_f64)?; + manager.add_library_function(LIBRARY_NAME, "acosf", acos_f32)?; + manager.add_library_function(LIBRARY_NAME, "acos", acos_f64)?; + manager.add_library_function(LIBRARY_NAME, "atanf", atan_f32)?; + manager.add_library_function(LIBRARY_NAME, "atan", atan_f64)?; + manager.add_library_function(LIBRARY_NAME, "sinhf", sinh_f32)?; + manager.add_library_function(LIBRARY_NAME, "sinh", sinh_f64)?; + manager.add_library_function(LIBRARY_NAME, "coshf", cosh_f32)?; + manager.add_library_function(LIBRARY_NAME, "cosh", cosh_f64)?; + manager.add_library_function(LIBRARY_NAME, "tanhf", tanh_f32)?; + manager.add_library_function(LIBRARY_NAME, "tanh", tanh_f64)?; + manager.add_library_function(LIBRARY_NAME, "asinhf", asinh_f32)?; + manager.add_library_function(LIBRARY_NAME, "asinh", asinh_f64)?; + manager.add_library_function(LIBRARY_NAME, "acoshf", acosh_f32)?; + manager.add_library_function(LIBRARY_NAME, "acosh", acosh_f64)?; + manager.add_library_function(LIBRARY_NAME, "atanhf", atanh_f32)?; + manager.add_library_function(LIBRARY_NAME, "atanh", atanh_f64)?; + manager.add_library_function(LIBRARY_NAME, "erff", erf_f32)?; + manager.add_library_function(LIBRARY_NAME, "erf", erf_f64)?; + manager.add_library_function(LIBRARY_NAME, "erfcf", erfc_f32)?; + manager.add_library_function(LIBRARY_NAME, "erfc", erfc_f64)?; + manager.add_library_function(LIBRARY_NAME, "gammaf", gamma_f32)?; + manager.add_library_function(LIBRARY_NAME, "gamma", gamma_f64)?; + manager.add_library_function(LIBRARY_NAME, "lgammaf", ln_gamma_f32)?; + manager.add_library_function(LIBRARY_NAME, "lgamma", ln_gamma_f64)?; + manager.add_library_function(LIBRARY_NAME, "lroundf", round_f32_i32)?; + manager.add_library_function(LIBRARY_NAME, "lround", round_f64_i32)?; + manager.add_library_function(LIBRARY_NAME, "llroundf", round_f32_i64)?; + manager.add_library_function(LIBRARY_NAME, "llround", round_f64_i64)?; + manager.add_library_function(LIBRARY_NAME, "frexpf", frexp_f32)?; + manager.add_library_function(LIBRARY_NAME, "frexp", frexp_f64)?; + manager.add_library_function(LIBRARY_NAME, "ldexpf", ldexp_f32)?; + manager.add_library_function(LIBRARY_NAME, "ldexp", ldexp_f64)?; + manager.add_library_function(LIBRARY_NAME, "modff", modf_f32)?; + manager.add_library_function(LIBRARY_NAME, "modf", modf_f64)?; + manager.add_library_function(LIBRARY_NAME, "ilogbf", logb_f32)?; + manager.add_library_function(LIBRARY_NAME, "ilogb", logb_f64)?; + manager.add_library_function(LIBRARY_NAME, "nextafterf", next_after_f32)?; + manager.add_library_function(LIBRARY_NAME, "nextafter", next_after_f64)?; + manager.add_library_function(LIBRARY_NAME, "copysignf", copy_sign_f32)?; + manager.add_library_function(LIBRARY_NAME, "copysign", copy_sign_f64)?; + + Some(()) +} diff --git a/scripts/src/libs/mod.rs b/scripts/src/libs/mod.rs new file mode 100644 index 0000000..c6b1ffc --- /dev/null +++ b/scripts/src/libs/mod.rs @@ -0,0 +1,19 @@ +use crate::ScriptManager; + +mod allocate; +mod buffer; +mod ctype; +mod math; +mod system; + +const LIBRARY_NAME: &str = "alligator"; + +pub fn add_alligator_library(manager: &mut ScriptManager) -> Option<()> { + allocate::library(manager)?; + buffer::library(manager)?; + ctype::library(manager)?; + math::library(manager)?; + system::library(manager)?; + + Some(()) +} diff --git a/scripts/src/libs/system.rs b/scripts/src/libs/system.rs new file mode 100644 index 0000000..e2d0ddb --- /dev/null +++ b/scripts/src/libs/system.rs @@ -0,0 +1,154 @@ +use std::mem::align_of; +use std::{borrow::Cow, process::Command}; + +use chrono::Offset; +use wasmtime::{Caller, Memory}; + +use crate::{ScriptManager, WasmScriptState}; + +use super::LIBRARY_NAME; + +pub fn _get_memory<'a>(caller: &'a mut Caller<WasmScriptState>) -> (Memory, &'a mut [u8]) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let mem_ptr = memory.data_mut(caller); + (memory, mem_ptr) +} + +fn _get_string<'a>( + caller: &'a mut Caller<WasmScriptState>, + offset: u32, + length: u32, +) -> Option<Cow<'a, str>> { + let (_, mem_ptr) = _get_memory(caller); + if (offset as usize + length as usize) > mem_ptr.len() { + return None; + } + + Some(String::from_utf8_lossy( + &mem_ptr[offset as usize..length as usize], + )) +} + +fn _log(caller: &mut Caller<WasmScriptState>, level: log::Level, message: u32, length: u32) -> u32 { + let Some(message) = _get_string(caller, message, length) else { + return 4; + }; + log::log!(level, "{}", message); + 0 +} + +pub fn _alloc<'a>( + mut caller: &'a mut Caller<WasmScriptState>, + size: u32, + align: u32, +) -> Option<(u32, &'a mut [u8])> { + // find bumper location in memory + let bump_offset = caller.data().bump_pointer; + let (memory, memory_ptr) = _get_memory(caller); + let bump_ptr = &memory_ptr[bump_offset as usize..]; + + // calculate pointer to the new allocation + let offset = bump_ptr.as_ptr().align_offset(align as usize); + let new_offset = bump_offset + offset as u32; + + // update bump pointer + let bump_offset = &mut caller.data_mut().bump_pointer; + *bump_offset = new_offset + size; + + // grow memory if necessary + let bump_offset = *bump_offset; + if memory.data_size(&caller) < bump_offset as usize { + let delta = (size + offset as u32) / (64 * 1024) + 1; + if memory.grow(&mut caller, delta as u64).is_err() { + return None; + } + } + + let new_ptr = &mut _get_memory(caller).1[new_offset as usize..size as usize]; + Some((new_offset, new_ptr)) +} + +fn allocate(mut caller: Caller<WasmScriptState>, size: u32, align: u32) -> u32 { + match _alloc(&mut caller, size, align) { + Some((ptr, _)) => ptr, + None => 0, + } +} + +fn free_sized(mut caller: Caller<'_, WasmScriptState>, ptr: u32, size: u32) -> u32 { + let bump_offset = &mut caller.data_mut().bump_pointer; + if ptr + size == *bump_offset { + *bump_offset = ptr; + } + + 0 +} + +fn free_aligned_sized( + caller: Caller<'_, WasmScriptState>, + ptr: u32, + _alignment: u32, + size: u32, +) -> u32 { + free_sized(caller, ptr, size) +} + +fn env_var(mut caller: Caller<'_, WasmScriptState>, name: u32, length: u32) -> u32 { + let Some(name) = _get_string(&mut caller, name, length) else { + return 4; + }; + let Ok(value) = std::env::var(name.to_string()) else { + return 1; + }; + + let Some((offset, ptr)) = _alloc(&mut caller, value.len() as u32, align_of::<u8>() as u32) + else { + return 0; + }; + ptr.clone_from_slice(value.as_bytes()); + + offset +} + +fn random_number() -> u64 { + rand::random() +} + +fn current_time_utc() -> (i64, u32) { + let duration = chrono::Utc::now() - chrono::DateTime::<chrono::Utc>::MIN_UTC; + ( + duration.num_seconds(), + duration.to_std().unwrap().subsec_nanos(), + ) +} + +fn local_offset() -> i32 { + chrono::Local::now().offset().fix().local_minus_utc() +} + +fn log(mut caller: Caller<'_, WasmScriptState>, message: u32, length: u32) { + _log(&mut caller, log::Level::Info, message, length); +} + +fn log_warn(mut caller: Caller<'_, WasmScriptState>, message: u32, length: u32) { + _log(&mut caller, log::Level::Warn, message, length); +} + +fn log_error(mut caller: Caller<'_, WasmScriptState>, message: u32, length: u32) { + _log(&mut caller, log::Level::Error, message, length); +} + +pub fn library(manager: &mut ScriptManager) -> Option<()> { + manager.add_library_function(LIBRARY_NAME, "aligned_alloc", allocate)?; + manager.add_library_function(LIBRARY_NAME, "free_sized", free_sized)?; + manager.add_library_function(LIBRARY_NAME, "free_aligned_sized", free_aligned_sized)?; + manager.add_library_function(LIBRARY_NAME, "env_var", env_var)?; + manager.add_library_function(LIBRARY_NAME, "random_number", random_number)?; + manager.add_library_function(LIBRARY_NAME, "current_time_utc", current_time_utc)?; + manager.add_library_function(LIBRARY_NAME, "local_offset", local_offset)?; + manager.add_library_function(LIBRARY_NAME, "log", log)?; + manager.add_library_function(LIBRARY_NAME, "log_warn", log_warn)?; + manager.add_library_function(LIBRARY_NAME, "log_error", log_error)?; + + Some(()) +} diff --git a/scripts/src/vtable.rs b/scripts/src/vtable.rs new file mode 100644 index 0000000..0fbb324 --- /dev/null +++ b/scripts/src/vtable.rs @@ -0,0 +1,10 @@ +/// A trait for scripts +pub trait ScriptTable: Send + Sync { + /// This is called whenever the script is enabled + fn begin(&mut self); + + /// This is called every frame + fn update(&mut self); +} + +pub type VTableScript = &'static mut dyn ScriptTable; diff --git a/scripts/src/wasm.rs b/scripts/src/wasm.rs new file mode 100644 index 0000000..7ac9dae --- /dev/null +++ b/scripts/src/wasm.rs @@ -0,0 +1,170 @@ +use std::path::Path; + +use thiserror::Error; +use wasmtime::{Engine, Instance, Linker, Module, Mutability, Store, ValType}; + +// It's a bad idea to have the memory allocator return a number that could also +// indicate an error, so this is set to be at least eight. +const MIN_BUMP_POINTER: u32 = 8; + +/// Information about the script which can be used by functions it calls +pub struct WasmScriptState { + pub bump_pointer: u32, + pub trusted: bool, +} + +impl WasmScriptState { + pub const fn new(trusted: bool) -> Self { + Self { + bump_pointer: MIN_BUMP_POINTER, + trusted, + } + } +} + +/// A script, its path in the filesystem, and some metadata +pub struct WasmScript { + path: Box<Path>, + module: Module, + store: Store<WasmScriptState>, + instance: Instance, + trusted: bool, + //state: Value, +} + +#[derive(Debug, Error)] +pub enum InvalidWasmScript { + #[error("There is no exported memory called 'memory', which is required")] + NoExportedMemory, + #[error("The exported symbol, 'memory' must be a memory")] + MemoryIsNotAMemory, + #[error("The memory must be 32-bit, not 64-bit")] + MemoryTooLarge, + #[error("There is no exported global called '__heap_base', which is required")] + NoHeapBase, + #[error("The exported symbol, '__heap_base' must be a constant global")] + HeapBaseIsNotGlobal, + #[error("The exported global, '__heap_base' must be an i32")] + HeapBaseMustBeI32, + #[error("The exported global, '__heap_base' must be a constant")] + HeapBaseMustBeConstant, + #[error("{}", .0)] + CompilerError(#[from] wasmtime::Error), +} + +/// Confirms that the module can be used as a script +fn validate_module(module: &Module) -> Result<(), InvalidWasmScript> { + // verify that memory is exported from this module and is valid + let Some(export) = module.get_export("memory") else { + return Err(InvalidWasmScript::NoExportedMemory); + }; + let Some(memory) = export.memory() else { + return Err(InvalidWasmScript::MemoryIsNotAMemory); + }; + if memory.is_64() { + return Err(InvalidWasmScript::MemoryTooLarge); + } + + // verify __heap_base global + let Some(export) = module.get_export("__heap_base") else { + return Err(InvalidWasmScript::NoHeapBase); + }; + let Some(heap_base) = export.global() else { + return Err(InvalidWasmScript::HeapBaseIsNotGlobal); + }; + if *heap_base.content() != ValType::I32 { + return Err(InvalidWasmScript::HeapBaseMustBeI32); + } + if heap_base.mutability() != Mutability::Const { + return Err(InvalidWasmScript::HeapBaseMustBeConstant); + } + + Ok(()) +} + +impl WasmScript { + pub fn new( + path: &Path, + engine: &Engine, + linker: &Linker<WasmScriptState>, + trusted: bool, + ) -> Result<Self, InvalidWasmScript> { + let module = Module::from_file(engine, path)?; + validate_module(&module)?; + let mut store = Store::new(engine, WasmScriptState::new(trusted)); + let instance = linker.instantiate(&mut store, &module)?; + + Ok(Self { + path: path.into(), + module, + store, + instance, + trusted, + }) + } + + /// Reload from the filesystem + pub fn reload( + &mut self, + engine: &Engine, + linker: &Linker<WasmScriptState>, + ) -> Result<(), InvalidWasmScript> { + let module = Module::from_file(engine, &self.path)?; + validate_module(&module)?; + self.store = Store::new(engine, WasmScriptState::new(self.trusted)); + self.instance = linker.instantiate(&mut self.store, &module)?; + + Ok(()) + } + + /// Re-links the module. This doesn't load the module from the filesystem. + pub fn relink( + &mut self, + engine: &Engine, + linker: &Linker<WasmScriptState>, + ) -> Result<(), InvalidWasmScript> { + self.store = Store::new(engine, WasmScriptState::new(self.trusted)); + self.instance = linker.instantiate(&mut self.store, &self.module)?; + Ok(()) + } + + pub fn is_trusted(&self) -> bool { + self.trusted + } + + pub fn trust(&mut self) { + self.trusted = true; + } + + pub fn untrust(&mut self) { + self.trusted = false; + } + + fn run_function_if_exists(&mut self, name: &str) -> wasmtime::Result<()> { + // set bump pointer to the start of the heap + let heap_base = self + .instance + .get_global(&mut self.store, "__heap_base") + .unwrap() + .get(&mut self.store) + .unwrap_i32(); + let bump_ptr = &mut self.store.data_mut().bump_pointer; + *bump_ptr = (heap_base as u32).max(MIN_BUMP_POINTER); + + // call the given function + let func = self.instance.get_func(&mut self.store, name); + if let Some(func) = func { + func.call(&mut self.store, &[], &mut [])?; + } + + Ok(()) + } + + pub fn begin(&mut self) -> wasmtime::Result<()> { + self.run_function_if_exists("begin") + } + + pub fn update(&mut self) -> wasmtime::Result<()> { + self.run_function_if_exists("update") + } +} |
