/* * Marine WebAssembly runtime * * Copyright (C) 2024 Fluence DAO * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation version 3 of the * License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use crate::JsInstance; use crate::JsWasmBackend; use crate::JsImportCallContext; use crate::JsContext; use crate::JsContextMut; use crate::js_conversions::js_array_from_wval_array; use crate::js_conversions::wval_array_from_js_array; use crate::js_conversions::wval_from_js; use crate::js_conversions::wval_to_i32; use crate::store::JsStoreInner; use crate::store::FunctionHandle; use crate::single_shot_async_executor::execute_future_blocking; use marine_wasm_backend_traits::impl_for_each_function_signature; use marine_wasm_backend_traits::replace_with; use marine_wasm_backend_traits::prelude::*; use anyhow::anyhow; use futures::future::BoxFuture; use futures::FutureExt; use js_sys::Array; use wasm_bindgen::prelude::*; /// Safety: js-backend is expected to run in single-threaded environment, /// so it is safe to assume that every type is Send + Sync unsafe impl Send for HostImportFunction {} unsafe impl Sync for HostImportFunction {} unsafe impl Send for WasmExportFunction {} unsafe impl Sync for WasmExportFunction {} #[derive(Clone)] pub struct HostImportFunction { pub(crate) store_handle: FunctionHandle, } #[derive(Clone)] pub struct WasmExportFunction { pub(crate) store_handle: FunctionHandle, /// Instance this export is extracted from. pub(crate) bound_instance: JsInstance, } pub(crate) struct StoredFunction { pub(crate) js_function: js_sys::Function, pub(crate) signature: FuncSig, } impl StoredFunction { pub(crate) fn new(js_function: js_sys::Function, signature: FuncSig) -> Self { Self { js_function, signature, } } } impl WasmExportFunction { pub(crate) fn new_stored( ctx: &mut impl AsContextMut, instance: JsInstance, func: js_sys::Function, signature: FuncSig, ) -> Self { let handle = ctx .as_context_mut() .inner .store_function(StoredFunction::new(func, signature)); Self { store_handle: handle, bound_instance: instance, } } pub(crate) fn stored_mut<'store>( &self, ctx: JsContextMut<'store>, ) -> &'store mut StoredFunction { &mut ctx.inner.functions[self.store_handle] } fn call_inner( &self, store: &mut impl AsContextMut, args: &[WValue], ) -> RuntimeResult> { let params = js_array_from_wval_array(args); let stored_func = self.stored_mut(store.as_context_mut()); let result = js_sys::Reflect::apply(&stored_func.js_function, &JsValue::NULL, ¶ms) .map_err(|e| { web_sys::console::log_2(&"failed to apply func".into(), &e); RuntimeError::Other(anyhow!("Failed to apply func")) })?; extract_function_results(result, stored_func.signature.returns()) } } fn extract_function_results(result: JsValue, result_types: &[WType]) -> RuntimeResult> { match result_types.len() { 0 => Ok(vec![]), 1 => { // Single value returned as is. let value = wval_from_js(&result_types[0], &result); Ok(vec![value]) } results_number => { // Multiple return values are returned as JS array of values. let result_array: Array = result.into(); if result_array.length() as usize != results_number { Err(RuntimeError::IncorrectResultsNumber { expected: results_number, actual: result_array.length() as usize, }) } else { Ok(wval_array_from_js_array(&result_array, result_types.iter())) } } } } impl HostImportFunction { pub(crate) fn stored<'store>(&self, ctx: &JsContext<'store>) -> &'store StoredFunction { &ctx.inner.functions[self.store_handle] } pub(crate) fn stored_mut<'store>( &self, ctx: JsContextMut<'store>, ) -> &'store mut StoredFunction { &mut ctx.inner.functions[self.store_handle] } } impl HostFunction for HostImportFunction { fn new(store: &mut impl AsContextMut, signature: FuncSig, func: F) -> Self where F: for<'c> Fn(&'c [WValue]) -> anyhow::Result> + Sync + Send + 'static, { let with_caller = move |_, args: &'_ [WValue]| func(args); Self::new_with_caller(store, signature, with_caller) } fn new_with_caller( store: &mut impl AsContextMut, signature: FuncSig, func: F, ) -> Self where F: for<'c> Fn(JsImportCallContext, &[WValue]) -> anyhow::Result> + Sync + Send + 'static, { // Safety: JsStoreInner is stored inside a Box and the Store is required by wasm-backend traits contract // to be valid for function execution. So it is safe to capture this ptr into closure and deference there let store_inner_ptr = store.as_context_mut().inner as *mut JsStoreInner; let wrapped = wrap_raw_host_fn(signature.clone(), store_inner_ptr, func); let closure = prepare_js_closure(wrapped); let handle = store .as_context_mut() .inner .store_function(StoredFunction::new(closure, signature)); Self { store_handle: handle, } } fn new_with_caller_async( store: &mut impl AsContextMut, signature: FuncSig, func: F, ) -> Self where F: for<'c> Fn( ::ImportCallContext<'c>, &'c [WValue], ) -> BoxFuture<'c, anyhow::Result>> + Sync + Send + 'static, { // Safety: JsStoreInner is stored inside a Box and the Store is required by wasm-backend traits contract // to be valid for function execution. So it is safe to capture this ptr into closure and deference there let store_inner_ptr = store.as_context_mut().inner as *mut JsStoreInner; let wrapped = wrap_raw_host_fn_async(signature.clone(), store_inner_ptr, func); let closure = prepare_js_closure(wrapped); let handle = store .as_context_mut() .inner .store_function(StoredFunction::new(closure, signature)); Self { store_handle: handle, } } fn new_async(store: &mut impl AsContextMut, sig: FuncSig, func: F) -> Self where F: for<'c> Fn(&'c [WValue]) -> BoxFuture<'c, anyhow::Result>> + Sync + Send + 'static, { Self::new_with_caller_async(store, sig, move |_caller, args| func(args)) } fn new_typed( store: &mut impl AsContextMut, func: impl IntoFunc, ) -> Self { func.into_func(store) } fn signature(&self, store: &mut impl AsContextMut) -> FuncSig { self.stored_mut(store.as_context_mut()).signature.clone() } } fn wrap_raw_host_fn( signature: FuncSig, store_inner_ptr: *mut JsStoreInner, raw_host_function: F, ) -> Box Array> where F: for<'c> Fn(JsImportCallContext, &[WValue]) -> anyhow::Result> + Sync + Send + 'static, { let func = move |args: &js_sys::Array| -> js_sys::Array { log::debug!( "function produced by JsFunction:::new_with_caller call, signature: {:?}", signature ); let store_inner = unsafe { &mut *store_inner_ptr }; let caller_instance = store_inner.wasm_call_stack.last().cloned().expect( "Import cannot be called outside of an export call, when wasm_call_stack is empty", ); let caller = JsImportCallContext { store_inner, caller_instance, }; let args = wval_array_from_js_array(args, signature.params().iter()); let result = raw_host_function(caller, &args).unwrap_throw(); // TODO is it right? js_array_from_wval_array(&result) }; Box::new(func) } fn wrap_raw_host_fn_async( signature: FuncSig, store_inner_ptr: *mut JsStoreInner, raw_host_function: F, ) -> Box Array> where F: for<'c> Fn(JsImportCallContext, &[WValue]) -> BoxFuture<'_, anyhow::Result>> + Sync + Send + 'static, { let func = move |args: &js_sys::Array| -> js_sys::Array { log::debug!( "function produced by JsFunction:::new_with_caller call, signature: {:?}", signature ); let store_inner = unsafe { &mut *store_inner_ptr }; let caller_instance = store_inner.wasm_call_stack.last().cloned().expect( "Import cannot be called outside of an export call, when wasm_call_stack is empty", ); let caller = JsImportCallContext { store_inner, caller_instance, }; let args = wval_array_from_js_array(args, signature.params().iter()); let result = execute_future_blocking(raw_host_function(caller, &args)).unwrap_throw(); js_array_from_wval_array(&result) }; Box::new(func) } fn prepare_js_closure(func: Box Array>) -> js_sys::Function { let closure = Closure::wrap(func).into_js_value(); // Make a function that converts function args into array and wrap our func with it. // Otherwise our closure will get only first argument. let wrapper = js_sys::Function::new_with_args( "wrapped_func", "return wrapped_func(Array.prototype.slice.call(arguments, 1))", ); wrapper.bind1(&JsValue::UNDEFINED, &closure) } impl ExportFunction for WasmExportFunction { fn signature(&self, store: &mut impl AsContextMut) -> FuncSig { self.stored_mut(store.as_context_mut()).signature.clone() } fn call_async<'args>( &'args self, store: &'args mut impl AsContextMut, args: &'args [WValue], ) -> BoxFuture<'args, RuntimeResult>> { async move { store .as_context_mut() .inner .wasm_call_stack .push(self.bound_instance.clone()); let result = self.call_inner(store, args); store.as_context_mut().inner.wasm_call_stack.pop(); result } .boxed() } } /// Generates a function that accepts a Fn with $num template parameters and turns it into WasmtimeFunction. /// Needed to allow users to pass almost any function to `Function::new_typed` without worrying about signature. macro_rules! impl_func_construction { ($num:tt $($args:ident)*) => (paste::paste!{ fn [< new_typed_with_env_ $num >] (mut ctx: JsContextMut<'_>, func: F) -> HostImportFunction where F: Fn(JsImportCallContext, $(replace_with!($args -> i32),)*) + Send + Sync + 'static { let func = move |caller: JsImportCallContext, args: &[WValue]| -> anyhow::Result> { let [$($args,)*] = args else { todo!() }; // TODO: Safety: explain why it will never fire func(caller, $(wval_to_i32($args),)*); Ok(vec![]) }; let arg_ty = vec![WType::I32; $num]; let ret_ty = vec![]; let signature = FuncSig::new(arg_ty, ret_ty); HostImportFunction::new_with_caller(&mut ctx, signature, func) } fn [< new_typed_with_env_ $num _r>] (mut ctx: JsContextMut<'_>, func: F) -> HostImportFunction where F: Fn(JsImportCallContext, $(replace_with!($args -> i32),)*) -> i32 + Send + Sync + 'static { let func = move |caller: JsImportCallContext, args: &[WValue]| -> anyhow::Result> { let [$($args,)*] = args else { panic!("args do not match signature") }; // Safety: signature should b let res = func(caller, $(wval_to_i32(&$args),)*); Ok(vec![WValue::I32(res)]) }; let arg_ty = vec![WType::I32; $num]; let ret_ty = vec![WType::I32]; let signature = FuncSig::new(arg_ty, ret_ty); HostImportFunction::new_with_caller(&mut ctx, signature, func) } }); } impl FuncConstructor for HostImportFunction { impl_for_each_function_signature!(impl_func_construction); }