Skip to main content

holger_plugin_abi/
guest.rs

1//! **The guest side of the ABI, written once and generated per module.**
2//!
3//! A wasm module has to export `#[no_mangle] extern "C"` symbols, and those
4//! cannot come from a generic — so without this macro every `-wasm` crate would
5//! carry its own copy of the same ~130 lines of pointer juggling, and two copies
6//! of an ABI is exactly how one half drifts from the other. [`export_package_handler!`]
7//! emits all eight exports plus the host-backed [`crate::BlobStore`] from one
8//! line, so a handler's wasm crate really is glue and nothing else (LAW 5).
9//!
10//! Everything it emits is `#[cfg(target_arch = "wasm32")]`. On any other target
11//! the crate compiles to an empty library — which is what lets a `-wasm` crate be
12//! an ordinary workspace member that `cargo check --workspace` covers, without a
13//! native build trying to link `env::host_store_get`.
14
15/// Generate the eight ABI exports for a [`crate::PackageHandler`].
16///
17/// ```ignore
18/// holger_plugin_abi::export_package_handler!(holger_handler_skidbladnir::SkidbladnirHandler);
19/// ```
20///
21/// The type must implement `PackageHandler` and be constructible with
22/// `Default::default()`. Nothing else is required of the calling crate, and
23/// nothing about the handler's *logic* passes through here — this macro moves
24/// bytes and calls the handler.
25#[macro_export]
26macro_rules! export_package_handler {
27    ($handler:ty) => {
28        #[cfg(target_arch = "wasm32")]
29        const _: () = {
30            use ::std::cell::{Cell, RefCell};
31            use ::std::vec::Vec;
32
33            use $crate::{
34                wire, BlobStore, PackageHandler, WireArtifactId, WireHttpRequest,
35                WireListRequest, WirePutRequest, Writer,
36            };
37
38            ::std::thread_local! {
39                /// Every buffer handed across the ABI stays owned here for the
40                /// life of the instance: the host writes into these addresses
41                /// and reads results back out of them, so freeing one would be a
42                /// use-after-free across the boundary. The host creates a fresh
43                /// wasmtime `Store` per call, so this never grows beyond one
44                /// request's worth.
45                static ALLOCS: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
46                /// Byte length of the value the last export returned a pointer
47                /// to, read by the host's `result_len()` immediately after.
48                static RESULT_LEN: Cell<u32> = const { Cell::new(0) };
49            }
50
51            // `wasm_import_module` MUST be spelled here and must match
52            // `$crate::imports::MODULE`. Without the attribute `rust-lld`
53            // treats these as ordinary undefined symbols and refuses to link
54            // the cdylib, rather than emitting them as wasm imports.
55            #[link(wasm_import_module = "env")]
56            extern "C" {
57                fn host_store_get(key_ptr: u32, key_len: u32) -> u64;
58                fn host_store_put(key_ptr: u32, key_len: u32, data_ptr: u32, data_len: u32) -> u64;
59                fn host_store_list(prefix_ptr: u32, prefix_len: u32) -> u64;
60                fn host_store_size(key_ptr: u32, key_len: u32) -> u64;
61            }
62
63            /// Reserve `size` bytes and return their address.
64            #[no_mangle]
65            pub extern "C" fn alloc(size: u32) -> u32 {
66                ALLOCS.with(|a| {
67                    let mut buf = ::std::vec![0u8; size as usize];
68                    let ptr = buf.as_mut_ptr() as u32;
69                    a.borrow_mut().push(buf);
70                    ptr
71                })
72            }
73
74            #[no_mangle]
75            pub extern "C" fn result_len() -> u32 {
76                RESULT_LEN.with(|l| l.get())
77            }
78
79            /// Copy `bytes` into linear memory, record the length, return the address.
80            fn ret(bytes: &[u8]) -> u32 {
81                let ptr = alloc(bytes.len() as u32);
82                // SAFETY: `ptr` was just returned by `alloc`, which reserved
83                // exactly `bytes.len()` bytes and keeps the buffer alive in ALLOCS.
84                unsafe {
85                    ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
86                }
87                RESULT_LEN.with(|l| l.set(bytes.len() as u32));
88                ptr
89            }
90
91            /// Borrow an argument the host wrote into linear memory.
92            ///
93            /// # Safety
94            /// `ptr`/`len` must be an address previously returned by `alloc`,
95            /// holding at least `len` bytes — which is the host's half of the
96            /// contract for every export below.
97            unsafe fn arg<'a>(ptr: u32, len: u32) -> &'a [u8] {
98                ::std::slice::from_raw_parts(ptr as *const u8, len as usize)
99            }
100
101            /// Unpack a `ptr << 32 | len` result from a host import.
102            /// `0` means the host call itself failed — never "absent", which is
103            /// carried inside the payload as an encoded `Option`.
104            fn unpack(packed: u64) -> Option<Vec<u8>> {
105                if packed == 0 {
106                    return None;
107                }
108                let ptr = (packed >> 32) as u32;
109                let len = (packed & 0xffff_ffff) as usize;
110                // SAFETY: the host allocated this through our own `alloc`, so it
111                // is inside linear memory and owned by ALLOCS.
112                Some(unsafe { ::std::slice::from_raw_parts(ptr as *const u8, len) }.to_vec())
113            }
114
115            /// The store, reached through the four host imports. This is the
116            /// **only** thing that differs from the native backend: the handler
117            /// above it is the same code.
118            struct HostStore;
119
120            impl BlobStore for HostStore {
121                fn get(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
122                    let packed =
123                        unsafe { host_store_get(key.as_ptr() as u32, key.len() as u32) };
124                    let raw = unpack(packed)
125                        .ok_or_else(|| ::std::format!("host_store_get({key}) failed"))?;
126                    wire::decode_store_get(&raw).map_err(|e| e.message())
127                }
128
129                fn put(&self, key: &str, data: &[u8]) -> Result<(), String> {
130                    let packed = unsafe {
131                        host_store_put(
132                            key.as_ptr() as u32,
133                            key.len() as u32,
134                            data.as_ptr() as u32,
135                            data.len() as u32,
136                        )
137                    };
138                    let raw = unpack(packed)
139                        .ok_or_else(|| ::std::format!("host_store_put({key}) failed"))?;
140                    wire::decode_unit_response(&raw).map_err(|e| e.message())?
141                }
142
143                fn list(&self, prefix: &str) -> Result<Vec<(String, u64)>, String> {
144                    let packed =
145                        unsafe { host_store_list(prefix.as_ptr() as u32, prefix.len() as u32) };
146                    let raw = unpack(packed)
147                        .ok_or_else(|| ::std::format!("host_store_list({prefix}) failed"))?;
148                    wire::decode_store_listing(&raw).map_err(|e| e.message())
149                }
150
151                fn size(&self, key: &str) -> Result<Option<u64>, String> {
152                    let packed =
153                        unsafe { host_store_size(key.as_ptr() as u32, key.len() as u32) };
154                    let raw = unpack(packed)
155                        .ok_or_else(|| ::std::format!("host_store_size({key}) failed"))?;
156                    let payload = wire::decode_store_get(&raw).map_err(|e| e.message())?;
157                    match payload {
158                        None => Ok(None),
159                        Some(b) if b.len() == 8 => {
160                            let mut a = [0u8; 8];
161                            a.copy_from_slice(&b);
162                            Ok(Some(u64::from_le_bytes(a)))
163                        }
164                        Some(b) => Err(::std::format!(
165                            "host_store_size({key}) returned {} bytes, expected 8",
166                            b.len()
167                        )),
168                    }
169                }
170            }
171
172            fn handler() -> $handler {
173                <$handler as ::std::default::Default>::default()
174            }
175
176            #[no_mangle]
177            pub extern "C" fn plugin_manifest() -> u32 {
178                ret(&handler().manifest().to_bytes())
179            }
180
181            /// # Safety
182            /// `ptr`/`len` must address an encoded `WireArtifactId` written by
183            /// the host through `alloc`.
184            #[no_mangle]
185            pub unsafe extern "C" fn plugin_fetch(ptr: u32, len: u32) -> u32 {
186                // A malformed argument is reported as an Err on the wire, not as
187                // "no such artifact": the host must be able to tell an ABI skew
188                // from a genuine miss.
189                let out = match WireArtifactId::from_bytes(unsafe { arg(ptr, len) }) {
190                    Ok(id) => handler().fetch(&HostStore, &id),
191                    Err(e) => Err(e.message()),
192                };
193                ret(&wire::encode_fetch_response(&out))
194            }
195
196            /// # Safety
197            /// As [`plugin_fetch`], for an encoded `WirePutRequest`.
198            #[no_mangle]
199            pub unsafe extern "C" fn plugin_put(ptr: u32, len: u32) -> u32 {
200                let out = match WirePutRequest::from_bytes(unsafe { arg(ptr, len) }) {
201                    Ok(req) => handler().put(&HostStore, &req.id, &req.data),
202                    Err(e) => Err(e.message()),
203                };
204                ret(&wire::encode_unit_response(&out))
205            }
206
207            /// # Safety
208            /// As [`plugin_fetch`], for an encoded `WireListRequest`.
209            #[no_mangle]
210            pub unsafe extern "C" fn plugin_list(ptr: u32, len: u32) -> u32 {
211                let out = match WireListRequest::from_bytes(unsafe { arg(ptr, len) }) {
212                    Ok(req) => {
213                        handler().list(&HostStore, req.name_filter.as_deref(), req.limit as usize)
214                    }
215                    Err(e) => Err(e.message()),
216                };
217                ret(&wire::encode_list_response(&out))
218            }
219
220            /// # Safety
221            /// As [`plugin_fetch`], for a length-prefixed suburl string.
222            #[no_mangle]
223            pub unsafe extern "C" fn plugin_coordinate_for_path(ptr: u32, len: u32) -> u32 {
224                let mut r = $crate::Reader::new(unsafe { arg(ptr, len) });
225                let out = match r.str("suburl").and_then(|s| r.expect_end("suburl").map(|_| s)) {
226                    Ok(suburl) => handler().coordinate_for_path(&suburl),
227                    // This verb's return type has no error arm — it is the
228                    // quarantine gate's lookup, whose `None` means "not gated by
229                    // coordinate". A skewed argument therefore loses a gate
230                    // check; it can never serve bytes that should not have been.
231                    Err(_) => None,
232                };
233                ret(&wire::encode_coordinate_response(&out))
234            }
235
236            /// # Safety
237            /// As [`plugin_fetch`], for an encoded `WireHttpRequest`.
238            #[no_mangle]
239            pub unsafe extern "C" fn plugin_http(ptr: u32, len: u32) -> u32 {
240                let out = match WireHttpRequest::from_bytes(unsafe { arg(ptr, len) }) {
241                    Ok(req) => handler().http(&HostStore, &req),
242                    Err(e) => Err(e.message()),
243                };
244                ret(&wire::encode_http_response(&out))
245            }
246
247            // Silence "unused" for the one import a handler may not reach.
248            #[allow(dead_code)]
249            fn _writer_is_used() -> Writer {
250                Writer::new()
251            }
252        };
253    };
254}