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}