Skip to main content

wasm_link/
interface.rs

1use std::sync::Arc ;
2use std::collections::{ HashMap, HashSet };
3use futures::lock::Mutex ;
4use wasmtime::component::{ Linker, ResourceType, Val };
5
6use crate::{ Binding, PluginContext, PluginInstanceAsync, PluginInstanceSync };
7use crate::cardinality::Cardinality ;
8use crate::linker::{
9	dispatch_all,
10	dispatch_all_async,
11	dispatch_all_async_blocking,
12	dispatch_method,
13	dispatch_method_async,
14	dispatch_method_async_blocking,
15};
16use crate::resource_wrapper::ResourceWrapper ;
17
18/// A single WIT interface within a [`Binding`].
19///
20/// Each interface declares functions and resources that implementers must export.
21/// Note that the interface name is not a part of the struct but rather a key in
22/// a hash map provided to the Binding constructor. This is to prevent duplicate
23/// interface names.
24///
25/// ```
26/// # use std::collections::{ HashMap, HashSet };
27/// # use wasm_link::{ Binding, Interface, PluginContext, PluginInstanceSync, ResourceTable };
28/// # use wasm_link::cardinality::AtMostOne ;
29/// # struct Ctx { resource_table: ResourceTable }
30/// # impl PluginContext for Ctx {
31/// # 	fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
32/// # }
33/// let binding: Binding<String, Ctx, AtMostOne<String, PluginInstanceSync<Ctx>>> = Binding::new(
34/// 	"my:package",
35/// 	HashMap::from([
36/// 		( "interface-a".to_string(), Interface::new( HashMap::new(), HashSet::new() )),
37/// 		( "interface-b".to_string(), Interface::new( HashMap::new(), HashSet::new() )),
38/// 	]),
39/// 	AtMostOne( None ),
40/// );
41/// # let _ = binding;
42/// ```
43#[derive( Debug, Clone, Default )]
44pub struct Interface {
45	/// Functions exported by this interface
46	functions: HashMap<String, Function>,
47	/// Resource types defined by this interface
48	resources: HashSet<String>,
49}
50
51impl Interface {
52	/// Creates a new interface declaration.
53	pub fn new(
54		functions: HashMap<String, Function>,
55		resources: HashSet<String>,
56	) -> Self {
57		Self { functions, resources }
58	}
59
60	#[inline]
61	pub(crate) fn function( &self, name: &str ) -> Option<&Function> {
62		self.functions.get( name )
63	}
64
65	#[inline]
66	pub(crate) fn add_to_linker<PluginId, Ctx, Plugins>(
67		&self,
68		linker: &mut Linker<Ctx>,
69		package_name: &str,
70		interface_ident: &str,
71		interface_name: &str,
72		binding: &Binding<PluginId, Ctx, Plugins, PluginInstanceSync<Ctx>>,
73	) -> Result<(), wasmtime::Error>
74	where
75		PluginId: std::hash::Hash + Eq + Clone + Send + Sync + Into<Val> + 'static,
76		Ctx: PluginContext,
77		Plugins: Cardinality<PluginId, PluginInstanceSync<Ctx>> + 'static,
78		<Plugins as Cardinality<PluginId, PluginInstanceSync<Ctx>>>::Rebind<Arc<Mutex<PluginInstanceSync<Ctx>>>>: Send + Sync,
79		<Plugins as Cardinality<PluginId, PluginInstanceSync<Ctx>>>::Rebind<Arc<Mutex<PluginInstanceSync<Ctx>>>>: Cardinality<PluginId, Arc<Mutex<PluginInstanceSync<Ctx>>>>,
80		<<Plugins as Cardinality<PluginId, PluginInstanceSync<Ctx>>>::Rebind<Arc<Mutex<PluginInstanceSync<Ctx>>>> as Cardinality<PluginId, Arc<Mutex<PluginInstanceSync<Ctx>>>>>::Rebind<Val>: Into<Val>,
81	{
82		let mut linker_root = linker.root();
83		let mut linker_instance = linker_root.instance( interface_ident )?;
84
85		self.functions.iter().try_for_each(|( name, metadata )| {
86
87			let package_name_clone = package_name.to_string();
88			let interface_name_clone = interface_name.to_string();
89			let binding_clone = binding.clone();
90			let name_clone = name.clone();
91			let metadata_clone = metadata.clone();
92
93			macro_rules! link {( $dispatch: expr ) => {
94				linker_instance.func_new( name, move | ctx, _ty, args, results | Ok(
95					results[0] = $dispatch( &binding_clone, ctx, &package_name_clone, &interface_name_clone, &name_clone, &metadata_clone, args )
96				))
97			}}
98
99			match metadata.kind() {
100				FunctionKind::Freestanding => link!( dispatch_all ),
101				FunctionKind::Method => link!( dispatch_method ),
102			}
103
104		})?;
105
106		self.resources.iter().try_for_each(| resource | linker_instance
107			.resource( resource.as_str(), ResourceType::host::<Arc<ResourceWrapper<PluginId>>>(), ResourceWrapper::<PluginId>::drop )
108		)?;
109
110		Ok(())
111
112	}
113
114	#[inline]
115	pub(crate) fn add_to_linker_async<PluginId, Ctx, Plugins>(
116		&self,
117		linker: &mut Linker<Ctx>,
118		package_name: &str,
119		interface_ident: &str,
120		interface_name: &str,
121		binding: &Binding<PluginId, Ctx, Plugins, PluginInstanceAsync<Ctx>>,
122	) -> Result<(), wasmtime::Error>
123	where
124		PluginId: std::hash::Hash + Eq + Clone + Send + Sync + Into<Val> + 'static,
125		Ctx: PluginContext,
126		Plugins: Cardinality<PluginId, PluginInstanceAsync<Ctx>> + 'static,
127		<Plugins as Cardinality<PluginId, PluginInstanceAsync<Ctx>>>::Rebind<Arc<Mutex<PluginInstanceAsync<Ctx>>>>: Send + Sync,
128		<Plugins as Cardinality<PluginId, PluginInstanceAsync<Ctx>>>::Rebind<Arc<Mutex<PluginInstanceAsync<Ctx>>>>: Cardinality<PluginId, Arc<Mutex<PluginInstanceAsync<Ctx>>>>,
129		<<Plugins as Cardinality<PluginId, PluginInstanceAsync<Ctx>>>::Rebind<Arc<Mutex<PluginInstanceAsync<Ctx>>>> as Cardinality<PluginId, Arc<Mutex<PluginInstanceAsync<Ctx>>>>>::Rebind<Val>: Into<Val> + Send,
130	{
131		let mut linker_root = linker.root();
132		let mut linker_instance = linker_root.instance( interface_ident )?;
133
134		self.functions.iter().try_for_each(|( name, metadata )| {
135			let package_name = package_name.to_string();
136			let interface_name = interface_name.to_string();
137			let binding = binding.clone();
138			let function_name = name.clone();
139			let function = metadata.clone();
140
141			macro_rules! link_concurrent {( $dispatch: expr ) => {
142				linker_instance.func_new_concurrent( name, move | ctx, _ty, args, results | {
143					let package_name = package_name.clone();
144					let interface_name = interface_name.clone();
145					let binding = binding.clone();
146					let function_name = function_name.clone();
147					let function = function.clone();
148					Box::pin( async move {
149						results[0] = $dispatch(
150							&binding, ctx, &package_name, &interface_name, &function_name, &function, args,
151						).await;
152						Ok(())
153					})
154				})
155			}}
156
157			macro_rules! link_blocking {( $dispatch: expr ) => {
158				linker_instance.func_new_async( name, move | ctx, _ty, args, results | {
159					let package_name = package_name.clone();
160					let interface_name = interface_name.clone();
161					let binding = binding.clone();
162					let function_name = function_name.clone();
163					let function = function.clone();
164					Box::new( async move {
165						results[0] = $dispatch(
166							&binding, ctx, &package_name, &interface_name, &function_name, &function, args,
167						).await;
168						Ok(())
169					})
170				})
171			}}
172
173			match ( metadata.is_async(), metadata.kind() ) {
174				( true, FunctionKind::Freestanding ) => link_concurrent!( dispatch_all_async ),
175				( true, FunctionKind::Method ) => link_concurrent!( dispatch_method_async ),
176				( false, FunctionKind::Freestanding ) => link_blocking!( dispatch_all_async_blocking ),
177				( false, FunctionKind::Method ) => link_blocking!( dispatch_method_async_blocking ),
178			}
179		})?;
180
181		self.resources.iter().try_for_each(| resource | linker_instance.resource( resource.as_str(), ResourceType::host::<Arc<ResourceWrapper<PluginId>>>(), ResourceWrapper::<PluginId>::drop ))?;
182
183		Ok(())
184	}
185
186}
187
188/// Denotes whether a function is freestanding or a resource method.
189/// Constructors are treated as freestanding functions.
190///
191/// Determines how dispatch is routed during cross-plugin calls:
192/// freestanding functions broadcast to all plugins, while methods
193/// route to the specific plugin that owns the resource.
194#[derive( Debug, Clone, Copy, Eq, PartialEq )]
195pub enum FunctionKind {
196	/// A freestanding function — dispatched to all plugins.
197	Freestanding,
198	/// A resource method (has a `self` parameter) — routed to the plugin that owns the resource.
199	Method,
200}
201
202/// Metadata about a function declared by an interface.
203///
204/// Provides information needed during linking to wire up cross-plugin dispatch.
205#[derive( Debug, Clone )]
206pub struct Function {
207	/// Whether this function is freestanding or a resource method.
208	kind: FunctionKind,
209	/// The function's return kind for dispatch handling
210	return_kind: ReturnKind,
211	/// Whether the WIT function is declared with the `async` effect.
212	is_async: bool,
213}
214
215impl Function {
216	/// Creates a new function metadata entry.
217	pub fn new(
218		kind: FunctionKind,
219		return_kind: ReturnKind,
220	) -> Self {
221		Self { kind, return_kind, is_async: false }
222	}
223
224	/// Creates metadata for a WIT function declared with the `async` effect.
225	///
226	/// ```
227	/// use wasm_link::{ Function, FunctionKind, ReturnKind };
228	///
229	/// let function = Function::new_async(
230	/// 	FunctionKind::Freestanding,
231	/// 	ReturnKind::MayContainResources,
232	/// );
233	/// assert!( function.is_async() );
234	/// ```
235	pub fn new_async(
236		kind: FunctionKind,
237		return_kind: ReturnKind,
238	) -> Self {
239		Self { kind, return_kind, is_async: true }
240	}
241
242	/// The function's return kind for dispatch handling.
243	pub fn return_kind( &self ) -> ReturnKind { self.return_kind }
244
245	/// Whether this function is freestanding or a resource method.
246	pub fn kind( &self ) -> FunctionKind { self.kind }
247
248	/// Whether the WIT function is declared with the `async` effect.
249	///
250	/// ```
251	/// # use wasm_link::{ Function, FunctionKind, ReturnKind };
252	/// let function = Function::new( FunctionKind::Freestanding, ReturnKind::Void );
253	/// assert!( !function.is_async() );
254	/// ```
255	pub fn is_async( &self ) -> bool { self.is_async }
256
257}
258
259/// Categorizes a function's return for dispatch handling.
260///
261/// Determines how return values are processed during cross-plugin dispatch.
262/// Resources require special wrapping to track ownership across plugin
263/// boundaries, while plain data can be passed through directly.
264///
265/// # Choosing the Right Variant
266///
267/// **When uncertain, use [`MayContainResources`]( Self::MayContainResources ).**
268/// Using [`AssumeNoResources`]( Self::AssumeNoResources ) when resources are
269/// actually present will cause resource handles to be passed through unwrapped
270/// causing runtime exceptions.
271///
272/// [`AssumeNoResources`]( Self::AssumeNoResources ) is a performance optimization
273/// that skips the wrapping step. Only use it when you are certain the return type
274/// contains no resource handles anywhere in its structure (including nested within
275/// records, variants, lists, etc.).
276#[derive( Copy, Clone, Eq, PartialEq, Hash, Debug, Default )]
277pub enum ReturnKind {
278	/// Function returns nothing (void).
279	#[default] Void,
280	/// Function may return resource handles - always wraps safely.
281	///
282	/// Use this variant whenever resources might be present in the return value,
283	/// or when you're unsure. The performance overhead of wrapping is preferable
284	/// to the undefined behavior caused by unwrapped resource handles.
285	MayContainResources,
286	/// Assumes no resource handles are present - skips wrapping for performance.
287	///
288	/// **Warning:** Only use this if you are certain no resources are present.
289	/// If resources are returned but this variant is used, resource handles will
290	/// not be wrapped correctly, potentially causing undefined behavior in plugins.
291	/// When in doubt, use [`MayContainResources`](Self::MayContainResources) instead.
292	AssumeNoResources,
293}
294
295impl std::fmt::Display for ReturnKind {
296	fn fmt( &self, f: &mut std::fmt::Formatter ) -> Result<(), std::fmt::Error> {
297		match self {
298			Self::Void => write!( f, "Function returns no data" ),
299			Self::MayContainResources => write!( f, "Return type may contain resources" ),
300			Self::AssumeNoResources => write!( f, "Function is assumed to not return any resources" ),
301		}
302	}
303}