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}