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}