Skip to main content

wasm_link/
binding.rs

1//! Binding specification and metadata types.
2//!
3//! A [`Binding`] defines an abstract contract specifying what plugins must implement
4//! (via plugs) or what they could depend on (via sockets). It bundles one or more WIT
5//! [`Interface`]s under a single identifier.
6
7use std::sync::Arc ;
8use std::collections::HashMap ;
9use futures::lock::Mutex ;
10use wasmtime::component::{ Linker, Val };
11
12use crate::{ Interface, PluginContext };
13use crate::cardinality::{ Any, AtLeastOne, AtMostOne, Cardinality, ExactlyOne };
14use crate::plugin_instance::{ PluginInstanceAsync, PluginInstanceSync };
15
16
17
18type PluginSockets<PluginId, Plugins, Instance> =
19	<Plugins as Cardinality<PluginId, Instance>>::Rebind<Arc<Mutex<Instance>>> ;
20
21type DispatchResults<PluginId, Plugins, Instance> =
22	<PluginSockets<PluginId, Plugins, Instance> as Cardinality<PluginId, Arc<Mutex<Instance>>>>::Rebind<
23		Result<wasmtime::component::Val, crate::DispatchError>
24	>;
25
26type DispatchVals<PluginId, Plugins, Instance> =
27	<PluginSockets<PluginId, Plugins, Instance> as Cardinality<PluginId, Arc<Mutex<Instance>>>>::Rebind<
28		wasmtime::component::Val
29	>;
30
31struct BindingData<PluginId, Plugins, Instance>
32where
33	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
34	Instance: Send + 'static,
35	Plugins: Cardinality<PluginId, Instance>,
36	PluginSockets<PluginId, Plugins, Instance>: Send + Sync,
37{
38	package_name: String,
39	interfaces: HashMap<String, Interface>,
40	plugins: PluginSockets<PluginId, Plugins, Instance>,
41}
42
43/// An abstract contract specifying what plugins must implement (via plugs) or what
44/// they could depend on (via sockets). It bundles one or more WIT [`Interface`]s
45/// under a single package name.
46///
47/// `Binding` is a handle to shared state. Cloning a `Binding` creates another handle
48/// to the same underlying binding, enabling shared dependencies where multiple
49/// plugins depend on the same binding.
50///
51/// ```
52/// # use std::collections::{ HashMap, HashSet };
53/// # use wasm_link::{ Binding, Interface, Function, FunctionKind, ReturnKind, Plugin, Engine, Component, Linker, ResourceTable };
54/// # use wasm_link::cardinality::ExactlyOne ;
55/// # struct Ctx { resource_table: ResourceTable }
56/// # impl wasm_link::PluginContext for Ctx {
57/// # 	fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
58/// # }
59/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
60/// # let engine = Engine::default();
61/// # let linker = Linker::new( &engine );
62/// # let plugin = Plugin::new( Component::new( &engine, "(component)" )?, Ctx { resource_table: ResourceTable::new() }).instantiate( &engine, &linker )?;
63/// let binding: Binding<String, Ctx> = Binding::new(
64/// 	"my:package",
65/// 	HashMap::from([
66/// 		( "api".to_string(), Interface::new(
67/// 			HashMap::from([( "get-value".into(), Function::new(
68/// 				FunctionKind::Freestanding,
69/// 				ReturnKind::MayContainResources,
70/// 			))]),
71/// 			HashSet::from([ "my-resource".to_string() ]),
72/// 		)),
73/// 	]),
74/// 	ExactlyOne( "my-plugin".to_string(), plugin ),
75/// );
76///
77/// // Clone for shared dependencies - both refer to the same binding
78/// let binding_clone = binding.clone();
79/// # let binding_any_clone = binding.into_any().clone();
80/// # Ok(())
81/// # }
82/// ```
83///
84/// # Type Parameters
85/// - `PluginId`: Unique identifier type for plugins (e.g., `String`, `UUID`)
86/// - `Ctx`: Context stored in each plugin's Wasmtime store
87/// - `Plugins`: Cardinality wrapper containing the plugin instances
88/// - `Instance`: [`PluginInstanceSync`] or [`PluginInstanceAsync`]
89pub struct Binding<PluginId, Ctx, Plugins = ExactlyOne<PluginId, PluginInstanceSync<Ctx>>, Instance = PluginInstanceSync<Ctx>>(
90	Arc<BindingData<PluginId, Plugins, Instance>>,
91	std::marker::PhantomData<fn() -> Ctx>,
92)
93where
94	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
95	Ctx: PluginContext + 'static,
96	Instance: Send + 'static,
97	Plugins: Cardinality<PluginId, Instance> + 'static,
98	PluginSockets<PluginId, Plugins, Instance>: Send + Sync;
99
100impl<PluginId, Ctx, Plugins, Instance> Clone for Binding<PluginId, Ctx, Plugins, Instance>
101where
102	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
103	Ctx: PluginContext + 'static,
104	Instance: Send + 'static,
105	Plugins: Cardinality<PluginId, Instance> + 'static,
106	PluginSockets<PluginId, Plugins, Instance>: Send + Sync,
107{
108	/// Creates another handle to the same underlying binding, enabling shared dependencies where
109	/// multiple plugins depend on the same binding.
110	fn clone( &self ) -> Self {
111		Self( Arc::clone( &self.0 ), std::marker::PhantomData )
112	}
113}
114
115impl<PluginId, Ctx, Plugins, Instance> std::fmt::Debug for Binding<PluginId, Ctx, Plugins, Instance>
116where
117	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + std::fmt::Debug + 'static,
118	Ctx: PluginContext + std::fmt::Debug + 'static,
119	Instance: Send + 'static,
120	Plugins: Cardinality<PluginId, Instance> + 'static,
121	PluginSockets<PluginId, Plugins, Instance>: Send + Sync + std::fmt::Debug,
122{
123	fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::fmt::Result {
124		f.debug_struct( "Binding" )
125			.field( "package_name", &self.0.package_name )
126			.field( "interfaces", &self.0.interfaces )
127			.field( "plugins", &self.0.plugins )
128			.finish()
129	}
130}
131
132impl<PluginId, Ctx, Plugins, Instance> Binding<PluginId, Ctx, Plugins, Instance>
133where
134	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
135	Ctx: PluginContext + 'static,
136	Instance: Send + 'static,
137	Plugins: Cardinality<PluginId, Instance> + 'static,
138	PluginSockets<PluginId, Plugins, Instance>: Cardinality<PluginId, Arc<Mutex<Instance>>> + Send + Sync,
139{
140
141	/// Creates a new binding specification.
142	pub fn new(
143		package_name: impl Into<String>,
144		interfaces: HashMap<String, Interface>,
145		plugins: Plugins
146	) -> Self {
147		Self( Arc::new( BindingData {
148			package_name: package_name.into(),
149			interfaces,
150			plugins: plugins.map_mut(| plugin | Arc::new( Mutex::new( plugin ))),
151		}), std::marker::PhantomData )
152	}
153
154	pub(crate) fn plugins( &self ) -> &PluginSockets<PluginId, Plugins, Instance> {
155		&self.0.plugins
156	}
157}
158
159impl<PluginId, Ctx, Plugins> Binding<PluginId, Ctx, Plugins, PluginInstanceSync<Ctx>>
160where
161	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
162	Ctx: PluginContext + 'static,
163	Plugins: Cardinality<PluginId, PluginInstanceSync<Ctx>> + 'static,
164	PluginSockets<PluginId, Plugins, PluginInstanceSync<Ctx>>: Cardinality<PluginId, Arc<Mutex<PluginInstanceSync<Ctx>>>> + Send + Sync,
165{
166
167	pub(crate) fn add_to_linker( binding: &Binding<PluginId, Ctx, Plugins>, linker: &mut Linker<Ctx> ) -> Result<(), wasmtime::Error>
168	where
169		PluginId: Into<Val>,
170		DispatchVals<PluginId, Plugins, PluginInstanceSync<Ctx>>: Into<Val>,
171	{
172		binding.0.interfaces.iter().try_for_each(|( name, interface )| {
173			let interface_ident = format!( "{}/{}", binding.0.package_name, name );
174			interface.add_to_linker( linker, &binding.0.package_name, &interface_ident, name, binding )
175		})
176	}
177
178	/// Dispatches a function call to all plugins implementing this binding.
179	///
180	/// This is used for external dispatch (calling into the plugin graph from outside).
181	/// The result is wrapped in a type matching the binding's cardinality.
182	///
183	/// # Arguments
184	/// * `interface_name` - The interface name within this binding (e.g., "example")
185	/// * `function_name` - The function name within the interface (e.g., "get-value")
186	/// * `args` - Arguments to pass to the function
187	///
188	/// # Returns
189	/// A cardinality wrapper containing `Result<Val, DispatchError>` for each plugin.
190	/// For [`ReturnKind::Void`]( crate::ReturnKind::Void ), the value is an empty tuple
191	/// (`Val::Option( None )`) placeholder.
192	///
193	/// # Errors
194	/// Returns an error if the interface or function is not found in this binding.
195	pub fn dispatch(
196		&self,
197		interface_name: &str,
198		function_name: &str,
199		args: &[wasmtime::component::Val],
200	) -> Result<DispatchResults<PluginId, Plugins, PluginInstanceSync<Ctx>>, crate::DispatchError> {
201
202		let interface = self.0.interfaces.get( interface_name )
203			.ok_or_else(|| crate::DispatchError::InvalidInterfacePath( format!( "{}/{}", self.0.package_name, interface_name )))?;
204
205		let function = interface.function( function_name )
206			.ok_or_else(|| crate::DispatchError::InvalidFunction( function_name.to_string() ))?;
207
208		Ok( self.0.plugins.map(| _, plugin | plugin
209			.try_lock().ok_or( crate::DispatchError::LockRejected )
210			.and_then(| mut lock | lock.dispatch(
211				&self.0.package_name,
212				interface_name,
213				function_name,
214				function,
215				args,
216			))
217		))
218
219	}
220
221
222}
223
224impl<PluginId, Ctx, Plugins> Binding<PluginId, Ctx, Plugins, PluginInstanceAsync<Ctx>>
225where
226	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
227	Ctx: PluginContext + 'static,
228	Plugins: Cardinality<PluginId, PluginInstanceAsync<Ctx>> + 'static,
229	PluginSockets<PluginId, Plugins, PluginInstanceAsync<Ctx>>: Cardinality<PluginId, Arc<Mutex<PluginInstanceAsync<Ctx>>>> + Send + Sync,
230{
231	pub(crate) fn add_to_linker_async( binding: &Self, linker: &mut Linker<Ctx> ) -> Result<(), wasmtime::Error>
232	where
233		PluginId: Into<Val>,
234		DispatchVals<PluginId, Plugins, PluginInstanceAsync<Ctx>>: Into<Val> + Send,
235	{
236		binding.0.interfaces.iter().try_for_each(|( name, interface )| {
237			let interface_ident = format!( "{}/{}", binding.0.package_name, name );
238			interface.add_to_linker_async( linker, &binding.0.package_name, &interface_ident, name, binding )
239		})
240	}
241
242	/// Asynchronously dispatches a function call to all plugins implementing this binding.
243	///
244	/// This method waits for a busy plugin instead of returning [`DispatchError::LockRejected`](crate::DispatchError::LockRejected).
245	/// It is required for instances created by [`Plugin::instantiate_async`](crate::Plugin::instantiate_async)
246	/// or [`Plugin::link_async`](crate::Plugin::link_async).
247	///
248	/// # Example
249	///
250	/// ```
251	/// # use std::collections::{ HashMap, HashSet };
252	/// # use wasm_link::{ Binding, Component, Engine, Function, FunctionKind, Interface, Linker, Plugin, PluginContext, ResourceTable, ReturnKind, Val };
253	/// # use wasm_link::cardinality::ExactlyOne;
254	/// # struct Context { table: ResourceTable }
255	/// # impl PluginContext for Context { fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.table } }
256	/// # fn main() -> Result<(), Box<dyn std::error::Error>> { futures::executor::block_on( async {
257	/// # let engine = Engine::default();
258	/// # let linker = Linker::new( &engine );
259	/// # let executor = futures::executor::ThreadPool::new()?;
260	/// # let component = Component::new( &engine, r#"(component
261	/// # 	(core module $m (func (export "get") (result i32) i32.const 42))
262	/// # 	(core instance $i (instantiate $m))
263	/// # 	(func $get (result u32) (canon lift (core func $i "get")))
264	/// # 	(instance $root (export "get" (func $get)))
265	/// # 	(export "example:plugin/root" (instance $root))
266	/// # )"# )?;
267	/// # let plugin = Plugin::new( component, Context { table: ResourceTable::new() })
268	/// # 	.instantiate_async( &engine, &linker, executor ).await?;
269	/// # let binding = Binding::new(
270	/// # 	"example:plugin",
271	/// # 	HashMap::from([( "root".to_string(), Interface::new(
272	/// # 		HashMap::from([( "get".to_string(), Function::new( FunctionKind::Freestanding, ReturnKind::MayContainResources ))]),
273	/// # 		HashSet::new(),
274	/// # 	))]),
275	/// # 	ExactlyOne( "plugin".to_string(), plugin ),
276	/// # );
277	/// let result = binding.dispatch_async( "root", "get", &[] ).await?;
278	/// assert!( matches!( result, ExactlyOne( _, Ok( Val::U32( 42 )))));
279	/// # Ok(()) }) }
280	/// ```
281	///
282	/// # Errors
283	/// Returns an error if the interface or function is not found in this binding.
284	pub async fn dispatch_async(
285		&self,
286		interface_name: &str,
287		function_name: &str,
288		args: &[wasmtime::component::Val],
289	) -> Result<DispatchResults<PluginId, Plugins, PluginInstanceAsync<Ctx>>, crate::DispatchError>
290	where
291		PluginId: Into<Val>,
292		DispatchResults<PluginId, Plugins, PluginInstanceAsync<Ctx>>: Send,
293	{
294		let interface = self.0.interfaces.get( interface_name )
295			.ok_or_else(|| crate::DispatchError::InvalidInterfacePath( format!( "{}/{}", self.0.package_name, interface_name )))?;
296		let function = interface.function( function_name )
297			.ok_or_else(|| crate::DispatchError::InvalidFunction( function_name.to_string() ))?;
298		let package_name = self.0.package_name.clone();
299		let interface_name = interface_name.to_string();
300		let function_name = function_name.to_string();
301		let function = function.clone();
302		let args = args.to_vec();
303
304		Ok( self.0.plugins.map_async(| _, plugin | {
305			let package_name = package_name.clone();
306			let interface_name = interface_name.clone();
307			let function_name = function_name.clone();
308			let function = function.clone();
309			let args = args.clone();
310			async move {
311				plugin.lock().await.dispatch_async(
312					&package_name,
313					&interface_name,
314					&function_name,
315					&function,
316					&args,
317				).await
318			}
319		}).await )
320	}
321
322}
323
324/// Type-erased binding wrapper for heterogeneous socket lists.
325///
326/// Use when a plugin's sockets include bindings with different cardinalities.
327#[derive( Debug )]
328pub enum BindingAny<PluginId, Ctx, Instance = PluginInstanceSync<Ctx>>
329where
330	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
331	Ctx: PluginContext + 'static,
332	Instance: Send + 'static,
333{
334	/// Exactly one plugin implementation.
335	ExactlyOne( Binding<PluginId, Ctx, ExactlyOne<PluginId, Instance>, Instance> ),
336	/// Zero or one plugin implementation.
337	AtMostOne( Binding<PluginId, Ctx, AtMostOne<PluginId, Instance>, Instance> ),
338	/// One or more plugin implementations.
339	AtLeastOne( Binding<PluginId, Ctx, AtLeastOne<PluginId, Instance>, Instance> ),
340	/// Zero or more plugin implementations.
341	Any( Binding<PluginId, Ctx, Any<PluginId, Instance>, Instance> ),
342}
343
344impl<PluginId, Ctx> BindingAny<PluginId, Ctx, PluginInstanceSync<Ctx>>
345where
346	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + Into<Val> + 'static,
347	Ctx: PluginContext + 'static,
348{
349	pub(crate) fn add_to_linker( &self, linker: &mut Linker<Ctx> ) -> Result<(), wasmtime::Error> {
350		match self {
351			Self::ExactlyOne( binding ) => Binding::add_to_linker( binding, linker ),
352			Self::AtMostOne( binding ) => Binding::add_to_linker( binding, linker ),
353			Self::AtLeastOne( binding ) => Binding::add_to_linker( binding, linker ),
354			Self::Any( binding ) => Binding::add_to_linker( binding, linker ),
355		}
356	}
357
358}
359
360impl<PluginId, Ctx> BindingAny<PluginId, Ctx, PluginInstanceAsync<Ctx>>
361where
362	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + Into<Val> + 'static,
363	Ctx: PluginContext + 'static,
364{
365	pub(crate) fn add_to_linker_async( &self, linker: &mut Linker<Ctx> ) -> Result<(), wasmtime::Error> {
366		match self {
367			Self::ExactlyOne( binding ) => Binding::add_to_linker_async( binding, linker ),
368			Self::AtMostOne( binding ) => Binding::add_to_linker_async( binding, linker ),
369			Self::AtLeastOne( binding ) => Binding::add_to_linker_async( binding, linker ),
370			Self::Any( binding ) => Binding::add_to_linker_async( binding, linker ),
371		}
372	}
373}
374
375impl<PluginId, Ctx, Instance> From<Binding<PluginId, Ctx, ExactlyOne<PluginId, Instance>, Instance>> for BindingAny<PluginId, Ctx, Instance>
376where
377	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
378	Ctx: PluginContext + 'static,
379	Instance: Send + 'static,
380{
381	fn from( binding: Binding<PluginId, Ctx, ExactlyOne<PluginId, Instance>, Instance> ) -> Self {
382		Self::ExactlyOne( binding )
383	}
384}
385
386impl<PluginId, Ctx, Instance> From<Binding<PluginId, Ctx, AtMostOne<PluginId, Instance>, Instance>> for BindingAny<PluginId, Ctx, Instance>
387where
388	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
389	Ctx: PluginContext + 'static,
390	Instance: Send + 'static,
391{
392	fn from( binding: Binding<PluginId, Ctx, AtMostOne<PluginId, Instance>, Instance> ) -> Self {
393		Self::AtMostOne( binding )
394	}
395}
396
397impl<PluginId, Ctx, Instance> From<Binding<PluginId, Ctx, AtLeastOne<PluginId, Instance>, Instance>> for BindingAny<PluginId, Ctx, Instance>
398where
399	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
400	Ctx: PluginContext + 'static,
401	Instance: Send + 'static,
402{
403	fn from( binding: Binding<PluginId, Ctx, AtLeastOne<PluginId, Instance>, Instance> ) -> Self {
404		Self::AtLeastOne( binding )
405	}
406}
407
408impl<PluginId, Ctx, Instance> From<Binding<PluginId, Ctx, Any<PluginId, Instance>, Instance>> for BindingAny<PluginId, Ctx, Instance>
409where
410	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
411	Ctx: PluginContext + 'static,
412	Instance: Send + 'static,
413{
414	fn from( binding: Binding<PluginId, Ctx, Any<PluginId, Instance>, Instance> ) -> Self {
415		Self::Any( binding )
416	}
417}
418
419impl<PluginId, Ctx, Plugins, Instance> Binding<PluginId, Ctx, Plugins, Instance>
420where
421	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
422	Ctx: PluginContext + 'static,
423	Instance: Send + 'static,
424	Plugins: Cardinality<PluginId, Instance>,
425	PluginSockets<PluginId, Plugins, Instance>: Send + Sync,
426	BindingAny<PluginId, Ctx, Instance>: From<Binding<PluginId, Ctx, Plugins, Instance>>,
427{
428	/// Converts this binding into a type-erased [`BindingAny`] for heterogeneous socket lists.
429	pub fn into_any( self ) -> BindingAny<PluginId, Ctx, Instance> {
430		self.into()
431	}
432}
433
434impl<PluginId, Ctx, Instance> Clone for BindingAny<PluginId, Ctx, Instance>
435where
436	PluginId: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
437	Ctx: PluginContext + 'static,
438	Instance: Send + 'static,
439{
440	/// Creates another handle to the same underlying binding, enabling shared dependencies where
441	/// multiple plugins depend on the same binding.
442	fn clone( &self ) -> Self {
443		match self {
444			Self::ExactlyOne( binding ) => Self::ExactlyOne( binding.clone() ),
445			Self::AtMostOne( binding ) => Self::AtMostOne( binding.clone() ),
446			Self::AtLeastOne( binding ) => Self::AtLeastOne( binding.clone() ),
447			Self::Any( binding ) => Self::Any( binding.clone() ),
448		}
449	}
450}