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");