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::{Context, Result, anyhow, bail};
12use indexmap::IndexMap;
13use std::{collections::VecDeque, ffi::OsStr, path::Path};
14use wasmparser::{
15    ComponentExternalKind, ComponentTypeRef, Validator, WasmFeatures,
16    component_types::{ComponentEntityType, ComponentInstanceTypeId},
17    types::TypesRef,
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!(
197                        "a dependency named `{component_name}` could not be found and instance imports are not allowed"
198                    );
199                }
200
201                log::warn!(
202                    "instance `{name}` will be imported because a dependency named `{component_name}` could not be found"
203                );
204                Ok(None)
205            }
206        }
207    }
208
209    /// Finds a compatible instance for the given instance type.
210    ///
211    /// Returns `Ok(None)` if the given instance itself is compatible.
212    /// Returns `Ok(Some(index))` if a compatible instance export from the instance was found.
213    /// Returns `Err(_)` if no compatible instance was found.
214    fn find_compatible_instance(
215        &self,
216        instance: usize,
217        dependent: usize,
218        arg_name: &str,
219        ty: ComponentInstanceTypeId,
220        types: TypesRef,
221    ) -> Result<Option<ExportIndex>> {
222        let (instance_name, instance_id) = self.instances.get_index(instance).unwrap();
223        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
224
225        let (dependent_name, dependent_instance_id) = self.instances.get_index(dependent).unwrap();
226
227        // Check if the instance or one of its exports is compatible with the expected import type
228        if component.is_instance_subtype_of(ty, types) {
229            // The instance itself can be used
230            log::debug!(
231                "instance `{instance_name}` can be used for argument `{arg_name}` of instance `{dependent_name}`"
232            );
233            return Ok(None);
234        }
235
236        log::debug!(
237            "searching for compatible export from instance `{instance_name}` for argument `{arg_name}` of instance `{dependent_name}`"
238        );
239
240        let export = component.find_compatible_export(ty, types, component_id, &self.graph).ok_or_else(|| {
241            anyhow!(
242                "component `{path}` is not compatible with import `{arg_name}` of component `{dependent_path}`",
243                path = component.path().unwrap().display(),
244                dependent_path = self.graph.get_component_of_instance(*dependent_instance_id).unwrap().1.path().unwrap().display(),
245            )
246        })?;
247
248        log::debug!(
249            "export `{export_name}` (export index {export}) from instance `{instance_name}` can be used for argument `{arg_name}` of instance `{dependent_name}`",
250            export = export.0,
251            export_name = component.exports.get_index(export.0).unwrap().0,
252        );
253
254        Ok(Some(export))
255    }
256
257    /// Resolves an explicitly specified export to its index.
258    ///
259    /// Returns an error if the export is not found or if it is not compatible with the given type.
260    fn resolve_export_index(
261        &self,
262        export: &str,
263        instance: usize,
264        dependent_path: &Path,
265        arg_name: &str,
266        ty: ComponentInstanceTypeId,
267        types: TypesRef,
268    ) -> Result<ExportIndex> {
269        let (_, instance_id) = self.instances.get_index(instance).unwrap();
270        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
271
272        match component.export_by_name(export) {
273            Some((export_index, kind, index)) if kind == ComponentExternalKind::Instance => {
274                let export_ty = component.types.as_ref().component_instance_at(index);
275
276                if self.graph.try_connection(
277                    component_id,
278                    ComponentEntityType::Instance(export_ty),
279                    component.types(),
280                    ComponentEntityType::Instance(ty),
281                    types,
282                ) {
283                    Ok(export_index)
284                } else {
285                    bail!(
286                        "component `{path}` exports an instance named `{export}` \
287                         but it is not compatible with import `{arg_name}` \
288                         of component `{dependent_path}`",
289                        path = component.path().unwrap().display(),
290                        dependent_path = dependent_path.display(),
291                    )
292                }
293            }
294            _ => bail!(
295                "component `{path}` does not export an instance named `{export}`",
296                path = component.path().unwrap().display(),
297            ),
298        }
299    }
300
301    /// Resolves an import instance reference.
302    fn resolve_import_ref(
303        &self,
304        r: InstanceImportRef,
305    ) -> (&Component<'_>, &str, ComponentInstanceTypeId) {
306        let component = self.graph.get_component(r.component).unwrap();
307        let (name, ty) = component.import(r.import).unwrap();
308        match ty {
309            ComponentTypeRef::Instance(index) => (
310                component,
311                name,
312                component
313                    .types
314                    .as_ref()
315                    .component_any_type_at(index)
316                    .unwrap_instance(),
317            ),
318            _ => unreachable!("should not have an instance import ref to a non-instance import"),
319        }
320    }
321
322    /// Processes a dependency in the graph.
323    ///
324    /// Returns `Ok(Some(index))` if the dependency resulted in a new dependency instance being created.
325    fn process_dependency(&mut self, dependency: Dependency) -> Result<Option<usize>> {
326        match dependency.kind {
327            DependencyKind::Instance { instance, export } => self.process_instance_dependency(
328                dependency.dependent,
329                dependency.import,
330                &instance,
331                export.as_deref(),
332            ),
333            DependencyKind::Definition { index, export } => {
334                // The dependency is on a definition component, so we simply connect the dependent to the definition's export
335                let (component_id, instance_id) = &mut self.definitions[index];
336                let instance_id = *instance_id
337                    .get_or_insert_with(|| self.graph.instantiate(*component_id).unwrap());
338
339                self.graph
340                    .connect(
341                        instance_id,
342                        Some(export),
343                        self.instances[dependency.dependent],
344                        dependency.import.import,
345                    )
346                    .with_context(|| {
347                        let name = self.instances.get_index(dependency.dependent).unwrap().0;
348                        format!(
349                            "failed to connect instance `{name}` to definition component `{path}`",
350                            path = self
351                                .graph
352                                .get_component(*component_id)
353                                .unwrap()
354                                .path()
355                                .unwrap()
356                                .display(),
357                        )
358                    })?;
359
360                // No new dependency instance was created
361                Ok(None)
362            }
363        }
364    }
365
366    fn process_instance_dependency(
367        &mut self,
368        dependent_index: usize,
369        import: InstanceImportRef,
370        instance: &str,
371        export: Option<&str>,
372    ) -> Result<Option<usize>> {
373        let name = self.config.dependency_name(instance);
374
375        log::info!(
376            "processing dependency `{name}` from instance `{dependent_name}` to instance `{instance}`",
377            dependent_name = self.instances.get_index(dependent_index).unwrap().0,
378        );
379
380        match self.instantiate(instance, name)? {
381            Some((instance, existing)) => {
382                let (dependent, import_name, import_type) = self.resolve_import_ref(import);
383
384                let export = match export {
385                    Some(export) => Some(self.resolve_export_index(
386                        export,
387                        instance,
388                        dependent.path().unwrap(),
389                        import_name,
390                        import_type,
391                        dependent.types(),
392                    )?),
393                    None => self.find_compatible_instance(
394                        instance,
395                        dependent_index,
396                        import_name,
397                        import_type,
398                        dependent.types(),
399                    )?,
400                };
401
402                // Connect the new instance to the dependent
403                self.graph.connect(
404                    self.instances[instance],
405                    export,
406                    self.instances[dependent_index],
407                    import.import,
408                )?;
409
410                if existing {
411                    return Ok(None);
412                }
413
414                Ok(Some(instance))
415            }
416            None => {
417                if let Some(export) = export {
418                    bail!(
419                        "an explicit export `{export}` cannot be specified for imported instance `{name}`"
420                    );
421                }
422                Ok(None)
423            }
424        }
425    }
426
427    /// Push dependencies of the given instance to the dependency queue.
428    fn push_dependencies(&self, instance: usize, queue: &mut VecDeque<Dependency>) -> Result<()> {
429        let (instance_name, instance_id) = self.instances.get_index(instance).unwrap();
430        let instantiation = self.config.instantiations.get(instance_name);
431        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
432        let count = queue.len();
433
434        // Push a dependency for every instance import
435        'outer: for (import, name, _) in component.imports() {
436            log::debug!(
437                "adding dependency for argument `{name}` (import index {import}) from instance `{instance_name}` to the queue",
438                import = import.0
439            );
440
441            // Search for a matching definition export for this import
442            for (index, (def_component_id, _)) in self.definitions.iter().enumerate() {
443                let def_component = self.graph.get_component(*def_component_id).unwrap();
444
445                match def_component.export_by_name(name) {
446                    Some((export, ComponentExternalKind::Instance, _)) => {
447                        log::debug!(
448                            "found matching instance export `{name}` in definition component `{path}`",
449                            path = def_component.path().unwrap().display()
450                        );
451
452                        queue.push_back(Dependency {
453                            dependent: instance,
454                            import: InstanceImportRef {
455                                component: component_id,
456                                import,
457                            },
458                            kind: DependencyKind::Definition { index, export },
459                        });
460
461                        continue 'outer;
462                    }
463                    _ => continue,
464                }
465            }
466
467            let arg = instantiation.and_then(|c| c.arguments.get(name));
468            queue.push_back(Dependency {
469                dependent: instance,
470                import: InstanceImportRef {
471                    component: component_id,
472                    import,
473                },
474                kind: DependencyKind::Instance {
475                    instance: arg
476                        .map(|arg| arg.instance.clone())
477                        .unwrap_or_else(|| name.to_string()),
478                    export: arg.and_then(|arg| arg.export.clone()),
479                },
480            });
481        }
482
483        // Ensure every explicit argument is a valid import name
484        if let Some(instantiation) = instantiation {
485            for arg in instantiation.arguments.keys() {
486                if !component.imports.contains_key(arg) {
487                    bail!(
488                        "component `{path}` has no import named `{arg}`",
489                        path = component.path().unwrap().display()
490                    );
491                }
492            }
493        }
494
495        // It is an error if the root component has no instance imports
496        if count == queue.len() && instance == 0 {
497            bail!(
498                "component `{path}` does not import any instances",
499                path = component.path().unwrap().display()
500            );
501        }
502
503        Ok(())
504    }
505
506    /// Build the instantiation graph.
507    fn build(mut self) -> Result<(InstanceId, CompositionGraph<'a>)> {
508        let mut queue: VecDeque<Dependency> = VecDeque::new();
509
510        // Instantiate the root and push its dependencies to the queue
511        let (root_instance, existing) = self
512            .instantiate(ROOT_COMPONENT_NAME, ROOT_COMPONENT_NAME)?
513            .unwrap();
514
515        assert!(!existing);
516
517        self.push_dependencies(0, &mut queue)?;
518
519        // Process all remaining dependencies in the queue
520        while let Some(dependency) = queue.pop_front() {
521            if let Some(instance) = self.process_dependency(dependency)? {
522                self.push_dependencies(instance, &mut queue)?;
523            }
524        }
525
526        Ok((self.instances[root_instance], self.graph))
527    }
528}
529
530/// Used to compose a WebAssembly component from other components.
531///
532/// The component composer resolves the dependencies of a root component
533/// from components of matching names in the file system.
534///
535/// The exports of the root component are then exported from the composed
536/// component.
537pub struct ComponentComposer<'a> {
538    component: &'a Path,
539    config: &'a Config,
540}
541
542impl<'a> ComponentComposer<'a> {
543    /// Constructs a new WebAssembly component composer.
544    ///
545    /// ## Arguments
546    /// * `component` - The path to the component to compose.
547    /// * `config` - The configuration to use for the composition.
548    pub fn new(component: &'a Path, config: &'a Config) -> Self {
549        Self { component, config }
550    }
551
552    /// Composes a WebAssembly component based on the composer's configuration.
553    ///
554    /// ## Returns
555    /// Returns the bytes of the composed component.
556    pub fn compose(&self) -> Result<Vec<u8>> {
557        let (root_instance, graph) =
558            CompositionGraphBuilder::new(self.component, self.config)?.build()?;
559
560        // If only the root component was instantiated, then there are no resolved dependencies
561        if graph.instances.len() == 1 {
562            bail!(
563                "no dependencies of component `{path}` were found",
564                path = self.component.display()
565            );
566        }
567
568        CompositionGraphEncoder::new(
569            EncodeOptions {
570                define_components: !self.config.import_components,
571                export: Some(root_instance),
572                validate: false,
573            },
574            &graph,
575        )
576        .encode()
577    }
578}