char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somgenerate ffi typedefs from typescript implf68be3e

main
14.8 KiB351 linesraw
1#![cfg(unix)]
2
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Output};
6
7const FIXTURE: &str = r#"
8#include <stdbool.h>
9#include <stddef.h>
10#include <stdint.h>
11#include <unistd.h>
12
13int32_t add_i32(int32_t a, int32_t b) { return a + b; }
14int64_t add_i64(int64_t a, int64_t b) { return a + b; }
15bool negate(bool value) { return !value; }
16float half(float value) { return value / 2.0f; }
17double twice(double value) { return value * 2.0; }
18size_t add_usize(size_t a, size_t b) { return a + b; }
19uint64_t identity_u64(uint64_t value) { return value; }
20
21void delayed_fill(uint8_t *bytes, uint8_t value, size_t length) {
22    usleep(50000);
23    if (length > 0) bytes[0] = value;
24}
25
26uint32_t sum_bytes(const uint8_t *bytes, size_t length) {
27    uint32_t total = 0;
28    for (size_t i = 0; i < length; i++) total += bytes[i];
29    return total;
30}
31
32void fill_bytes(uint8_t *bytes, uint8_t value, size_t length) {
33    for (size_t i = 0; i < length; i++) bytes[i] = value;
34}
35
36const char *greeting(void) { return "hello from ffi"; }
37void *identity(void *pointer) { return pointer; }
38
39typedef int (*int_fn)(int);
40static int inc(int value) { return value + 1; }
41int_fn get_inc(void) { return inc; }
42int apply_fn(int_fn function, int value) { return function(value); }
43"#;
44
45fn fixture() -> (tempfile::TempDir, PathBuf) {
46    let dir = tempfile::tempdir().unwrap();
47    let source = dir.path().join("fixture.c");
48    let library = dir.path().join("libfixture.so");
49    fs::write(&source, FIXTURE).unwrap();
50    let output = Command::new("cc")
51        .args(["-shared", "-fPIC", "-o"])
52        .arg(&library)
53        .arg(&source)
54        .output()
55        .unwrap();
56    assert!(
57        output.status.success(),
58        "cc failed: {}",
59        String::from_utf8_lossy(&output.stderr)
60    );
61    (dir, library)
62}
63
64fn run(dir: &tempfile::TempDir, source: &str, args: &[String]) -> Output {
65    let main = dir.path().join("main.js");
66    fs::write(&main, source).unwrap();
67    Command::new(env!("CARGO_BIN_EXE_avril"))
68        .arg("run")
69        .args(args)
70        .arg(&main)
71        .output()
72        .unwrap()
73}
74
75fn script(library: &Path, body: &str) -> String {
76    format!(
77        "import {{ dlopen }} from \"avril:ffi\";\nconst lib = dlopen({}, {{\n  add: {{ parameters: [\"i32\", \"i32\"], result: \"i32\", name: \"add_i32\" }},\n  add64: {{ parameters: [\"i64\", \"i64\"], result: \"i64\", name: \"add_i64\" }},\n  negate: {{ parameters: [\"bool\"], result: \"bool\" }},\n  half: {{ parameters: [\"f32\"], result: \"f32\" }},\n  twice: {{ parameters: [\"f64\"], result: \"f64\" }},\n  addSize: {{ parameters: [\"usize\", \"usize\"], result: \"usize\", name: \"add_usize\" }},\n  idU64: {{ parameters: [\"u64\"], result: \"u64\", name: \"identity_u64\" }},\n  slowSum: {{ parameters: [\"buffer\", \"usize\"], result: \"u32\", name: \"sum_bytes\", nonblocking: true }},\n  slowFill: {{ parameters: [\"buffer\", \"u8\", \"usize\"], result: \"void\", name: \"delayed_fill\", nonblocking: true }},\n  sum: {{ parameters: [\"buffer\", \"usize\"], result: \"u32\", name: \"sum_bytes\" }},\n  fill: {{ parameters: [\"buffer\", \"u8\", \"usize\"], result: \"void\", name: \"fill_bytes\" }},\n  greeting: {{ parameters: [], result: \"pointer\" }},\n  identity: {{ parameters: [\"pointer\"], result: \"pointer\" }},\n  getInc: {{ parameters: [], result: \"function\", name: \"get_inc\" }},\n  apply: {{ parameters: [\"function\", \"i32\"], result: \"i32\", name: \"apply_fn\" }},\n  slowAdd: {{ parameters: [\"i32\", \"i32\"], result: \"i32\", name: \"add_i32\", nonblocking: true }},\n}});\n{body}\n",
78        serde_json::to_string(&library.to_string_lossy()).unwrap()
79    )
80}
81
82#[test]
83fn ffi_calls_native_symbols() {
84    let (dir, library) = fixture();
85    let source = script(
86        &library,
87        r#"
88import { UnsafeFnPointer, UnsafePointer, UnsafePointerView } from "avril:ffi";
89const out = [];
90out.push(lib.symbols.add(40, 2));
91out.push(lib.symbols.add64(20n, 22n));
92out.push(lib.symbols.negate(false));
93out.push(lib.symbols.half(21));
94out.push(lib.symbols.twice(21));
95out.push(lib.symbols.addSize(20n, 22n));
96out.push(lib.symbols.idU64(9223372036854775808n));
97out.push(lib.symbols.idU64(18446744073709551615n));
98const bytes = new Uint8Array([1, 2, 3, 36]);
99out.push(lib.symbols.sum(bytes, 4));
100lib.symbols.fill(bytes, 9, 4);
101out.push(lib.symbols.sum(bytes, 4));
102out.push(new UnsafePointerView(lib.symbols.greeting()).getCString());
103const [pointer, view] = UnsafePointer.create(4);
104new UnsafePointerView(pointer).setInt32(42);
105out.push(view[0]);
106out.push(UnsafePointer.value(lib.symbols.identity(pointer)) === UnsafePointer.value(pointer));
107const inc = new UnsafeFnPointer(lib.symbols.getInc(), { parameters: ["i32"], result: "i32" });
108out.push(inc.call(41));
109out.push(lib.symbols.apply(inc, 41));
110out.push(await lib.symbols.slowAdd(40, 2));
111out.push(await lib.symbols.slowSum(new Uint8Array(0), 0));
112out.push(new UnsafePointerView(UnsafePointer.create(0)[0]).getArrayBuffer(0).byteLength);
113const nb = new Uint8Array([0, 0]);
114const pending = lib.symbols.slowFill(nb, 9, 2);
115nb[1] = 77;
116await pending;
117out.push(nb.join(","));
118console.log(out.join(" "));
119lib.close();
120try { lib.symbols.add(1, 2); console.log("still open"); } catch { console.log("closed"); }
121"#,
122    );
123    let output = run(&dir, &source, &[]);
124    assert!(
125        output.status.success(),
126        "{}",
127        String::from_utf8_lossy(&output.stderr)
128    );
129    assert_eq!(
130        String::from_utf8_lossy(&output.stdout).trim(),
131        "42 42 true 10.5 42 42 9223372036854775808 18446744073709551615 42 36 hello from ffi 42 true 42 42 42 0 0 9,77\nclosed"
132    );
133}
134
135#[test]
136fn ffi_requires_authority_and_honors_pledge() {
137    let (dir, library) = fixture();
138    let source = script(&library, "console.log(lib.symbols.add(40, 2));");
139    let denied = run(&dir, &source, &["-0".into()]);
140    assert!(!denied.status.success());
141    assert!(
142        String::from_utf8_lossy(&denied.stderr).contains("PermissionDenied: ffi"),
143        "{}",
144        String::from_utf8_lossy(&denied.stderr)
145    );
146
147    let deny_flag = run(
148        &dir,
149        &source,
150        &[format!("--deny-ffi={}", library.display())],
151    );
152    assert!(!deny_flag.status.success());
153    assert!(
154        String::from_utf8_lossy(&deny_flag.stderr).contains("PermissionDenied: ffi"),
155        "{}",
156        String::from_utf8_lossy(&deny_flag.stderr)
157    );
158
159    let pointers = "import { UnsafePointer, UnsafePointerView } from \"avril:ffi\";\nconst view = new UnsafePointerView(UnsafePointer.create(4)[0]);\nview.setInt32(42);\nconsole.log(view.getInt32());";
160    let pointers_denied_root = run(&dir, pointers, &["--deny-ffi=/".into()]);
161    assert!(!pointers_denied_root.status.success());
162    assert!(
163        String::from_utf8_lossy(&pointers_denied_root.stderr).contains("PermissionDenied: ffi"),
164        "{}",
165        String::from_utf8_lossy(&pointers_denied_root.stderr)
166    );
167    let pointers_denied = run(&dir, pointers, &["-0".into()]);
168    assert!(!pointers_denied.status.success());
169    assert!(
170        String::from_utf8_lossy(&pointers_denied.stderr).contains("PermissionDenied: ffi"),
171        "{}",
172        String::from_utf8_lossy(&pointers_denied.stderr)
173    );
174
175    let pointers_allowed = run(
176        &dir,
177        pointers,
178        &["-0".into(), format!("--allow-ffi={}", library.display())],
179    );
180    assert!(
181        pointers_allowed.status.success(),
182        "{}",
183        String::from_utf8_lossy(&pointers_allowed.stderr)
184    );
185    assert_eq!(
186        String::from_utf8_lossy(&pointers_allowed.stdout).trim(),
187        "42"
188    );
189
190    let allowed = run(
191        &dir,
192        &source,
193        &[format!("--allow-ffi={}", library.display())],
194    );
195    assert!(
196        allowed.status.success(),
197        "{}",
198        String::from_utf8_lossy(&allowed.stderr)
199    );
200    assert_eq!(String::from_utf8_lossy(&allowed.stdout).trim(), "42");
201
202    let source = format!(
203        "import {{ pledge }} from \"avril:std\";\nimport {{ dlopen }} from \"avril:ffi\";\npledge({{ ffi: false }});\ntry {{ dlopen({}, {{ add: {{ parameters: [\"i32\", \"i32\"], result: \"i32\", name: \"add_i32\" }} }}); console.log(\"allowed\"); }} catch (error) {{ console.log(error.message.includes(\"PermissionDenied: ffi\") ? \"denied\" : error.message); }}",
204        serde_json::to_string(&library.to_string_lossy()).unwrap()
205    );
206    let output = run(
207        &dir,
208        &source,
209        &[format!("--allow-ffi={}", library.display())],
210    );
211    assert!(
212        output.status.success(),
213        "{}",
214        String::from_utf8_lossy(&output.stderr)
215    );
216    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "denied");
217}
218
219#[test]
220fn ffi_rejects_bad_calls() {
221    let (dir, library) = fixture();
222    let source = script(
223        &library,
224        r#"
225const out = [];
226try { lib.symbols.add(2147483648, 0); } catch (error) { out.push(error instanceof TypeError ? "range" : error.message); }
227try { lib.symbols.add(1); } catch (error) { out.push("arity"); }
228try { lib.symbols.add("7", 0); } catch (error) { out.push(error instanceof TypeError ? "string" : error.message); }
229const victim = new ArrayBuffer(16);
230const view = new Uint8Array(victim);
231try { lib.symbols.fill(view, { valueOf() { victim.transfer(8); return 1; } }, 16); out.push("coerced"); } catch (error) { out.push(error instanceof TypeError ? "detach" : error.message); }
232import { DynamicLibrary } from "avril:ffi";
233try { new DynamicLibrary(1, {}); out.push("forged"); } catch { out.push("forge"); }
234const buf2 = new ArrayBuffer(4096);
235const view2 = new Uint8Array(buf2);
236const realMap = Array.prototype.map;
237Array.prototype.map = function (fn) { const mapped = Reflect.apply(realMap, this, [fn]); if (mapped.length === 3 && mapped[0] === view2) Object.defineProperty(mapped, 2, { get() { buf2.transfer(16); return 4096; } }); return mapped; };
238try { lib.symbols.fill(view2, 65, 4096); out.push(view2[0] === 65 ? "getter" : "getter-fail"); } finally { Array.prototype.map = realMap; }
239console.log(out.join(" "));
240"#,
241    );
242    let output = run(&dir, &source, &[]);
243    assert!(
244        output.status.success(),
245        "{}",
246        String::from_utf8_lossy(&output.stderr)
247    );
248    assert_eq!(
249        String::from_utf8_lossy(&output.stdout).trim(),
250        "range arity string detach forge getter"
251    );
252
253    let missing = format!(
254        "import {{ dlopen }} from \"avril:ffi\";\ndlopen({}, {{ nope: {{ parameters: [], result: \"void\" }} }});",
255        serde_json::to_string(&library.to_string_lossy()).unwrap()
256    );
257    let output = run(&dir, &missing, &[]);
258    assert!(!output.status.success());
259}
260
261#[test]
262fn definitions_keep_getter_order_and_pointer_objects_keep_their_shape() {
263    let (dir, library) = fixture();
264    let source = format!(
265        "const libraryPath = {};\n",
266        serde_json::to_string(&library.to_string_lossy()).unwrap()
267    ) + r#"
268        import { dlopen, Pointer, UnsafePointer, UnsafePointerView, UnsafeFnPointer, DynamicLibrary } from "avril:ffi";
269        import * as ffi from "avril:ffi";
270        if (Object.keys(ffi).sort().join() !== "DynamicLibrary,Pointer,UnsafeFnPointer,UnsafePointer,UnsafePointerView,dlopen") {
271            throw new Error("public namespace");
272        }
273        for (const name of ["Pointer", "UnsafePointerView", "UnsafeFnPointer", "DynamicLibrary"]) {
274            const constructor = ffi[name];
275            if (constructor.name !== name || constructor.prototype.constructor !== constructor
276                || Object.hasOwn(constructor, Symbol.hasInstance)
277                || !(Object.create(constructor.prototype) instanceof constructor)) {
278                throw new Error(`constructor shape: ${name}`);
279            }
280        }
281        const order = [];
282        let iterations = 0;
283        const parameters = ["i32", "i32"];
284        parameters[Symbol.iterator] = function* () {
285            if (this !== parameters) throw new Error("parameter iterator receiver");
286            iterations++;
287            yield "i32"; yield "i32";
288        };
289        const definitions = {
290            get add() {
291                order.push("symbol");
292                return {
293                    get parameters() { order.push("parameters"); return parameters; },
294                    get result() { order.push("result"); return "i32"; },
295                    get nonblocking() { order.push("nonblocking"); return false; },
296                    get name() { order.push("name"); return "add_i32"; },
297                };
298            },
299        };
300        const library = dlopen({
301            [Symbol.toPrimitive](hint) {
302                if (hint !== "string") throw new Error("path hint");
303                order.push("path"); return libraryPath;
304            },
305        }, definitions);
306        if (order.join() !== "symbol,parameters,result,nonblocking,name,path"
307            || iterations !== 2 || library.symbols.add(20, 22) !== 42) {
308            throw new Error("definition conversion");
309        }
310        const pointer = new Pointer("0");
311        if (Reflect.ownKeys(pointer).length !== 0 || !Object.isFrozen(pointer)
312            || UnsafePointer.value(pointer) !== 0n
313            || Object.keys(library).join() !== "symbols"
314            || !Object.isFrozen(library) || !Object.isFrozen(library.symbols)) {
315            throw new Error("resource shape");
316        }
317        if (dlopen.length !== 2 || Pointer.length !== 1 || UnsafePointerView.length !== 1
318            || UnsafeFnPointer.length !== 2 || DynamicLibrary.length !== 3
319            || library.symbols.add.length !== 0 || UnsafeFnPointer.prototype.call.length !== 0) {
320            throw new Error("reflected arity");
321        }
322        class DerivedPointer extends Pointer {}
323        const derived = new DerivedPointer(0n);
324        const proxiedConstructor = new Proxy(Pointer, {});
325        if (!(derived instanceof Pointer) || !(derived instanceof DerivedPointer)
326            || !(derived instanceof proxiedConstructor) || UnsafePointer.value(derived) !== 0n
327            || !(new proxiedConstructor(0n) instanceof Pointer) || !(library instanceof DynamicLibrary)) {
328            throw new Error("constructor identity");
329        }
330        const fn = new UnsafeFnPointer(pointer, { parameters: [], result: "void" });
331        if (!Object.isFrozen(fn) || fn.address !== 0n || Object.keys(fn).join() !== "address"
332            || !(fn instanceof UnsafeFnPointer) || !(new UnsafePointerView(pointer) instanceof UnsafePointerView)) {
333            throw new Error("pointer instance shape");
334        }
335        let forgedRejected = false;
336        try { UnsafePointer.value(Object.create(Pointer.prototype)); }
337        catch (error) { forgedRejected = error instanceof TypeError; }
338        if (!forgedRejected) throw new Error("forged pointer accepted");
339        if (library.close() !== undefined || library.close() !== undefined || !library.closed) {
340            throw new Error("library close");
341        }
342        console.log("preserved");
343    "#;
344    let output = run(&dir, &source, &[]);
345    assert!(
346        output.status.success(),
347        "{}",
348        String::from_utf8_lossy(&output.stderr)
349    );
350    assert_eq!(output.stdout, b"preserved\n");
351}