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}