Skip to main content

wasmtime/compile/code_builder/
compile_time_builtins.rs

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