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