cove_wasm/abi.rs
1//! The C ABI a JavaScript embedder calls, and the two allocation primitives
2//! that make it usable.
3//!
4//! # Why there is no `wasm-bindgen`
5//!
6//! This workspace has exactly one third-party dependency (`toml`, in
7//! `cove-sema`); the CLI parses its own arguments and writes its own JSON. A
8//! binding generator would be the largest dependency in the tree, and a probe
9//! established before any of this was written that it buys nothing here: five
10//! `extern "C"` functions and a length prefix are the whole of what crossing
11//! this boundary needs.
12//!
13//! Five and not four because a debug run was added after the first four
14//! shipped. That it was one more `extern "C"` function taking two more
15//! integers, with nothing else about the boundary moved, is the strongest
16//! evidence available that the probe's conclusion was right.
17//!
18//! Six and not five for the same reason a second time: syntax highlighting
19//! was added, and it is [`cove_lex`] — one more `extern "C"` function taking
20//! the two integers every one of them takes, answering the same
21//! length-prefixed JSON. Three additions in a row that cost one function each
22//! is the evidence, and it is now hard to argue with.
23//!
24//! Seven and not six because the disassembly pane wanted colouring too, and
25//! it is [`cove_lex_ir`]: the same two integers, the same length-prefixed
26//! JSON, the same tiling. Four in a row.
27//!
28//! # The calling convention
29//!
30//! Two directions, one shape each.
31//!
32//! *Into* the module: the caller asks for `n` bytes with [`cove_alloc`],
33//! writes UTF-8 into the module's exported `memory` at the returned offset,
34//! and passes `(offset, n)`. It owns those bytes and releases them with
35//! [`cove_free`]; nothing here takes them.
36//!
37//! *Out of* the module: [`cove_compile`], [`cove_run`], [`cove_debug`],
38//! [`cove_lex`] and [`cove_lex_ir`] each answer one offset into the same
39//! memory. The four bytes there are a little-endian `u32` length, and the `n`
40//! bytes after them are UTF-8 JSON.
41//! The caller decodes them and releases the whole block with
42//! `cove_free(offset, n + 4)`.
43//!
44//! The length prefix is what removes the alternative — a second exported
45//! function answering "how long was the last answer?" — and with it the
46//! module-level state such a function would need. Two calls in flight at once
47//! would have raced over it. This has nothing to race over.
48//!
49//! # The one import
50//!
51//! `cove.cove_now_millis() -> f64`, described by [`cove_runtime`]'s
52//! `wallclock` module. It is the monotonic clock, and without it a deadline
53//! could not be enforced. A module instantiated without it does not load.
54
55use std::alloc::{alloc, dealloc, Layout};
56
57use crate::{compile_json, debug_json, lex_ir_json, lex_json, run_json};
58
59/// Reserves `len` bytes of the module's memory and answers where they start.
60///
61/// Byte-aligned, because everything that crosses this boundary is UTF-8 or a
62/// little-endian integer read a byte at a time by a `DataView`.
63///
64/// A zero-length request answers a non-null aligned offset that must still be
65/// passed back to [`cove_free`], so a caller never has to special-case the
66/// empty string.
67///
68/// # Safety
69///
70/// The caller owns the returned block until it hands it to [`cove_free`].
71/// Nothing else in this module will free it.
72#[no_mangle]
73pub extern "C" fn cove_alloc(len: usize) -> *mut u8 {
74 // SAFETY: the alignment is 1, which is non-zero and a power of two, and
75 // `len` rounded up to it cannot overflow because it is already a
76 // multiple of it.
77 unsafe {
78 let layout = Layout::from_size_align_unchecked(len.max(1), 1);
79 alloc(layout)
80 }
81}
82
83/// Releases a block [`cove_alloc`] answered, or an answer block one of the
84/// entry points answered.
85///
86/// For an answer block, `len` is the four-byte prefix plus the length it
87/// holds — the JavaScript side has both numbers by the time it decodes the
88/// payload, so asking it for the total is cheaper than storing the total here.
89///
90/// # Safety
91///
92/// `ptr` must have come from [`cove_alloc`] or from an entry point of this
93/// module, `len` must be the size that block was created with, and the block
94/// must not be freed twice.
95#[no_mangle]
96pub unsafe extern "C" fn cove_free(ptr: *mut u8, len: usize) {
97 if ptr.is_null() {
98 return;
99 }
100 dealloc(ptr, Layout::from_size_align_unchecked(len.max(1), 1));
101}
102
103/// Checks and lowers `source`, and answers the diagnostics and the
104/// disassembly as an answer block. See [`crate::compile_json`].
105///
106/// # Safety
107///
108/// `source` must point at `len` initialized bytes inside this module's
109/// memory. They are read and not retained; the caller still owns them.
110#[no_mangle]
111pub unsafe extern "C" fn cove_compile(source: *const u8, len: usize) -> *mut u8 {
112 answer(compile_json(&read(source, len)))
113}
114
115/// Checks, lowers and runs `source`, and answers what it printed, what it
116/// produced and how it ended. See [`crate::run_json`].
117///
118/// `fuel` and `deadline_ms` are the two bounds a page can put on a run; zero
119/// means "whatever [`crate::RUN_LIMITS`] says", which is a bound and not the
120/// absence of one. A deadline is enforced against the imported clock, so it
121/// does what it says.
122///
123/// They are `u32` and not `u64` although [`cove_runtime::Limits`] counts fuel
124/// in `u64`, because a `u64` parameter is a wasm `i64`, and a wasm `i64`
125/// reaches JavaScript as a `BigInt`: every caller would have to write `10n`
126/// where it means ten. Four billion units of fuel is far past what a tab
127/// should spend before a page decides it has hung, and forty-nine days is
128/// past what a deadline in a browser can mean, so nothing is lost that the
129/// awkwardness would buy back.
130///
131/// # Safety
132///
133/// As [`cove_compile`].
134#[no_mangle]
135pub unsafe extern "C" fn cove_run(
136 source: *const u8,
137 len: usize,
138 fuel: u32,
139 deadline_ms: u32,
140) -> *mut u8 {
141 answer(run_json(
142 &read(source, len),
143 (fuel != 0).then_some(u64::from(fuel)),
144 (deadline_ms != 0).then_some(u64::from(deadline_ms)),
145 ))
146}
147
148/// Checks, lowers and runs `source` under a recording debugger, and answers
149/// what [`cove_run`] answers plus the recording. See [`crate::debug_json`].
150///
151/// `fuel` and `deadline_ms` are [`cove_run`]'s, meaning the same things. A
152/// debugged run is slower than a run — the machine asks the recorder before
153/// every instruction — so a program that finished inside the default
154/// deadline may not finish inside it here. That is reported as a `deadline`
155/// outcome beside the recording of everything up to it, which is the honest
156/// answer and not a silent one.
157///
158/// `moments` is how many moments to keep, with zero meaning
159/// [`crate::record::MOMENTS`] and anything larger than
160/// [`crate::record::MOST_MOMENTS`] clamped to it. It is a `u32` for
161/// [`cove_run`]'s reason: a `u64` parameter reaches JavaScript as a `BigInt`.
162///
163/// # Safety
164///
165/// As [`cove_compile`].
166#[no_mangle]
167pub unsafe extern "C" fn cove_debug(
168 source: *const u8,
169 len: usize,
170 fuel: u32,
171 deadline_ms: u32,
172 moments: u32,
173) -> *mut u8 {
174 answer(debug_json(
175 &read(source, len),
176 (fuel != 0).then_some(u64::from(fuel)),
177 (deadline_ms != 0).then_some(u64::from(deadline_ms)),
178 moments as usize,
179 ))
180}
181
182/// Lexes `source` and answers a colour for every part of it, as an answer
183/// block. See [`crate::lex_json`].
184///
185/// The one entry point that neither checks nor runs anything: it is the
186/// lexer and nothing after it, so it costs a pass over the text. That is what
187/// lets a page call it on every keystroke, and it is why a page can call it
188/// on its own thread rather than on the worker a run needs — the reason the
189/// worker exists is that a Cove program can loop, and lexing cannot.
190///
191/// # Safety
192///
193/// As [`cove_compile`].
194#[no_mangle]
195pub unsafe extern "C" fn cove_lex(source: *const u8, len: usize) -> *mut u8 {
196 answer(lex_json(&read(source, len)))
197}
198
199/// Colours the disassembly in `text` and answers a tiling of it, as an
200/// answer block. See [`crate::lex_ir_json`].
201///
202/// `text` is what [`cove_compile`] or [`cove_run`] put in `ir`, handed back
203/// so that the colouring is of the text the caller is actually showing. Like
204/// [`cove_lex`] it neither checks nor runs anything, and for the same reason
205/// it is a call a page can make on its own thread.
206///
207/// # Safety
208///
209/// As [`cove_compile`].
210#[no_mangle]
211pub unsafe extern "C" fn cove_lex_ir(text: *const u8, len: usize) -> *mut u8 {
212 answer(lex_ir_json(&read(text, len)))
213}
214
215/// The caller's bytes as a string.
216///
217/// Lossy rather than refusing: the source of a playground is whatever the
218/// page's `TextEncoder` produced, and a replacement character in a string
219/// literal is a thing the parser can report a span for, while "your bytes
220/// were not UTF-8" is not.
221///
222/// # Safety
223///
224/// As [`cove_compile`].
225unsafe fn read(source: *const u8, len: usize) -> String {
226 if source.is_null() || len == 0 {
227 return String::new();
228 }
229 String::from_utf8_lossy(std::slice::from_raw_parts(source, len)).into_owned()
230}
231
232/// Copies `json` into a freshly allocated length-prefixed block and answers
233/// where it starts.
234fn answer(json: String) -> *mut u8 {
235 let bytes = json.into_bytes();
236 // A length that does not fit in the prefix cannot be described to the
237 // caller at all, and a truncated answer would be a lie about what the
238 // run did. Nothing this module produces approaches 4 GiB; if something
239 // ever did, the empty object is the one answer that cannot be
240 // misread as a result.
241 let len = match u32::try_from(bytes.len()) {
242 Ok(len) => len,
243 Err(_) => return answer("{}".to_string()),
244 };
245 let total = bytes.len() + 4;
246 let ptr = cove_alloc(total);
247 if ptr.is_null() {
248 return ptr;
249 }
250 // SAFETY: `cove_alloc` answered `total` bytes and both writes are inside
251 // them.
252 unsafe {
253 std::ptr::copy_nonoverlapping(len.to_le_bytes().as_ptr(), ptr, 4);
254 std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(4), bytes.len());
255 }
256 ptr
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 /// Round-trips a string through the ABI the way a page does: allocate,
264 /// write, call, decode the prefix, free both blocks.
265 ///
266 /// This is the test that would catch the prefix being written big-endian,
267 /// or the payload starting at the wrong offset — the two mistakes that a
268 /// browser reports as mojibake and nothing else.
269 #[test]
270 fn an_answer_is_a_little_endian_length_and_then_that_many_bytes() {
271 let source = "export fn main() -> Int { 1 }";
272 let held = cove_alloc(source.len());
273 // SAFETY: `held` names `source.len()` bytes just reserved.
274 unsafe { std::ptr::copy_nonoverlapping(source.as_ptr(), held, source.len()) };
275
276 // SAFETY: `held` names `source.len()` initialized bytes.
277 let answer = unsafe { cove_compile(held, source.len()) };
278 // SAFETY: an answer block starts with four length bytes.
279 let len = u32::from_le_bytes(
280 unsafe { std::slice::from_raw_parts(answer, 4) }
281 .try_into()
282 .expect("four bytes are four bytes"),
283 ) as usize;
284 // SAFETY: the block holds `len` payload bytes after the prefix.
285 let json =
286 String::from_utf8(unsafe { std::slice::from_raw_parts(answer.add(4), len).to_vec() })
287 .expect("the answer is UTF-8");
288
289 assert!(json.starts_with('{'), "{json}");
290 assert!(json.contains("\"ir\""), "{json}");
291 assert_eq!(json.len(), len, "the prefix counts the payload");
292
293 // SAFETY: both blocks came from `cove_alloc` with these sizes.
294 unsafe {
295 cove_free(held, source.len());
296 cove_free(answer, len + 4);
297 }
298 }
299
300 #[test]
301 fn an_empty_source_is_read_rather_than_refused() {
302 // SAFETY: a null pointer with a zero length is the empty string.
303 let source = unsafe { read(std::ptr::null(), 0) };
304 assert_eq!(source, "");
305 }
306}