externref/lib.rs
1//! Low-cost [reference type] shims for WASM modules.
2//!
3//! Reference type (aka `externref` or `anyref`) is an opaque reference made available to
4//! a WASM module by the host environment. Such references cannot be forged in the WASM code
5//! and can be associated with arbitrary host data, thus making them a good alternative to
6//! ad-hoc handles (e.g., numeric ones). References cannot be stored in WASM linear memory;
7//! they are confined to the stack and tables with `externref` elements.
8//!
9//! Rust does not support reference types natively; there is no way to produce an import / export
10//! that has `externref` as an argument or a return type. [`wasm-bindgen`] patches WASM if
11//! `externref`s are enabled. This library strives to accomplish the same goal for generic
12//! low-level WASM ABIs (`wasm-bindgen` is specialized for browser hosts).
13//!
14//! # `externref` use cases
15//!
16//! Since `externref`s are completely opaque from the module perspective, the only way to use
17//! them is to send an `externref` back to the host as an argument of an imported function.
18//! (Depending on the function semantics, the call may or may not consume the `externref`
19//! and may or may not modify the underlying data; this is not reflected
20//! by the WASM function signature.) An `externref` cannot be dereferenced by the module,
21//! thus, the module cannot directly access or modify the data behind the reference. Indeed,
22//! the module cannot even be sure which kind of data is being referenced.
23//!
24//! It may seem that this limits `externref` utility significantly,
25//! but `externref`s can still be useful, e.g. to model [capability-based security] tokens
26//! or resource handles in the host environment. Another potential use case is encapsulating
27//! complex data that would be impractical to transfer across the WASM API boundary
28//! (especially if the data shape may evolve over time), and/or if interactions with data
29//! must be restricted from the module side.
30//!
31//! [capability-based security]: https://en.wikipedia.org/wiki/Capability-based_security
32//!
33//! # Usage
34//!
35//! 1. Use [`Resource`]s as arguments / return results for imported and/or exported functions
36//! in a WASM module in place of `externref`s . Reference args (including mutable references)
37//! and the `Option<_>` wrapper are supported as well.
38//! 2. Add the `#[externref]` proc macro on the imported / exported functions.
39//! 3. Post-process the generated WASM module with the [`processor`].
40//!
41//! `Resource`s support primitive downcasting and upcasting with `Resource<()>` signalling
42//! a generic resource. Downcasting is *unchecked*; it is up to the `Resource` users to
43//! define a way to check the resource kind dynamically if necessary. One possible approach
44//! for this is defining a WASM import `fn(&Resource<()>) -> Kind`, where `Kind` is the encoded
45//! kind of the supplied resource, such as `i32`.
46//!
47//! # How it works
48//!
49//! The [`externref` macro](macro@externref) detects `Resource` args / return types
50//! for imported and exported functions. All `Resource` args or return types are replaced
51//! with `usize`s and a wrapper function is added that performs the necessary transform
52//! from / to `usize`.
53//! Additionally, a function signature describing where `Resource` args are located
54//! is recorded in a WASM custom section.
55//!
56//! To handle `usize` (~`i32` in WASM) <-> `externref` conversions, managing resources is performed
57//! using 3 function imports from a surrogate module:
58//!
59//! - Creating a `Resource` ("real" signature `fn(externref) -> usize`) stores a reference
60//! into an `externref` table and returns the table index. The index is what is actually
61//! stored within the `Resource`, meaning that `Resource`s can be easily placed on heap.
62//! - Getting a reference from a `Resource` ("real" signature `fn(usize) -> externref`)
63//! is an indexing operation for the `externref` table.
64//! - [`Resource::drop()`] ("real" signature `fn(usize)`) removes the reference from the table.
65//!
66//! Real `externref`s are patched back to the imported / exported functions
67//! by the WASM module post-processor:
68//!
69//! - Imports from a surrogate module referenced by `Resource` methods are replaced
70//! with local WASM functions. Functions for getting an `externref` from the table
71//! and dropping an `externref` are more or less trivial. Storing an `externref` is less so;
72//! we don't want to excessively grow the `externref`s table, thus we search for null refs
73//! among its existing elements first, and only grow the table if all existing table elements are
74//! occupied.
75//! - Patching changes function types, and as a result types of some locals.
76//! This is OK because the post-processor also changes the signatures of affected
77//! imported / exported functions. The success relies on the fact that
78//! a reference is only stored *immediately* after receiving it from the host;
79//! likewise, a reference is only obtained *immediately* before passing it to the host.
80//! `Resource`s can be dropped anywhere, but the corresponding `externref` removal function
81//! does not need its type changed.
82//!
83//! [reference type]: https://webassembly.github.io/spec/core/syntax/types.html#reference-types
84//! [`wasm-bindgen`]: https://crates.io/crates/wasm-bindgen
85//!
86//! # Crate features
87//!
88//! ## `processor`
89//!
90//! *(Off by default)*
91//!
92//! Enables WASM module processing via the [`processor`] module.
93//!
94//! ## `tracing`
95//!
96//! *(Off by default)*
97//!
98//! Enables tracing during [module processing](processor) with the [`tracing`] facade.
99//! Tracing events / spans mostly use `INFO` and `DEBUG` levels.
100//!
101//! [`tracing`]: https://docs.rs/tracing/
102//!
103//! # Examples
104//!
105//! Using the `#[externref]` macro and `Resource`s in WASM-targeting code:
106//!
107//! ```no_run
108//! use externref::{externref, Resource};
109//!
110//! // Two marker types for different resources.
111//! pub struct Sender(());
112//! pub struct Bytes(());
113//!
114//! #[externref]
115//! #[link(wasm_import_module = "test")]
116//! extern "C" {
117//! // This import will have signature `(externref, i32, i32) -> externref`
118//! // on host.
119//! fn send_message(
120//! sender: &Resource<Sender>,
121//! message_ptr: *const u8,
122//! message_len: usize,
123//! ) -> Resource<Bytes>;
124//!
125//! // `Option`s are used to deal with null references. This function will have
126//! // `(externref) -> i32` signature.
127//! fn message_len(bytes: Option<&Resource<Bytes>>) -> usize;
128//! // This one has `() -> externref` signature.
129//! fn last_sender() -> Option<Resource<Sender>>;
130//! }
131//!
132//! // This export will have signature `(externref)` on host.
133//! #[externref]
134//! #[export_name = "test_export"]
135//! pub extern "C" fn test_export(sender: Resource<Sender>) {
136//! let messages: Vec<_> = ["test", "42", "some other string"]
137//! .into_iter()
138//! .map(|msg| {
139//! unsafe { send_message(&sender, msg.as_ptr(), msg.len()) }
140//! })
141//! .collect();
142//! // ...
143//! // All 4 resources are dropped when exiting the function.
144//! }
145//! ```
146
147// Documentation settings.
148#![cfg_attr(docsrs, feature(doc_cfg))]
149#![doc(html_root_url = "https://docs.rs/externref/0.2.0")]
150// Linter settings.
151#![warn(missing_debug_implementations, missing_docs, bare_trait_objects)]
152#![warn(clippy::all, clippy::pedantic)]
153#![allow(
154 clippy::must_use_candidate,
155 clippy::module_name_repetitions,
156 clippy::inline_always
157)]
158
159use core::{alloc::Layout, marker::PhantomData, mem};
160
161mod error;
162#[cfg(feature = "processor")]
163#[cfg_attr(docsrs, doc(cfg(feature = "processor")))]
164pub mod processor;
165mod signature;
166
167pub use crate::{
168 error::{ReadError, ReadErrorKind},
169 signature::{BitSlice, BitSliceBuilder, Function, FunctionKind},
170};
171#[cfg(feature = "macro")]
172#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
173pub use externref_macro::externref;
174
175/// `externref` surrogate.
176///
177/// The post-processing logic replaces variables of this type with real `externref`s.
178#[doc(hidden)] // should only be used by macro-generated code
179#[derive(Debug)]
180#[repr(transparent)]
181pub struct ExternRef(usize);
182
183impl ExternRef {
184 /// Guard for imported function wrappers. The processor checks that each transformed function
185 /// has this guard as the first instruction.
186 ///
187 /// # Safety
188 ///
189 /// This guard should only be inserted by the `externref` macro.
190 #[inline(always)]
191 pub unsafe fn guard() {
192 #[cfg(target_arch = "wasm32")]
193 #[link(wasm_import_module = "externref")]
194 extern "C" {
195 #[link_name = "guard"]
196 fn guard();
197 }
198
199 #[cfg(target_arch = "wasm32")]
200 guard();
201 }
202}
203
204/// Host resource exposed to WASM.
205///
206/// Internally, a resource is just an index into the `externref`s table; thus, it is completely
207/// valid to store `Resource`s on heap (in a `Vec`, thread-local storage, etc.). The type param
208/// can be used for type safety.
209#[derive(Debug)]
210#[repr(C)]
211pub struct Resource<T> {
212 id: usize,
213 _ty: PhantomData<fn(T)>,
214}
215
216impl<T> Resource<T> {
217 /// Creates a new resource converting it from.
218 ///
219 /// # Safety
220 ///
221 /// This method must be called with an `externref` obtained from the host (as a return
222 /// type for an imported function or an argument for an exported function); it is not
223 /// a "real" `usize`. The proper use is ensured by the [`externref`] macro.
224 #[doc(hidden)] // should only be used by macro-generated code
225 #[inline(always)]
226 pub unsafe fn new(id: ExternRef) -> Option<Self> {
227 #[cfg(target_arch = "wasm32")]
228 #[link(wasm_import_module = "externref")]
229 extern "C" {
230 #[link_name = "insert"]
231 fn insert_externref(id: ExternRef) -> usize;
232 }
233
234 #[cfg(not(target_arch = "wasm32"))]
235 #[allow(clippy::needless_pass_by_value)]
236 unsafe fn insert_externref(id: ExternRef) -> usize {
237 id.0
238 }
239
240 let id = insert_externref(id);
241 if id == usize::MAX {
242 None
243 } else {
244 Some(Self {
245 id,
246 _ty: PhantomData,
247 })
248 }
249 }
250
251 /// Obtains an `externref` from this resource.
252 ///
253 /// # Safety
254 ///
255 /// The returned value of this method must be passed as an `externref` to the host
256 /// (as a return type of an exported function or an argument of the imported function);
257 /// it is not a "real" `usize`. The proper use is ensured by the [`externref`] macro.
258 #[doc(hidden)] // should only be used by macro-generated code
259 #[inline(always)]
260 pub unsafe fn raw(this: Option<&Self>) -> ExternRef {
261 #[cfg(target_arch = "wasm32")]
262 #[link(wasm_import_module = "externref")]
263 extern "C" {
264 #[link_name = "get"]
265 fn get_externref(id: usize) -> ExternRef;
266 }
267
268 #[cfg(not(target_arch = "wasm32"))]
269 unsafe fn get_externref(id: usize) -> ExternRef {
270 ExternRef(id)
271 }
272
273 get_externref(this.map_or(usize::MAX, |resource| resource.id))
274 }
275
276 /// Obtains an `externref` from this resource and drops the resource.
277 #[doc(hidden)] // should only be used by macro-generated code
278 #[inline(always)]
279 #[allow(clippy::needless_pass_by_value)]
280 pub unsafe fn take_raw(this: Option<Self>) -> ExternRef {
281 Self::raw(this.as_ref())
282 }
283
284 /// Upcasts this resource to a generic resource.
285 pub fn upcast(self) -> Resource<()> {
286 Resource {
287 id: self.leak_id(),
288 _ty: PhantomData,
289 }
290 }
291
292 #[inline]
293 fn leak_id(self) -> usize {
294 let id = self.id;
295 mem::forget(self);
296 id
297 }
298
299 /// Upcasts a reference to this resource to a generic resource reference.
300 pub fn upcast_ref(&self) -> &Resource<()> {
301 debug_assert_eq!(Layout::new::<Self>(), Layout::new::<Resource<()>>());
302
303 let ptr = (self as *const Self).cast::<Resource<()>>();
304 unsafe {
305 // SAFETY: All resource types have identical alignment (thanks to `repr(C)`),
306 // hence, casting among them is safe.
307 &*ptr
308 }
309 }
310}
311
312impl Resource<()> {
313 /// Downcasts this generic resource to a specific type.
314 ///
315 /// # Safety
316 ///
317 /// No checks are performed that the resource actually encapsulates what is meant
318 /// by `Resource<T>`. It is up to the caller to check this beforehand (e.g., by calling
319 /// a WASM import taking `&Resource<()>` and returning an app-specific resource kind).
320 pub unsafe fn downcast_unchecked<T>(self) -> Resource<T> {
321 Resource {
322 id: self.leak_id(),
323 _ty: PhantomData,
324 }
325 }
326}
327
328/// Drops the `externref` associated with this resource.
329impl<T> Drop for Resource<T> {
330 #[inline(always)]
331 fn drop(&mut self) {
332 #[cfg(target_arch = "wasm32")]
333 #[link(wasm_import_module = "externref")]
334 extern "C" {
335 #[link_name = "drop"]
336 fn drop_externref(id: usize);
337 }
338
339 #[cfg(not(target_arch = "wasm32"))]
340 unsafe fn drop_externref(_id: usize) {
341 // Do nothing
342 }
343
344 unsafe { drop_externref(self.id) };
345 }
346}
347
348#[cfg(doctest)]
349doc_comment::doctest!("../README.md");