Skip to main content

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}