Skip to main content

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}