Skip to main content

wasmtime/compile/code_builder/
compile_time_builtins.rs

1use super::*;
2use crate::ToWasmtimeResult as _;
3use wasm_compose::graph::{CompositionGraph, EncodeOptions, ExportIndex, ImportIndex, InstanceId};
4
5/// Returns the instance imports of `component`, in declaration order.
6fn instance_imports<'c>(
7    component: &'c wasm_compose::graph::Component<'_>,
8) -> impl Iterator<Item = (ImportIndex, &'c str)> {
9    component.imports().filter_map(|(index, name, ty)| {
10        matches!(ty, wasmparser::ComponentTypeRef::Instance(_)).then_some((index, name))
11    })
12}
13
14/// Composes `main_wasm` with the compile-time builtins it imports.
15///
16/// Each imported builtin is instantiated once and satisfies every instance
17/// import named after it, including other builtins' imports. Other imports,
18/// such as the unsafe intrinsics, remain imports, merged by name.
19fn compose(
20    main_wasm: &[u8],
21    builtins: &HashMap<Cow<'_, str>, Cow<'_, [u8]>>,
22    features: wasmparser::WasmFeatures,
23) -> Result<Vec<u8>> {
24    let mut validator = wasmparser::Validator::new_with_features(features);
25    let mut graph = CompositionGraph::new();
26
27    let main = wasm_compose::graph::Component::from_bytes(&mut validator, "main", main_wasm)
28        .to_wasmtime_result()?;
29    let main_imports: Vec<String> = instance_imports(&main)
30        .map(|(_, name)| name.to_owned())
31        .collect();
32    let main = graph.add_component(main).to_wasmtime_result()?;
33    let main = graph.instantiate(main).to_wasmtime_result()?;
34
35    // Instantiate in import order, so the output doesn't depend on hash map
36    // order.
37    let mut instances: Vec<(&str, InstanceId)> = Vec::new();
38    for name in &main_imports {
39        let Some((name, bytes)) = builtins.get_key_value(name.as_str()) else {
40            continue;
41        };
42
43        // Graph labels must be unique and valid but aren't encoded, so number
44        // them rather than reuse import names, which could clash with `main`.
45        let label = format!("builtin{}", instances.len());
46
47        // wasm-compose's errors use the label, so name the builtin here.
48        let component = wasm_compose::graph::Component::from_bytes(&mut validator, label, &**bytes)
49            .to_wasmtime_result()
50            .with_context(|| format!("invalid compile-time builtin `{name}`"))?;
51        let component = graph.add_component(component).to_wasmtime_result()?;
52
53        instances.push((name, graph.instantiate(component).to_wasmtime_result()?));
54    }
55
56    // Collect the connections first, since connecting needs the graph
57    // mutably.
58    let mut connections: Vec<(InstanceId, InstanceId, ImportIndex)> = Vec::new();
59    for target in core::iter::once(main).chain(instances.iter().map(|&(_, instance)| instance)) {
60        let (_, component) = graph.get_component_of_instance(target).unwrap();
61        for (import, name) in instance_imports(component) {
62            if let Some(&(_, source)) = instances.iter().find(|&&(builtin, _)| builtin == name) {
63                connections.push((source, target, import));
64            }
65        }
66    }
67
68    for (source, target, import) in connections {
69        graph
70            .connect(source, None::<ExportIndex>, target, import)
71            .to_wasmtime_result()?;
72    }
73
74    graph
75        .encode(EncodeOptions {
76            define_components: true,
77            export: Some(main),
78            validate: false,
79        })
80        .to_wasmtime_result()
81}
82
83impl<'a> CodeBuilder<'a> {
84    pub(crate) fn get_compile_time_builtins(&self) -> &HashMap<Cow<'a, str>, Cow<'a, [u8]>> {
85        &self.compile_time_builtins
86    }
87
88    pub(super) fn compose_compile_time_builtins<'b>(
89        &self,
90        main_wasm: &'b [u8],
91    ) -> Result<Cow<'b, [u8]>> {
92        if self.get_compile_time_builtins().is_empty() {
93            return Ok(main_wasm.into());
94        }
95
96        let imports = self.check_imports_for_compile_time_builtins(&main_wasm)?;
97        let builtins = self.get_compile_time_builtins();
98
99        // Nothing to compose if the main Wasm imports none of the builtins.
100        if !imports.iter().any(|import| builtins.contains_key(*import)) {
101            drop(imports);
102            return Ok(main_wasm.into());
103        }
104
105        Ok(compose(main_wasm, builtins, self.engine.features())?.into())
106    }
107
108    /// Check that the main Wasm doesn't import unsafe intrinsics, keeping the
109    /// TCB to just the compile-time builtins' implementation.
110    ///
111    /// Returns the Wasm's top-level instance imports, so that composition can
112    /// be skipped when none of them is a compile-time builtin.
113    fn check_imports_for_compile_time_builtins<'b>(
114        &self,
115        main_wasm: &'b [u8],
116    ) -> Result<crate::hash_set::HashSet<&'b str>, Error> {
117        let intrinsics_import = self.unsafe_intrinsics_import.as_deref().ok_or_else(|| {
118            format_err!(
119                "must configure the unsafe-intrinsics import when using compile-time builtins"
120            )
121        })?;
122
123        let mut instance_imports = crate::hash_set::HashSet::new();
124        let parser = wasmparser::Parser::new(0);
125        let mut level = 0;
126
127        for payload in parser.parse_all(main_wasm) {
128            match payload? {
129                wasmparser::Payload::Version { .. } => {
130                    level += 1;
131                }
132                wasmparser::Payload::End(_) => {
133                    level -= 1;
134                }
135                wasmparser::Payload::ComponentImportSection(imports) if level == 1 => {
136                    for imp in imports.into_iter() {
137                        let imp = imp?;
138                        // Ideally we would simply choose a new import name that
139                        // doesn't conflict with the main Wasm's imports and
140                        // plumb that through to the compile-time builtins
141                        // regardless of the import name that they use, but
142                        // unfortunately the `wasm-compose` API is not powerful
143                        // enough for us to do all that.
144                        ensure!(
145                            imp.name.name != intrinsics_import,
146                            "main Wasm cannot import the unsafe intrinsics (`{intrinsics_import}`) \
147                             when using compile-time builtins"
148                        );
149
150                        if let wasmparser::ComponentTypeRef::Instance(_) = imp.ty {
151                            instance_imports.insert(imp.name.name);
152                        }
153                    }
154                }
155                _ => {}
156            }
157        }
158
159        Ok(instance_imports)
160    }
161
162    /// Define a compile-time builtin component, via its Wasm bytes.
163    ///
164    /// Compile-time builtins enable you to build safe, zero-copy, and (with
165    /// [inlining][crate::Config::compiler_inlining])
166    /// zero-function-call-overhead Wasm APIs for accessing host data, buffers,
167    /// and objects.
168    ///
169    /// A compile-time builtin is a component that is
170    ///
171    /// * authored by the host (Wasmtime embedder),
172    ///
173    /// * whose implementation (though not necessarily its interface!) is
174    ///   host-specific,
175    ///
176    /// * has access to unsafe intrinsics (and is therefore part of the host's
177    ///   [trusted compute base]), and
178    ///
179    /// * is linked into guest Wasm programs at compile-time.
180    ///
181    /// Any imports satisfied by a compile-time builtin during compilation will
182    /// not show up in the resulting component's
183    /// [imports][crate::component::types::Component::imports], and they can no
184    /// longer be customized by a [`Linker`][crate::component::Linker]
185    /// definition at instantiation time.[^0]
186    ///
187    /// [^0]: If linking compile-time builtins into a component at compile-time
188    /// reminds you of [component composition], that is not a coincidence:
189    /// component composition is used under the covers as part of compile-time
190    /// builtins' implementation.
191    ///
192    /// Comparing compile-time builtins with
193    /// [`Linker`][crate::component::Linker]s is informative:
194    ///
195    /// * Both mechanisms define APIs to satisfy a Wasm program's imports.
196    ///
197    /// * A `Linker` satisfies those imports at instantiation-time, while
198    ///   compile-time builtins do it during compilation.
199    ///
200    /// * APIs defined by a `Linker` are implemented in Rust, and hosts can
201    ///   build safe, sandboxed Wasm APIs on top of raw, un-sandboxed primitives
202    ///   via Rust's `unsafe`. APIs defined by compile-time builtins are
203    ///   implemented as Wasm components, and hosts can build safe, sandboxed
204    ///   Wasm APIs on top of raw, un-sandboxed primitives via [unsafe
205    ///   intrinsics][CodeBuilder::expose_unsafe_intrinsics].
206    ///
207    /// * Imports satisfied via `Linker`-defined APIs are implemented with
208    ///   [PLT/GOT]-style function table lookups and indirect calls in the
209    ///   Wasm's compiled native code. On the other hand, Wasmtime implements
210    ///   calls to imports satisfied via compile-time builtins with direct calls
211    ///   in the Wasm's compiled native code. Wasmtime's compiler can also
212    ///   [inline][crate::Config::compiler_inlining] these direct calls,
213    ///   removing function call overheads and enabling further, cascading
214    ///   compiler optimizations.
215    ///
216    /// If you are familiar with Wasm on the Web, you can think of compile-time
217    /// builtins as the rough equivalent of [the `js-string-builtins` proposal]
218    /// but for arbitrary host-defined APIs in a Wasmtime embedding environment
219    /// rather than JS string APIs in a Web browser environment.
220    ///
221    /// [trusted compute base]: https://en.wikipedia.org/wiki/Trusted_computing_base
222    /// [the `js-string-builtins` proposal]: https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md
223    /// [component composition]: https://component-model.bytecodealliance.org/composing-and-distributing/composing.html
224    /// [PLT/GOT]: https://reverseengineering.stackexchange.com/a/1993
225    ///
226    /// # Safety
227    ///
228    /// Compile-time builtins are part of your [trusted compute base] and should
229    /// be authored by trusted, first-party developers with extreme care. You
230    /// should never use compile-time builtins authored by untrusted,
231    /// third-party developers.
232    ///
233    /// Compile-time builtins are given access to Wasmtime's [unsafe
234    /// intrinsics][CodeBuilder::expose_unsafe_intrinsics], and the same safety
235    /// invariants and portability concerns apply. However, when compile-time
236    /// builtins are defined on a `CodeBuilder`, unsafe intrinsics are *only*
237    /// exposed to the compile-time builtins, and they are *not* exposed to the
238    /// main guest Wasm program. This means that — assuming your compile-time
239    /// builtins only exposing safe APIs, encapsulating the intrinsics'
240    /// unsafety, and modulo bugs in your implementation of those safe APIs —
241    /// that the main guest Wasm program is not part of your trusted compute
242    /// base.
243    ///
244    /// # Example
245    ///
246    /// See the example in [CodeBuilder::expose_unsafe_intrinsics].
247    pub unsafe fn compile_time_builtins_binary(
248        &mut self,
249        name: impl Into<Cow<'a, str>>,
250        wasm_bytes: impl Into<Cow<'a, [u8]>>,
251    ) -> &mut Self {
252        self.compile_time_builtins
253            .insert(name.into(), wasm_bytes.into());
254        self
255    }
256
257    /// Equivalent of [`CodeBuilder::compile_time_builtins_binary`] that also
258    /// accepts the WebAssembly text format.
259    ///
260    /// This method will configure the WebAssembly binary to be compiled and
261    /// used to satisfy the `name` instance import. The input `wasm_bytes` may
262    /// either be the wasm text format or the binary format. If the `wat` crate
263    /// feature is enabled, which is enabled by default, then the text format
264    /// will automatically be converted to the binary format.
265    ///
266    /// # Errors
267    ///
268    /// This method will also return an error if `wasm_bytes` is the wasm text
269    /// format and the text syntax is not valid.
270    ///
271    /// # Safety
272    ///
273    /// See [`CodeBuilder::compile_time_builtins_binary`].
274    ///
275    /// # Example
276    ///
277    /// See the example in [CodeBuilder::expose_unsafe_intrinsics], which uses
278    /// compile-time builtins.
279    #[allow(unused_variables, reason = "`wasm_path` only used with `wat` feature")]
280    pub unsafe fn compile_time_builtins_binary_or_text(
281        &mut self,
282        name: impl Into<Cow<'a, str>>,
283        wasm_bytes: impl Into<Cow<'a, [u8]>>,
284        wasm_path: Option<&Path>,
285    ) -> Result<&mut Self> {
286        let wasm_bytes = wasm_bytes.into();
287
288        #[cfg(feature = "wat")]
289        if let Cow::Owned(wasm_bytes) = wat::parse_bytes(&wasm_bytes).map_err(|mut e| {
290            if let Some(path) = wasm_path {
291                e.set_path(path);
292            }
293            e
294        })? {
295            // SAFETY: Same as our unsafe contract.
296            return Ok(unsafe { self.compile_time_builtins_binary(name, wasm_bytes) });
297        }
298
299        // SAFETY: Same as our unsafe contract.
300        Ok(unsafe { self.compile_time_builtins_binary(name, wasm_bytes) })
301    }
302
303    /// Like [`CodeBuilder::compile_time_builtins_binary`], but reads the `file`
304    /// specified for the bytes that will define the compile-time builtin.
305    ///
306    /// # Safety
307    ///
308    /// See [`CodeBuilder::compile_time_builtins_binary`].
309    ///
310    /// # Example
311    ///
312    /// See the example in [CodeBuilder::expose_unsafe_intrinsics], which uses
313    /// compile-time builtins.
314    pub unsafe fn compile_time_builtins_binary_file(
315        &mut self,
316        name: impl Into<Cow<'a, str>>,
317        file: &Path,
318    ) -> Result<&mut Self> {
319        let wasm_bytes = std::fs::read(file)
320            .with_context(|| format!("failed to read file: {}", file.display()))?;
321        // SAFETY: Same as our unsafe contract.
322        Ok(unsafe { self.compile_time_builtins_binary(name, wasm_bytes) })
323    }
324
325    /// Equivalent of [`CodeBuilder::compile_time_builtins_binary_file`] that
326    /// also accepts the WebAssembly text format.
327    ///
328    /// This method is will read the file at the given path and interpret the
329    /// contents to determine if it's the Wasm text format or binary format. The
330    /// file extension is not consulted. The text format is automatically
331    /// converted to the binary format if the crate feature `wat` is active.
332    ///
333    /// # Errors
334    ///
335    /// In addition to the errors returned by
336    /// [`CodeBuilder::compile_time_builtins_binary_file`] this may also fail if
337    /// the text format is read and the syntax is invalid.
338    ///
339    /// # Safety
340    ///
341    /// See [`CodeBuilder::compile_time_builtins_binary`].
342    ///
343    /// # Example
344    ///
345    /// See the example in [CodeBuilder::expose_unsafe_intrinsics], which uses
346    /// compile-time builtins.
347    pub unsafe fn compile_time_builtins_binary_or_text_file(
348        &mut self,
349        name: impl Into<Cow<'a, str>>,
350        file: &Path,
351    ) -> Result<&mut Self> {
352        #[cfg(feature = "wat")]
353        {
354            let wasm = wat::parse_file(file)
355                .with_context(|| format!("error parsing file: {}", file.display()))?;
356            // SAFETY: Same as our unsafe contract.
357            Ok(unsafe { self.compile_time_builtins_binary(name, wasm) })
358        }
359
360        #[cfg(not(feature = "wat"))]
361        {
362            // SAFETY: Same as our unsafe contract.
363            unsafe { self.compile_time_builtins_binary_file(name, file) }
364        }
365    }
366}