use std::collections::HashMap ;
use std::sync::Arc ;
use futures::future::BoxFuture ;
use futures::lock::Mutex ;
use futures::task::{ FutureObj, Spawn };
use thiserror::Error ;
use wasmtime::component::{ Instance, Val };
use wasmtime::Store ;
use crate::{ Function, PluginContext, Remap, ReturnKind };
use crate::resource_wrapper::{ ResourceCreationError, ResourceReceiveError };
type CallLimiter<Ctx> = Box<dyn FnMut( &mut Store<Ctx>, &str, &str, &Function ) -> u64 + Send>;
pub struct PluginInstanceSync<Ctx: 'static> {
state: PluginState<Ctx>,
}
pub struct PluginInstanceAsync<Ctx: 'static> {
state: Arc<Mutex<PluginState<Ctx>>>,
executor: Arc<dyn Spawn + Send + Sync>,
}
struct PluginState<Ctx: 'static> {
store: Store<Ctx>,
instance: Instance,
interface_remaps: HashMap<String, Remap>,
fuel_limiter: Option<CallLimiter<Ctx>>,
epoch_limiter: Option<CallLimiter<Ctx>>,
}
impl<Ctx: std::fmt::Debug + 'static> std::fmt::Debug for PluginInstanceSync<Ctx> {
fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct( "PluginInstanceSync" )
.field( "data", &self.state.store.data() )
.field( "store", &self.state.store )
.field( "interface_remaps", &self.state.interface_remaps )
.field( "fuel_limiter", &self.state.fuel_limiter.as_ref().map(| _ | "<closure>" ))
.field( "epoch_limiter", &self.state.epoch_limiter.as_ref().map(| _ | "<closure>" ))
.finish_non_exhaustive()
}
}
impl<Ctx: 'static> std::fmt::Debug for PluginInstanceAsync<Ctx> {
fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct( "PluginInstanceAsync" )
.field( "state", &"<serialized store>" )
.field( "executor", &"<executor>" )
.finish_non_exhaustive()
}
}
#[derive( Error, Debug )]
pub enum DispatchError {
#[error( "Lock Rejected" )] LockRejected,
#[error( "Invalid Interface Path: {0}" )] InvalidInterfacePath( String ),
#[error( "Invalid Function: {0}" )] InvalidFunction( String ),
#[error( "Missing Response" )] MissingResponse,
#[error( "Runtime Exception" )] RuntimeException( wasmtime::Error ),
#[error( "Invalid Argument List" )] InvalidArgumentList,
#[error( "Unsupported type: {0}" )] UnsupportedType( String ),
#[error( "Async executor unavailable" )] ExecutorUnavailable,
#[error( "Resource Create Error: {0}" )] ResourceCreationError( #[from] ResourceCreationError ),
#[error( "Resource Receive Error: {0}" )] ResourceReceiveError( #[from] ResourceReceiveError ),
}
impl From<DispatchError> for Val {
fn from( error: DispatchError ) -> Val { match error {
DispatchError::LockRejected => Val::Variant( "lock-rejected".to_string(), None ),
DispatchError::InvalidInterfacePath( package ) => Val::Variant( "invalid-interface-path".to_string(), Some( Box::new( Val::String( package )))),
DispatchError::InvalidFunction( function ) => Val::Variant( "invalid-function".to_string(), Some( Box::new( Val::String( function )))),
DispatchError::MissingResponse => Val::Variant( "missing-response".to_string(), None ),
DispatchError::RuntimeException( exception ) => Val::Variant( "runtime-exception".to_string(), Some( Box::new( Val::String( exception.to_string() )))),
DispatchError::InvalidArgumentList => Val::Variant( "invalid-argument-list".to_string(), None ),
DispatchError::UnsupportedType( name ) => Val::Variant( "unsupported-type".to_string(), Some( Box::new( Val::String( name )))),
DispatchError::ExecutorUnavailable => Val::Variant( "executor-unavailable".to_string(), None ),
DispatchError::ResourceCreationError( err ) => err.into(),
DispatchError::ResourceReceiveError( err ) => err.into(),
}}
}
impl<Ctx: PluginContext + 'static> PluginInstanceSync<Ctx> {
pub(crate) fn new_sync(
store: Store<Ctx>,
instance: Instance,
interface_remaps: HashMap<String, Remap>,
fuel_limiter: Option<CallLimiter<Ctx>>,
epoch_limiter: Option<CallLimiter<Ctx>>,
) -> Self {
Self { state: PluginState {
store,
instance,
interface_remaps,
fuel_limiter,
epoch_limiter,
}}
}
pub(crate) fn dispatch(
&mut self,
package_name: &str,
interface_name: &str,
function_name: &str,
function: &Function,
data: &[Val],
) -> Result<Val, DispatchError> {
self.state.dispatch( package_name, interface_name, function_name, function, data )
}
}
impl<Ctx: PluginContext + 'static> PluginInstanceAsync<Ctx> {
pub(crate) fn new(
store: Store<Ctx>,
instance: Instance,
interface_remaps: HashMap<String, Remap>,
fuel_limiter: Option<CallLimiter<Ctx>>,
epoch_limiter: Option<CallLimiter<Ctx>>,
executor: impl Spawn + Send + Sync + 'static,
) -> Self {
Self {
state: Arc::new( Mutex::new( PluginState {
store,
instance,
interface_remaps,
fuel_limiter,
epoch_limiter,
})),
executor: Arc::new( executor ),
}
}
pub(crate) async fn dispatch_async(
&self,
package_name: &str,
interface_name: &str,
function_name: &str,
function: &Function,
data: &[Val],
) -> Result<Val, DispatchError> {
let state = Arc::clone( &self.state );
let package_name = package_name.to_string();
let interface_name = interface_name.to_string();
let function_name = function_name.to_string();
let function = function.clone();
let data = data.to_vec();
let ( response, result ) = futures::channel::oneshot::channel();
let task: BoxFuture<'static, ()> = Box::pin( async move {
let result = state.lock().await.dispatch_async(
&package_name,
&interface_name,
&function_name,
&function,
&data,
).await;
let _ = response.send( result );
});
self.executor.spawn_obj( FutureObj::new( task ))
.map_err(| _ | DispatchError::ExecutorUnavailable )?;
result.await.map_err(| _ | DispatchError::ExecutorUnavailable )?
}
}
impl<Ctx: PluginContext + 'static> PluginState<Ctx> {
const PLACEHOLDER_VAL: Val = Val::Option( None );
const VOID_RETURN_VAL: Val = Val::Option( None );
fn dispatch(
&mut self,
package_name: &str,
interface_name: &str,
function_name: &str,
function: &Function,
data: &[Val],
) -> Result<Val, DispatchError> {
ensure_supported_values( data )?;
let mut buffer = self.prepare_call( package_name, interface_name, function_name, function )?;
let ( exported_interface_path, exported_function_name ) = self.resolve_export( package_name, interface_name, function_name );
let func = self.function( &exported_interface_path, &exported_function_name )?;
let call_result = func.call( &mut self.store, data, &mut buffer );
self.finish_call( function, buffer, call_result )
}
async fn dispatch_async(
&mut self,
package_name: &str,
interface_name: &str,
function_name: &str,
function: &Function,
data: &[Val],
) -> Result<Val, DispatchError> {
ensure_supported_values( data )?;
let mut buffer = self.prepare_call( package_name, interface_name, function_name, function )?;
let ( exported_interface_path, exported_function_name ) = self.resolve_export( package_name, interface_name, function_name );
let func = self.function( &exported_interface_path, &exported_function_name )?;
let call_result = func.call_async( &mut self.store, data, &mut buffer ).await;
self.finish_call( function, buffer, call_result )
}
fn prepare_call(
&mut self,
package_name: &str,
interface_name: &str,
function_name: &str,
function: &Function,
) -> Result<Vec<Val>, DispatchError> {
let canonical_interface_path = format!( "{}/{}", package_name, interface_name );
if let Some( mut limiter ) = self.fuel_limiter.take() {
let fuel = limiter( &mut self.store, &canonical_interface_path, function_name, function );
self.fuel_limiter = Some( limiter );
self.store.set_fuel( fuel ).map_err( DispatchError::RuntimeException )?;
}
if let Some( mut limiter ) = self.epoch_limiter.take() {
let ticks = limiter( &mut self.store, &canonical_interface_path, function_name, function );
self.epoch_limiter = Some( limiter );
self.store.set_epoch_deadline( ticks );
}
Ok( match function.return_kind() != ReturnKind::Void {
true => vec![ Self::PLACEHOLDER_VAL ],
false => Vec::with_capacity( 0 ),
})
}
fn function( &mut self, interface_path: &str, function_name: &str ) -> Result<wasmtime::component::Func, DispatchError> {
let interface_index = self.instance
.get_export_index( &mut self.store, None, interface_path )
.ok_or_else(|| DispatchError::InvalidInterfacePath( interface_path.to_string() ))?;
let func_index = self.instance
.get_export_index( &mut self.store, Some( &interface_index ), function_name )
.ok_or_else(|| DispatchError::InvalidFunction( format!( "{interface_path}:{function_name}" )))?;
self.instance
.get_func( &mut self.store, func_index )
.ok_or_else(|| DispatchError::InvalidFunction( format!( "{interface_path}:{function_name}" )))
}
fn finish_call(
&mut self,
function: &Function,
mut buffer: Vec<Val>,
call_result: Result<(), wasmtime::Error>,
) -> Result<Val, DispatchError> {
if self.fuel_limiter.is_some() { let _ = self.store.set_fuel( 0 ); }
call_result.map_err( DispatchError::RuntimeException )?;
let result = match function.return_kind() != ReturnKind::Void {
true => buffer.pop().ok_or( DispatchError::MissingResponse )?,
false => Self::VOID_RETURN_VAL,
};
ensure_supported_value( &result )?;
Ok( result )
}
fn resolve_export( &self, package_name: &str, interface_name: &str, function_name: &str ) -> (String, String) {
match self.interface_remaps.get( interface_name ) {
Some( remap ) => (
format!( "{}/{}", package_name, remap.interface_name( interface_name )),
remap.item_name( function_name ).to_string(),
),
None => (
format!( "{}/{}", package_name, interface_name ),
function_name.to_string(),
),
}
}
}
fn ensure_supported_values( values: &[Val] ) -> Result<(), DispatchError> {
values.iter().try_for_each( ensure_supported_value )
}
fn ensure_supported_value( value: &Val ) -> Result<(), DispatchError> {
match value {
Val::List( values ) | Val::Tuple( values ) => ensure_supported_values( values ),
Val::Map( values ) => values.iter().try_for_each(|( key, value )| {
ensure_supported_value( key )?;
ensure_supported_value( value )
}),
Val::Record( values ) => values.iter().try_for_each(|( _, value )| ensure_supported_value( value )),
Val::Variant( _, Some( value ))
| Val::Option( Some( value ))
| Val::Result( Ok( Some( value )))
| Val::Result( Err( Some( value ))) => ensure_supported_value( value ),
Val::Future( _ ) => Err( DispatchError::UnsupportedType( "future".to_string() )),
Val::Stream( _ ) => Err( DispatchError::UnsupportedType( "stream".to_string() )),
Val::ErrorContext( _ ) => Err( DispatchError::UnsupportedType( "error-context".to_string() )),
_ => Ok(()),
}
}
#[cfg(test)] mod tests { include!( "plugin_instance_tests.rs" ); }