wasm_link/plugin_tree.rs
1//! Plugin dependency tree construction.
2//!
3//! The [`PluginTree`] represents an unloaded plugin dependency tree. Internally,
4//! multiple plugins may share a dependency (so it's technically a DAG), but this
5//! is an implementation detail - conceptually it's a tree rooted at the entry
6//! interface, and cycles are forbidden.
7//!
8//! Call [`PluginTree::load`] to compile the WASM components and link them together.
9
10use std::collections::HashMap ;
11use itertools::Itertools ;
12use thiserror::Error ;
13use wasmtime::Engine ;
14use wasmtime::component::Linker ;
15
16use crate::interface::{ InterfaceData };
17use crate::plugin::PluginData ;
18use crate::plugin_tree_head::PluginTreeHead ;
19use crate::loading::{ LoadError, load_plugin_tree };
20use crate::utils::{ PartialSuccess, PartialResult, Merge };
21
22
23
24/// Error that can occur during plugin tree construction.
25///
26/// These errors occur in [`PluginTree::new`] when building the dependency graph,
27/// before any WASM compilation happens.
28#[derive( Debug, Error )]
29pub enum PluginTreeError<I: InterfaceData, P: PluginData> {
30 /// Failed to read interface metadata (e.g., couldn't determine the interface's id).
31 #[error( "InterfaceData error: {0}" )] InterfaceDataError( I::Error ),
32 /// Failed to read plugin metadata (e.g., couldn't determine the plugin's plug).
33 #[error( "PluginData error: {0}" )] PluginDataError( P::Error ),
34 /// Plugins reference an interface that wasn't provided in the interfaces list.
35 #[error( "Missing interface {} required by {} plugins", interface_id, plugins.len() )]
36 MissingInterface { interface_id: I::Id, plugins: Vec<P> },
37}
38
39
40
41/// An unloaded plugin dependency tree.
42///
43/// Built from a list of plugins by grouping them according to the interfaces
44/// they implement (their plug) and depend on (their sockets). The structure
45/// has a single root interface and cycles are forbidden, so it can be thought
46/// of as a tree (though internally, multiple plugins may share a dependency).
47///
48/// This is the pre-compilation representation - no WASM has been loaded yet.
49///
50/// Call [`load`]( Self::load ) to compile WASM components and link dependencies,
51/// producing a [`PluginTreeHead`] for dispatching function calls.
52///
53/// # Type Parameters
54/// - `I`: [`InterfaceData`] implementation for loading interface metadata
55/// - `P`: [`PluginData`] implementation for loading plugin metadata
56///
57/// # Example
58///
59/// ```
60/// use wasm_link::{
61/// InterfaceData, InterfaceCardinality, FunctionData, ReturnKind,
62/// PluginData, PluginCtxView, PluginTree, Engine, Component, Linker, ResourceTable,
63/// };
64///
65/// # #[derive( Clone )]
66/// # struct Func { name: &'static str, return_kind: ReturnKind }
67/// # impl FunctionData for Func {
68/// # fn name( &self ) -> &str { unreachable!() }
69/// # fn return_kind( &self ) -> ReturnKind { unreachable!() }
70/// # fn is_method( &self ) -> bool { unreachable!() }
71/// # }
72/// #
73/// struct Interface { id: &'static str, funcs: Vec<Func> }
74/// impl InterfaceData for Interface {
75/// /* ... */
76/// # type Id = &'static str ;
77/// # type Error = std::convert::Infallible ;
78/// # type Function = Func ;
79/// # type FunctionIter<'a> = std::slice::Iter<'a, Func> ;
80/// # type ResourceIter<'a> = std::iter::Empty<&'a String> ;
81/// # fn id( &self ) -> Result<&Self::Id, Self::Error> { Ok( &self.id ) }
82/// # fn cardinality( &self ) -> Result<&InterfaceCardinality, Self::Error> {
83/// # Ok( &InterfaceCardinality::ExactlyOne )
84/// # }
85/// # fn package_name( &self ) -> Result<&str, Self::Error> { Ok( "my:package/example" ) }
86/// # fn functions( &self ) -> Result<Self::FunctionIter<'_>, Self::Error> {
87/// # Ok( self.funcs.iter())
88/// # }
89/// # fn resources( &self ) -> Result<Self::ResourceIter<'_>, Self::Error> {
90/// # Ok( std::iter::empty())
91/// # }
92/// }
93///
94/// struct Plugin { id: &'static str, plug: &'static str, resource_table: ResourceTable }
95/// # impl PluginCtxView for Plugin {
96/// # fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
97/// # }
98/// impl PluginData for Plugin {
99/// /* ... */
100/// # type Id = &'static str ;
101/// # type InterfaceId = &'static str ;
102/// # type Error = std::convert::Infallible ;
103/// # type SocketIter<'a> = std::iter::Empty<&'a Self::InterfaceId> ;
104/// # fn id( &self ) -> Result<&Self::Id, Self::Error> { Ok( &self.id ) }
105/// # fn plug( &self ) -> Result<&Self::InterfaceId, Self::Error> { Ok( &self.plug ) }
106/// # fn sockets( &self ) -> Result<Self::SocketIter<'_>, Self::Error> {
107/// # Ok( std::iter::empty())
108/// # }
109/// # fn component( &self, engine: &Engine ) -> Result<Component, Self::Error> {
110/// # Ok( Component::new( engine, r#"(component
111/// # (core module $m)
112/// # (core instance $i (instantiate $m))
113/// # (instance $inst)
114/// # (export "my:package/example" (instance $inst))
115/// # )"# ).unwrap())
116/// # }
117/// }
118///
119/// let root_interface_id = "root" ;
120/// let plugins = [ Plugin { id: "foo", plug: root_interface_id, resource_table: ResourceTable::new() }];
121/// let interfaces = [ Interface { id: root_interface_id, funcs: vec![] }];
122///
123/// // Build the dependency graph
124/// let ( tree, build_errors ) = PluginTree::new( root_interface_id, interfaces, plugins );
125/// assert!( build_errors.is_empty() );
126///
127/// // Compile and link the plugins
128/// let engine = Engine::default();
129/// let linker = Linker::new( &engine );
130/// let ( tree_head, load_errors ) = tree.load( &engine, &linker ).unwrap();
131/// assert!( load_errors.is_empty() );
132/// ```
133pub struct PluginTree<I: InterfaceData, P: PluginData> {
134 root_interface_id: I::Id,
135 socket_map: HashMap<I::Id, ( I, Vec<P> )>,
136}
137
138impl<I: InterfaceData, P: PluginData, InterfaceId> PluginTree<I, P>
139where
140 I: InterfaceData<Id = InterfaceId>,
141 P: PluginData<InterfaceId = InterfaceId>,
142 InterfaceId: Clone + std::hash::Hash + Eq + std::fmt::Display,
143{
144
145 /// Builds a plugin dependency graph from the given interfaces and plugins.
146 ///
147 /// Plugins are grouped by the interface they implement ( via [`PluginData::plug`] ).
148 /// Interfaces are indexed by their [`PluginData::id`] method.
149 ///
150 /// The `root_interface_id` specifies the entry point of the tree - the interface
151 /// whose plugins will be directly accessible via [`PluginTreeHead::dispatch`] after loading.
152 ///
153 /// Does not validate all interfaces required for linking are present.
154 /// Does not validate cardinality requirements.
155 ///
156 /// # Partial Success
157 /// Attempts to construct a tree for all plugins it received valid data for. Returns a list
158 /// of errors alongside the loaded `PluginTree` is any of the following occurs:
159 /// - An Interface mentioned in a plugin's plug is not passed in
160 /// - Calling [`PluginData::plug`] returns an error
161 ///
162 /// # Panics
163 /// Panics if an interface with id `root_interface_id` is not present in `interfaces`.
164 pub fn new(
165 root_interface_id: I::Id,
166 interfaces: impl IntoIterator<Item = I>,
167 plugins: impl IntoIterator<Item = P>,
168 ) -> PartialSuccess<Self, PluginTreeError<I, P>> {
169
170 let ( interface_map, interface_errors ) = interfaces.into_iter()
171 .map(| i | Ok::<_, PluginTreeError<I, P>>(( i.id().map_err(| err | PluginTreeError::InterfaceDataError( err ))?.clone(), i )))
172 .partition_result::<HashMap<_, _ >, Vec<_>, _, _>();
173
174 assert!(
175 interface_map.contains_key( &root_interface_id ),
176 "Root interface {} must be provided in interfaces list",
177 root_interface_id,
178 );
179
180 let ( entries, plugin_errors ) = plugins.into_iter()
181 .map(| plugin | Ok(( plugin.plug().map_err( PluginTreeError::PluginDataError )?.clone(), plugin )))
182 .partition_result::<Vec<_>, Vec<_>, _, _>();
183
184 let plugin_groups = entries.into_iter().into_group_map();
185 let mut interface_map = interface_map ;
186
187 let ( socket_entries, missing_errors ) = plugin_groups.into_iter()
188 .map(|( id, plugins )| match interface_map.remove( &id ) {
189 Some( interface ) => Ok(( id, ( interface, plugins ))),
190 None => Err( PluginTreeError::MissingInterface { interface_id: id, plugins }),
191 })
192 .partition_result::<Vec<_>, Vec<_>, _, _>();
193
194 // Include remaining interfaces with no plugins. Does not overwrite any
195 // entries since interfaces for sockets that had plugins on them were
196 // already removed from the map.
197 let socket_map = socket_entries.into_iter()
198 .chain( interface_map.into_iter().map(|( id, interface )| ( id, ( interface, Vec::new() ))))
199 .collect::<HashMap<_, _>>();
200
201 ( Self { root_interface_id, socket_map }, interface_errors.merge_all( plugin_errors ).merge_all( missing_errors ))
202
203 }
204
205 /// Creates a plugin tree directly from a pre-built socket map.
206 ///
207 /// Does not validate all interfaces required for linking are present.
208 /// Does not validate cardinality requirements.
209 ///
210 /// # Panics
211 /// Panics if an interface with id `root_interface_id` is not present in `interfaces`.
212 pub fn from_socket_map(
213 root_interface_id: I::Id,
214 socket_map: HashMap<I::Id, ( I, Vec<P> )>,
215 ) -> Self {
216
217 assert!(
218 socket_map.contains_key( &root_interface_id ),
219 "Root interface {} must be provided in interfaces list",
220 root_interface_id,
221 );
222
223 Self { root_interface_id, socket_map }
224 }
225
226 /// Compiles and links all plugins in the tree, returning a loaded tree head.
227 ///
228 /// This recursively loads plugins starting from the root interface, compiling
229 /// WASM components and linking their dependencies.
230 ///
231 /// # Errors
232 /// Returns `LoadError` variants for:
233 /// - Invalid or missing socket interfaces
234 /// - Dependency cycles between plugins
235 /// - Cardinality violations (too few/many plugins for an interface)
236 /// - Corrupted interface or plugin manifests
237 /// - WASM compilation or linking failures
238 pub fn load(
239 self,
240 engine: &Engine,
241 exports: &Linker<P>,
242 ) -> PartialResult<PluginTreeHead<I, P>, LoadError<I, P>, LoadError<I, P>> {
243 match load_plugin_tree( self.socket_map, engine, exports, self.root_interface_id ) {
244 Ok((( interface, socket ), errors )) => Ok(( PluginTreeHead { _interface: interface, socket }, errors )),
245 Err(( err, errors )) => Err(( err , errors )),
246 }
247 }
248
249}