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