wasmtime/compile/code_builder/
compile_time_builtins.rs

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