wasm_compose/
composer.rs

1//! Module for composing WebAssembly components.
2
3use crate::{
4    config::Config,
5    encoding::CompositionGraphEncoder,
6    graph::{
7        Component, ComponentId, CompositionGraph, EncodeOptions, ExportIndex, ImportIndex,
8        InstanceId,
9    },
10};
11use anyhow::{anyhow, bail, Context, Result};
12use indexmap::IndexMap;
13use std::{collections::VecDeque, ffi::OsStr, path::Path};
14use wasmparser::{
15    component_types::{ComponentEntityType, ComponentInstanceTypeId},
16    types::TypesRef,
17    ComponentExternalKind, ComponentTypeRef, Validator, WasmFeatures,
18};
19
20/// The root component name used in configuration.
21pub const ROOT_COMPONENT_NAME: &str = "root";
22
23/// A reference to an instance import on a component.
24#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
25pub(crate) struct InstanceImportRef {
26    /// The id of the component with the instance import.
27    pub(crate) component: ComponentId,
28    /// The index of the import on the component.
29    pub(crate) import: ImportIndex,
30}
31
32/// Represents the kind of dependency to process.
33enum DependencyKind {
34    /// The dependency is on a configured instance.
35    Instance {
36        /// The name of the instance from the instantiation argument.
37        instance: String,
38        /// The name of the export on the instance to use as the instantiation argument.
39        export: Option<String>,
40    },
41
42    /// The dependency is on a definition component.
43    Definition {
44        /// The index into `definitions` for the dependency.
45        index: usize,
46        /// The export on the definition component to use as the instantiation argument.
47        export: ExportIndex,
48    },
49}
50
51/// An instance dependency to process in the composer.
52struct Dependency {
53    /// The index into `instances` for the dependent instance.
54    dependent: usize,
55    /// The instance import reference on the dependent instance.
56    import: InstanceImportRef,
57    /// The kind of dependency.
58    kind: DependencyKind,
59}
60
61/// A composition graph builder that wires up instances from components
62/// resolved from the file system.
63struct CompositionGraphBuilder<'a> {
64    /// The associated composition configuration.
65    config: &'a Config,
66    /// The graph being built.
67    graph: CompositionGraph<'a>,
68    /// A map from instance name to graph instance id.
69    instances: IndexMap<String, InstanceId>,
70    /// The definition components in the graph.
71    definitions: Vec<(ComponentId, Option<InstanceId>)>,
72    /// Wasm validator and shared type arenas.
73    validator: Validator,
74}
75
76impl<'a> CompositionGraphBuilder<'a> {
77    fn new(root_path: &Path, config: &'a Config) -> Result<Self> {
78        let mut graph = CompositionGraph::new();
79        let mut validator = Validator::new_with_features(WasmFeatures::all());
80        graph.add_component(Component::from_file(
81            &mut validator,
82            ROOT_COMPONENT_NAME,
83            root_path,
84        )?)?;
85
86        let definitions = config
87            .definitions
88            .iter()
89            .map(|path| {
90                let name = path.file_stem().and_then(OsStr::to_str).with_context(|| {
91                    format!(
92                        "invalid definition component path `{path}`",
93                        path = path.display()
94                    )
95                })?;
96
97                let component = Component::from_file(&mut validator, name, config.dir.join(path))?;
98
99                Ok((graph.add_component(component)?, None))
100            })
101            .collect::<Result<_>>()?;
102
103        Ok(Self {
104            config,
105            graph,
106            instances: Default::default(),
107            definitions,
108            validator,
109        })
110    }
111
112    /// Adds a component of the given name to the graph.
113    ///
114    /// If a component with the given name already exists, its id is returned.
115    /// Returns `Ok(None)` if a matching component cannot be found.
116    fn add_component(&mut self, name: &str) -> Result<Option<ComponentId>> {
117        if let Some((id, _)) = self.graph.get_component_by_name(name) {
118            return Ok(Some(id));
119        }
120
121        match self.find_component(name)? {
122            Some(component) => Ok(Some(self.graph.add_component(component)?)),
123            None => Ok(None),
124        }
125    }
126
127    /// Finds the component with the given name on disk.
128    fn find_component(&mut self, name: &str) -> Result<Option<Component<'a>>> {
129        // Check the config for an explicit path (must be a valid component)
130        if let Some(dep) = self.config.dependencies.get(name) {
131            log::debug!(
132                "component with name `{name}` has an explicit path of `{path}`",
133                path = dep.path.display()
134            );
135            return Ok(Some(Component::from_file(
136                &mut self.validator,
137                name,
138                self.config.dir.join(&dep.path),
139            )?));
140        }
141
142        // Otherwise, search the paths for a valid component with the same name
143        log::info!("searching for a component with name `{name}`");
144        for dir in std::iter::once(&self.config.dir).chain(self.config.search_paths.iter()) {
145            if let Some(component) = Self::parse_component(&mut self.validator, dir, name)? {
146                return Ok(Some(component));
147            }
148        }
149
150        Ok(None)
151    }
152
153    /// Parses a component from the given directory, if it exists.
154    ///
155    /// Returns `Ok(None)` if the component does not exist.
156    fn parse_component(
157        validator: &mut Validator,
158        dir: &Path,
159        name: &str,
160    ) -> Result<Option<Component<'a>>> {
161        let mut path = dir.join(name);
162
163        for ext in ["wasm", "wat"] {
164            path.set_extension(ext);
165            if !path.is_file() {
166                log::info!("component `{path}` does not exist", path = path.display());
167                continue;
168            }
169
170            return Ok(Some(Component::from_file(validator, name, &path)?));
171        }
172
173        Ok(None)
174    }
175
176    /// Instantiates an instance with the given name into the graph.
177    ///
178    /// Returns an index into `instances` for the instance being instantiated.
179    ///
180    /// Returns `Ok(None)` if a component to instantiate cannot be found.
181    fn instantiate(&mut self, name: &str, component_name: &str) -> Result<Option<(usize, bool)>> {
182        if let Some(index) = self.instances.get_index_of(name) {
183            return Ok(Some((index, true)));
184        }
185
186        match self.add_component(component_name)? {
187            Some(component_id) => {
188                let (index, prev) = self
189                    .instances
190                    .insert_full(name.to_string(), self.graph.instantiate(component_id)?);
191                assert!(prev.is_none());
192                Ok(Some((index, false)))
193            }
194            None => {
195                if self.config.disallow_imports {
196                    bail!("a dependency named `{component_name}` could not be found and instance imports are not allowed");
197                }
198
199                log::warn!("instance `{name}` will be imported because a dependency named `{component_name}` could not be found");
200                Ok(None)
201            }
202        }
203    }
204
205    /// Finds a compatible instance for the given instance type.
206    ///
207    /// Returns `Ok(None)` if the given instance itself is compatible.
208    /// Returns `Ok(Some(index))` if a compatible instance export from the instance was found.
209    /// Returns `Err(_)` if no compatible instance was found.
210    fn find_compatible_instance(
211        &self,
212        instance: usize,
213        dependent: usize,
214        arg_name: &str,
215        ty: ComponentInstanceTypeId,
216        types: TypesRef,
217    ) -> Result<Option<ExportIndex>> {
218        let (instance_name, instance_id) = self.instances.get_index(instance).unwrap();
219        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
220
221        let (dependent_name, dependent_instance_id) = self.instances.get_index(dependent).unwrap();
222
223        // Check if the instance or one of its exports is compatible with the expected import type
224        if component.is_instance_subtype_of(ty, types) {
225            // The instance itself can be used
226            log::debug!("instance `{instance_name}` can be used for argument `{arg_name}` of instance `{dependent_name}`");
227            return Ok(None);
228        }
229
230        log::debug!("searching for compatible export from instance `{instance_name}` for argument `{arg_name}` of instance `{dependent_name}`");
231
232        let export = component.find_compatible_export(ty, types, component_id, &self.graph).ok_or_else(|| {
233            anyhow!(
234                "component `{path}` is not compatible with import `{arg_name}` of component `{dependent_path}`",
235                path = component.path().unwrap().display(),
236                dependent_path = self.graph.get_component_of_instance(*dependent_instance_id).unwrap().1.path().unwrap().display(),
237            )
238        })?;
239
240        log::debug!(
241            "export `{export_name}` (export index {export}) from instance `{instance_name}` can be used for argument `{arg_name}` of instance `{dependent_name}`",
242            export = export.0,
243            export_name = component.exports.get_index(export.0).unwrap().0,
244        );
245
246        Ok(Some(export))
247    }
248
249    /// Resolves an explicitly specified export to its index.
250    ///
251    /// Returns an error if the export is not found or if it is not compatible with the given type.
252    fn resolve_export_index(
253        &self,
254        export: &str,
255        instance: usize,
256        dependent_path: &Path,
257        arg_name: &str,
258        ty: ComponentInstanceTypeId,
259        types: TypesRef,
260    ) -> Result<ExportIndex> {
261        let (_, instance_id) = self.instances.get_index(instance).unwrap();
262        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
263
264        match component.export_by_name(export) {
265            Some((export_index, kind, index)) if kind == ComponentExternalKind::Instance => {
266                let export_ty = component.types.as_ref().component_instance_at(index);
267
268                if self.graph.try_connection(
269                    component_id,
270                    ComponentEntityType::Instance(export_ty),
271                    component.types(),
272                    ComponentEntityType::Instance(ty),
273                    types,
274                ) {
275                    Ok(export_index)
276                } else {
277                    bail!(
278                        "component `{path}` exports an instance named `{export}` \
279                         but it is not compatible with import `{arg_name}` \
280                         of component `{dependent_path}`",
281                        path = component.path().unwrap().display(),
282                        dependent_path = dependent_path.display(),
283                    )
284                }
285            }
286            _ => bail!(
287                "component `{path}` does not export an instance named `{export}`",
288                path = component.path().unwrap().display(),
289            ),
290        }
291    }
292
293    /// Resolves an import instance reference.
294    fn resolve_import_ref(
295        &self,
296        r: InstanceImportRef,
297    ) -> (&Component, &str, ComponentInstanceTypeId) {
298        let component = self.graph.get_component(r.component).unwrap();
299        let (name, ty) = component.import(r.import).unwrap();
300        match ty {
301            ComponentTypeRef::Instance(index) => (
302                component,
303                name,
304                component
305                    .types
306                    .as_ref()
307                    .component_any_type_at(index)
308                    .unwrap_instance(),
309            ),
310            _ => unreachable!("should not have an instance import ref to a non-instance import"),
311        }
312    }
313
314    /// Processes a dependency in the graph.
315    ///
316    /// Returns `Ok(Some(index))` if the dependency resulted in a new dependency instance being created.
317    fn process_dependency(&mut self, dependency: Dependency) -> Result<Option<usize>> {
318        match dependency.kind {
319            DependencyKind::Instance { instance, export } => self.process_instance_dependency(
320                dependency.dependent,
321                dependency.import,
322                &instance,
323                export.as_deref(),
324            ),
325            DependencyKind::Definition { index, export } => {
326                // The dependency is on a definition component, so we simply connect the dependent to the definition's export
327                let (component_id, instance_id) = &mut self.definitions[index];
328                let instance_id = *instance_id
329                    .get_or_insert_with(|| self.graph.instantiate(*component_id).unwrap());
330
331                self.graph
332                    .connect(
333                        instance_id,
334                        Some(export),
335                        self.instances[dependency.dependent],
336                        dependency.import.import,
337                    )
338                    .with_context(|| {
339                        let name = self.instances.get_index(dependency.dependent).unwrap().0;
340                        format!(
341                            "failed to connect instance `{name}` to definition component `{path}`",
342                            path = self
343                                .graph
344                                .get_component(*component_id)
345                                .unwrap()
346                                .path()
347                                .unwrap()
348                                .display(),
349                        )
350                    })?;
351
352                // No new dependency instance was created
353                Ok(None)
354            }
355        }
356    }
357
358    fn process_instance_dependency(
359        &mut self,
360        dependent_index: usize,
361        import: InstanceImportRef,
362        instance: &str,
363        export: Option<&str>,
364    ) -> Result<Option<usize>> {
365        let name = self.config.dependency_name(instance);
366
367        log::info!(
368            "processing dependency `{name}` from instance `{dependent_name}` to instance `{instance}`",
369            dependent_name = self.instances.get_index(dependent_index).unwrap().0,
370        );
371
372        match self.instantiate(instance, name)? {
373            Some((instance, existing)) => {
374                let (dependent, import_name, import_type) = self.resolve_import_ref(import);
375
376                let export = match export {
377                    Some(export) => Some(self.resolve_export_index(
378                        export,
379                        instance,
380                        dependent.path().unwrap(),
381                        import_name,
382                        import_type,
383                        dependent.types(),
384                    )?),
385                    None => self.find_compatible_instance(
386                        instance,
387                        dependent_index,
388                        import_name,
389                        import_type,
390                        dependent.types(),
391                    )?,
392                };
393
394                // Connect the new instance to the dependent
395                self.graph.connect(
396                    self.instances[instance],
397                    export,
398                    self.instances[dependent_index],
399                    import.import,
400                )?;
401
402                if existing {
403                    return Ok(None);
404                }
405
406                Ok(Some(instance))
407            }
408            None => {
409                if let Some(export) = export {
410                    bail!("an explicit export `{export}` cannot be specified for imported instance `{name}`");
411                }
412                Ok(None)
413            }
414        }
415    }
416
417    /// Push dependencies of the given instance to the dependency queue.
418    fn push_dependencies(&self, instance: usize, queue: &mut VecDeque<Dependency>) -> Result<()> {
419        let (instance_name, instance_id) = self.instances.get_index(instance).unwrap();
420        let instantiation = self.config.instantiations.get(instance_name);
421        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
422        let count = queue.len();
423
424        // Push a dependency for every instance import
425        'outer: for (import, name, _) in component.imports() {
426            log::debug!("adding dependency for argument `{name}` (import index {import}) from instance `{instance_name}` to the queue", import = import.0);
427
428            // Search for a matching definition export for this import
429            for (index, (def_component_id, _)) in self.definitions.iter().enumerate() {
430                let def_component = self.graph.get_component(*def_component_id).unwrap();
431
432                match def_component.export_by_name(name) {
433                    Some((export, ComponentExternalKind::Instance, _)) => {
434                        log::debug!(
435                            "found matching instance export `{name}` in definition component `{path}`",
436                            path = def_component.path().unwrap().display()
437                        );
438
439                        queue.push_back(Dependency {
440                            dependent: instance,
441                            import: InstanceImportRef {
442                                component: component_id,
443                                import,
444                            },
445                            kind: DependencyKind::Definition { index, export },
446                        });
447
448                        continue 'outer;
449                    }
450                    _ => continue,
451                }
452            }
453
454            let arg = instantiation.and_then(|c| c.arguments.get(name));
455            queue.push_back(Dependency {
456                dependent: instance,
457                import: InstanceImportRef {
458                    component: component_id,
459                    import,
460                },
461                kind: DependencyKind::Instance {
462                    instance: arg
463                        .map(|arg| arg.instance.clone())
464                        .unwrap_or_else(|| name.to_string()),
465                    export: arg.and_then(|arg| arg.export.clone()),
466                },
467            });
468        }
469
470        // Ensure every explicit argument is a valid import name
471        if let Some(instantiation) = instantiation {
472            for arg in instantiation.arguments.keys() {
473                if !component.imports.contains_key(arg) {
474                    bail!(
475                        "component `{path}` has no import named `{arg}`",
476                        path = component.path().unwrap().display()
477                    );
478                }
479            }
480        }
481
482        // It is an error if the root component has no instance imports
483        if count == queue.len() && instance == 0 {
484            bail!(
485                "component `{path}` does not import any instances",
486                path = component.path().unwrap().display()
487            );
488        }
489
490        Ok(())
491    }
492
493    /// Build the instantiation graph.
494    fn build(mut self) -> Result<(InstanceId, CompositionGraph<'a>)> {
495        let mut queue: VecDeque<Dependency> = VecDeque::new();
496
497        // Instantiate the root and push its dependencies to the queue
498        let (root_instance, existing) = self
499            .instantiate(ROOT_COMPONENT_NAME, ROOT_COMPONENT_NAME)?
500            .unwrap();
501
502        assert!(!existing);
503
504        self.push_dependencies(0, &mut queue)?;
505
506        // Process all remaining dependencies in the queue
507        while let Some(dependency) = queue.pop_front() {
508            if let Some(instance) = self.process_dependency(dependency)? {
509                self.push_dependencies(instance, &mut queue)?;
510            }
511        }
512
513        Ok((self.instances[root_instance], self.graph))
514    }
515}
516
517/// Used to compose a WebAssembly component from other components.
518///
519/// The component composer resolves the dependencies of a root component
520/// from components of matching names in the file system.
521///
522/// The exports of the root component are then exported from the composed
523/// component.
524pub struct ComponentComposer<'a> {
525    component: &'a Path,
526    config: &'a Config,
527}
528
529impl<'a> ComponentComposer<'a> {
530    /// Constructs a new WebAssembly component composer.
531    ///
532    /// ## Arguments
533    /// * `component` - The path to the component to compose.
534    /// * `config` - The configuration to use for the composition.
535    pub fn new(component: &'a Path, config: &'a Config) -> Self {
536        Self { component, config }
537    }
538
539    /// Composes a WebAssembly component based on the composer's configuration.
540    ///
541    /// ## Returns
542    /// Returns the bytes of the composed component.
543    pub fn compose(&self) -> Result<Vec<u8>> {
544        let (root_instance, graph) =
545            CompositionGraphBuilder::new(self.component, self.config)?.build()?;
546
547        // If only the root component was instantiated, then there are no resolved dependencies
548        if graph.instances.len() == 1 {
549            bail!(
550                "no dependencies of component `{path}` were found",
551                path = self.component.display()
552            );
553        }
554
555        CompositionGraphEncoder::new(
556            EncodeOptions {
557                define_components: !self.config.import_components,
558                export: Some(root_instance),
559                validate: false,
560            },
561            &graph,
562        )
563        .encode()
564    }
565}