Skip to main content

wasm_link/
plugin_instance.rs

1use std::collections::HashMap ;
2use std::sync::Arc ;
3use futures::future::BoxFuture ;
4use futures::lock::Mutex ;
5use futures::task::{ FutureObj, Spawn };
6use thiserror::Error ;
7use wasmtime::component::{ Instance, Val };
8use wasmtime::Store ;
9
10use crate::{ Function, PluginContext, Remap, ReturnKind };
11use crate::resource_wrapper::{ ResourceCreationError, ResourceReceiveError };
12
13type CallLimiter<Ctx> = Box<dyn FnMut( &mut Store<Ctx>, &str, &str, &Function ) -> u64 + Send>;
14
15
16/// A synchronously instantiated plugin, ready for synchronous dispatch.
17///
18/// Created by calling [`Plugin::instantiate`]( crate::Plugin::instantiate ),
19/// or [`Plugin::link`]( crate::Plugin::link ).
20pub struct PluginInstanceSync<Ctx: 'static> {
21	state: PluginState<Ctx>,
22}
23
24/// An asynchronously instantiated plugin, ready for asynchronous dispatch.
25///
26/// Created by calling [`Plugin::instantiate_async`]( crate::Plugin::instantiate_async )
27/// or [`Plugin::link_async`]( crate::Plugin::link_async ). Calls are submitted to the
28/// executor supplied during instantiation. The plugin's Wasmtime [`Store`] remains
29/// independent and is serialized by an internal lock.
30pub struct PluginInstanceAsync<Ctx: 'static> {
31	state: Arc<Mutex<PluginState<Ctx>>>,
32	executor: Arc<dyn Spawn + Send + Sync>,
33}
34
35struct PluginState<Ctx: 'static> {
36	store: Store<Ctx>,
37	instance: Instance,
38	interface_remaps: HashMap<String, Remap>,
39	fuel_limiter: Option<CallLimiter<Ctx>>,
40	epoch_limiter: Option<CallLimiter<Ctx>>,
41}
42
43impl<Ctx: std::fmt::Debug + 'static> std::fmt::Debug for PluginInstanceSync<Ctx> {
44	fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::result::Result<(), std::fmt::Error> {
45		f.debug_struct( "PluginInstanceSync" )
46			.field( "data", &self.state.store.data() )
47			.field( "store", &self.state.store )
48			.field( "interface_remaps", &self.state.interface_remaps )
49			.field( "fuel_limiter", &self.state.fuel_limiter.as_ref().map(| _ | "<closure>" ))
50			.field( "epoch_limiter", &self.state.epoch_limiter.as_ref().map(| _ | "<closure>" ))
51			.finish_non_exhaustive()
52	}
53}
54
55impl<Ctx: 'static> std::fmt::Debug for PluginInstanceAsync<Ctx> {
56	fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::result::Result<(), std::fmt::Error> {
57		f.debug_struct( "PluginInstanceAsync" )
58			.field( "state", &"<serialized store>" )
59			.field( "executor", &"<executor>" )
60			.finish_non_exhaustive()
61	}
62}
63
64/// Errors that can occur when dispatching a function call to plugins.
65///
66/// Returned inside a cardinality wrapper from
67/// [`Binding::dispatch`]( crate::binding::Binding::dispatch )
68/// when a function call fails at runtime.
69#[derive( Error, Debug )]
70pub enum DispatchError {
71	/// Failed to acquire lock on plugin instance (another call is in progress).
72	#[error( "Lock Rejected" )] LockRejected,
73	/// The specified interface path doesn't match any known interface.
74	#[error( "Invalid Interface Path: {0}" )] InvalidInterfacePath( String ),
75	/// The specified function doesn't exist on the interface.
76	#[error( "Invalid Function: {0}" )] InvalidFunction( String ),
77	/// Function was expected to return a value but didn't.
78	#[error( "Missing Response" )] MissingResponse,
79	/// The WASM function threw an exception during execution.
80	#[error( "Runtime Exception" )] RuntimeException( wasmtime::Error ),
81	/// The provided arguments don't match the function signature.
82	#[error( "Invalid Argument List" )] InvalidArgumentList,
83	/// Async types (`Future`, `Stream`, `ErrorContext`) are not yet supported for cross-plugin transfer.
84	#[error( "Unsupported type: {0}" )] UnsupportedType( String ),
85	/// The executor supplied for an async plugin rejected a dispatch task.
86	#[error( "Async executor unavailable" )] ExecutorUnavailable,
87	/// Failed to create a resource handle for cross-plugin transfer.
88	#[error( "Resource Create Error: {0}" )] ResourceCreationError( #[from] ResourceCreationError ),
89	/// Failed to receive a resource handle from another plugin.
90	#[error( "Resource Receive Error: {0}" )] ResourceReceiveError( #[from] ResourceReceiveError ),
91}
92
93impl From<DispatchError> for Val {
94	fn from( error: DispatchError ) -> Val { match error {
95		DispatchError::LockRejected => Val::Variant( "lock-rejected".to_string(), None ),
96		DispatchError::InvalidInterfacePath( package ) => Val::Variant( "invalid-interface-path".to_string(), Some( Box::new( Val::String( package )))),
97		DispatchError::InvalidFunction( function ) => Val::Variant( "invalid-function".to_string(), Some( Box::new( Val::String( function )))),
98		DispatchError::MissingResponse => Val::Variant( "missing-response".to_string(), None ),
99		DispatchError::RuntimeException( exception ) => Val::Variant( "runtime-exception".to_string(), Some( Box::new( Val::String( exception.to_string() )))),
100		DispatchError::InvalidArgumentList => Val::Variant( "invalid-argument-list".to_string(), None ),
101		DispatchError::UnsupportedType( name ) => Val::Variant( "unsupported-type".to_string(), Some( Box::new( Val::String( name )))),
102		DispatchError::ExecutorUnavailable => Val::Variant( "executor-unavailable".to_string(), None ),
103		DispatchError::ResourceCreationError( err ) => err.into(),
104		DispatchError::ResourceReceiveError( err ) => err.into(),
105	}}
106}
107
108impl<Ctx: PluginContext + 'static> PluginInstanceSync<Ctx> {
109	pub(crate) fn new_sync(
110		store: Store<Ctx>,
111		instance: Instance,
112		interface_remaps: HashMap<String, Remap>,
113		fuel_limiter: Option<CallLimiter<Ctx>>,
114		epoch_limiter: Option<CallLimiter<Ctx>>,
115	) -> Self {
116		Self { state: PluginState {
117			store,
118			instance,
119			interface_remaps,
120			fuel_limiter,
121			epoch_limiter,
122		}}
123	}
124
125	pub(crate) fn dispatch(
126		&mut self,
127		package_name: &str,
128		interface_name: &str,
129		function_name: &str,
130		function: &Function,
131		data: &[Val],
132	) -> Result<Val, DispatchError> {
133		self.state.dispatch( package_name, interface_name, function_name, function, data )
134	}
135}
136
137impl<Ctx: PluginContext + 'static> PluginInstanceAsync<Ctx> {
138	pub(crate) fn new(
139		store: Store<Ctx>,
140		instance: Instance,
141		interface_remaps: HashMap<String, Remap>,
142		fuel_limiter: Option<CallLimiter<Ctx>>,
143		epoch_limiter: Option<CallLimiter<Ctx>>,
144		executor: impl Spawn + Send + Sync + 'static,
145	) -> Self {
146		Self {
147			state: Arc::new( Mutex::new( PluginState {
148				store,
149				instance,
150				interface_remaps,
151				fuel_limiter,
152				epoch_limiter,
153			})),
154			executor: Arc::new( executor ),
155		}
156	}
157
158	pub(crate) async fn dispatch_async(
159		&self,
160		package_name: &str,
161		interface_name: &str,
162		function_name: &str,
163		function: &Function,
164		data: &[Val],
165	) -> Result<Val, DispatchError> {
166		let state = Arc::clone( &self.state );
167		let package_name = package_name.to_string();
168		let interface_name = interface_name.to_string();
169		let function_name = function_name.to_string();
170		let function = function.clone();
171		let data = data.to_vec();
172		let ( response, result ) = futures::channel::oneshot::channel();
173		let task: BoxFuture<'static, ()> = Box::pin( async move {
174			let result = state.lock().await.dispatch_async(
175				&package_name,
176				&interface_name,
177				&function_name,
178				&function,
179				&data,
180			).await;
181			let _ = response.send( result );
182		});
183		self.executor.spawn_obj( FutureObj::new( task ))
184			.map_err(| _ | DispatchError::ExecutorUnavailable )?;
185		result.await.map_err(| _ | DispatchError::ExecutorUnavailable )?
186	}
187
188}
189
190impl<Ctx: PluginContext + 'static> PluginState<Ctx> {
191	const PLACEHOLDER_VAL: Val = Val::Option( None );
192	const VOID_RETURN_VAL: Val = Val::Option( None );
193
194	fn dispatch(
195		&mut self,
196		package_name: &str,
197		interface_name: &str,
198		function_name: &str,
199		function: &Function,
200		data: &[Val],
201	) -> Result<Val, DispatchError> {
202		ensure_supported_values( data )?;
203		let mut buffer = self.prepare_call( package_name, interface_name, function_name, function )?;
204		let ( exported_interface_path, exported_function_name ) = self.resolve_export( package_name, interface_name, function_name );
205		let func = self.function( &exported_interface_path, &exported_function_name )?;
206		let call_result = func.call( &mut self.store, data, &mut buffer );
207		Self::finish_call( function, buffer, call_result )
208	}
209
210	async fn dispatch_async(
211		&mut self,
212		package_name: &str,
213		interface_name: &str,
214		function_name: &str,
215		function: &Function,
216		data: &[Val],
217	) -> Result<Val, DispatchError> {
218		ensure_supported_values( data )?;
219		let mut buffer = self.prepare_call( package_name, interface_name, function_name, function )?;
220		let ( exported_interface_path, exported_function_name ) = self.resolve_export( package_name, interface_name, function_name );
221		let func = self.function( &exported_interface_path, &exported_function_name )?;
222		let call_result = func.call_async( &mut self.store, data, &mut buffer ).await;
223		Self::finish_call( function, buffer, call_result )
224	}
225
226	fn prepare_call(
227		&mut self,
228		package_name: &str,
229		interface_name: &str,
230		function_name: &str,
231		function: &Function,
232	) -> Result<Vec<Val>, DispatchError> {
233		let canonical_interface_path = format!( "{}/{}", package_name, interface_name );
234		if let Some( mut limiter ) = self.fuel_limiter.take() {
235			let fuel = limiter( &mut self.store, &canonical_interface_path, function_name, function );
236			self.fuel_limiter = Some( limiter );
237			self.store.set_fuel( fuel ).map_err( DispatchError::RuntimeException )?;
238		}
239		if let Some( mut limiter ) = self.epoch_limiter.take() {
240			let ticks = limiter( &mut self.store, &canonical_interface_path, function_name, function );
241			self.epoch_limiter = Some( limiter );
242			self.store.set_epoch_deadline( ticks );
243		}
244		Ok( match function.return_kind() != ReturnKind::Void {
245			true => vec![ Self::PLACEHOLDER_VAL ],
246			false => Vec::with_capacity( 0 ),
247		})
248	}
249
250	fn function( &mut self, interface_path: &str, function_name: &str ) -> Result<wasmtime::component::Func, DispatchError> {
251		let interface_index = self.instance
252			.get_export_index( &mut self.store, None, interface_path )
253			.ok_or_else(|| DispatchError::InvalidInterfacePath( interface_path.to_string() ))?;
254		let func_index = self.instance
255			.get_export_index( &mut self.store, Some( &interface_index ), function_name )
256			.ok_or_else(|| DispatchError::InvalidFunction( format!( "{interface_path}:{function_name}" )))?;
257		self.instance
258			.get_func( &mut self.store, func_index )
259			.ok_or_else(|| DispatchError::InvalidFunction( format!( "{interface_path}:{function_name}" )))
260	}
261
262	fn finish_call(
263		function: &Function,
264		mut buffer: Vec<Val>,
265		call_result: Result<(), wasmtime::Error>,
266	) -> Result<Val, DispatchError> {
267		call_result.map_err( DispatchError::RuntimeException )?;
268		let result = match function.return_kind() != ReturnKind::Void {
269			true => buffer.pop().ok_or( DispatchError::MissingResponse )?,
270			false => Self::VOID_RETURN_VAL,
271		};
272		ensure_supported_value( &result )?;
273		Ok( result )
274	}
275
276	fn resolve_export( &self, package_name: &str, interface_name: &str, function_name: &str ) -> (String, String) {
277		match self.interface_remaps.get( interface_name ) {
278			Some( remap ) => (
279				format!( "{}/{}", package_name, remap.interface_name( interface_name )),
280				remap.item_name( function_name ).to_string(),
281			),
282			None => (
283				format!( "{}/{}", package_name, interface_name ),
284				function_name.to_string(),
285			),
286		}
287	}
288
289}
290
291fn ensure_supported_values( values: &[Val] ) -> Result<(), DispatchError> {
292	values.iter().try_for_each( ensure_supported_value )
293}
294
295fn ensure_supported_value( value: &Val ) -> Result<(), DispatchError> {
296	match value {
297		Val::List( values ) | Val::Tuple( values ) => ensure_supported_values( values ),
298		Val::Map( values ) => values.iter().try_for_each(|( key, value )| {
299			ensure_supported_value( key )?;
300			ensure_supported_value( value )
301		}),
302		Val::Record( values ) => values.iter().try_for_each(|( _, value )| ensure_supported_value( value )),
303		Val::Variant( _, Some( value ))
304		| Val::Option( Some( value ))
305		| Val::Result( Ok( Some( value )))
306		| Val::Result( Err( Some( value ))) => ensure_supported_value( value ),
307		Val::Future( _ ) => Err( DispatchError::UnsupportedType( "future".to_string() )),
308		Val::Stream( _ ) => Err( DispatchError::UnsupportedType( "stream".to_string() )),
309		Val::ErrorContext( _ ) => Err( DispatchError::UnsupportedType( "error-context".to_string() )),
310		_ => Ok(()),
311	}
312}
313
314#[cfg(test)] mod tests { include!( "plugin_instance_tests.rs" ); }