char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f68be3e
main
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) { 22usleep(50000); 23if (length > 0) bytes[0] = value; 24} 25 26uint32_t sum_bytes(const uint8_t *bytes, size_t length) { 27uint32_t total = 0; 28for (size_t i = 0; i < length; i++) total += bytes[i]; 29return total; 30} 31 32void fill_bytes(uint8_t *bytes, uint8_t value, size_t length) { 33for (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 ) { 46let dir = tempfile:: tempdir (). unwrap (); 47let source = dir. path (). join ( "fixture.c" ); 48let library = dir. path (). join ( "libfixture.so" ); 49 fs:: write ( & source, FIXTURE ). unwrap (); 50let output =Command :: new ( "cc" ) 51. args ([ "-shared" , "-fPIC" , "-o" ]) 52. arg ( & library) 53. arg ( & source) 54. output () 55. unwrap (); 56assert! ( 57 output. status . success (), 58"cc failed: {}" , 59String :: from_utf8_lossy ( & output. stderr ) 60); 61( dir, library) 62} 63 64fn run ( dir : & tempfile:: TempDir , source : & str , args : & [ String ]) ->Output { 65let main = dir. path (). join ( "main.js" ); 66 fs:: write ( & main, source). unwrap (); 67Command :: 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 { 76format! ( 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 () { 84let ( dir, library) =fixture (); 85let source =script ( 86& library, 87r#" 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); 123let output =run ( & dir, & source, & []); 124assert! ( 125 output. status . success (), 126"{}" , 127String :: from_utf8_lossy ( & output. stderr ) 128); 129assert_eq! ( 130String :: 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 () { 137let ( dir, library) =fixture (); 138let source =script ( & library, "console.log(lib.symbols.add(40, 2));" ); 139let denied =run ( & dir, & source, & [ "-0" . into ()]); 140assert! ( !denied. status . success ()); 141assert! ( 142String :: from_utf8_lossy ( & denied. stderr ). contains ( "PermissionDenied: ffi" ), 143"{}" , 144String :: from_utf8_lossy ( & denied. stderr ) 145); 146 147let deny_flag =run ( 148& dir, 149& source, 150& [ format! ( "--deny-ffi={}" , library. display ())], 151); 152assert! ( !deny_flag. status . success ()); 153assert! ( 154String :: from_utf8_lossy ( & deny_flag. stderr ). contains ( "PermissionDenied: ffi" ), 155"{}" , 156String :: from_utf8_lossy ( & deny_flag. stderr ) 157); 158 159let pointers ="import { UnsafePointer, UnsafePointerView } from \"avril:ffi\";\nconst view = new UnsafePointerView(UnsafePointer.create(4)[0]);\nview.setInt32(42);\nconsole.log(view.getInt32());" ; 160let pointers_denied_root =run ( & dir, pointers, & [ "--deny-ffi=/" . into ()]); 161assert! ( !pointers_denied_root. status . success ()); 162assert! ( 163String :: from_utf8_lossy ( & pointers_denied_root. stderr ). contains ( "PermissionDenied: ffi" ), 164"{}" , 165String :: from_utf8_lossy ( & pointers_denied_root. stderr ) 166); 167let pointers_denied =run ( & dir, pointers, & [ "-0" . into ()]); 168assert! ( !pointers_denied. status . success ()); 169assert! ( 170String :: from_utf8_lossy ( & pointers_denied. stderr ). contains ( "PermissionDenied: ffi" ), 171"{}" , 172String :: from_utf8_lossy ( & pointers_denied. stderr ) 173); 174 175let pointers_allowed =run ( 176& dir, 177 pointers, 178& [ "-0" . into (), format! ( "--allow-ffi={}" , library. display ())], 179); 180assert! ( 181 pointers_allowed. status . success (), 182"{}" , 183String :: from_utf8_lossy ( & pointers_allowed. stderr ) 184); 185assert_eq! ( 186String :: from_utf8_lossy ( & pointers_allowed. stdout ). trim (), 187"42" 188); 189 190let allowed =run ( 191& dir, 192& source, 193& [ format! ( "--allow-ffi={}" , library. display ())], 194); 195assert! ( 196 allowed. status . success (), 197"{}" , 198String :: from_utf8_lossy ( & allowed. stderr ) 199); 200assert_eq! ( String :: from_utf8_lossy ( & allowed. stdout ). trim (), "42" ); 201 202let 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); 206let output =run ( 207& dir, 208& source, 209& [ format! ( "--allow-ffi={}" , library. display ())], 210); 211assert! ( 212 output. status . success (), 213"{}" , 214String :: from_utf8_lossy ( & output. stderr ) 215); 216assert_eq! ( String :: from_utf8_lossy ( & output. stdout ). trim (), "denied" ); 217} 218 219# [ test ] 220fn ffi_rejects_bad_calls () { 221let ( dir, library) =fixture (); 222let source =script ( 223& library, 224r#" 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); 242let output =run ( & dir, & source, & []); 243assert! ( 244 output. status . success (), 245"{}" , 246String :: from_utf8_lossy ( & output. stderr ) 247); 248assert_eq! ( 249String :: from_utf8_lossy ( & output. stdout ). trim (), 250"range arity string detach forge getter" 251); 252 253let missing =format! ( 254"import {{ dlopen }} from \"avril:ffi\";\ndlopen({}, {{ nope: {{ parameters: [], result: \"void\" }} }});" , 255 serde_json:: to_string ( & library. to_string_lossy ()). unwrap () 256); 257let output =run ( & dir, & missing, & []); 258assert! ( !output. status . success ()); 259} 260 261# [ test ] 262fn definitions_keep_getter_order_and_pointer_objects_keep_their_shape () { 263let ( dir, library) =fixture (); 264let source =format! ( 265"const libraryPath = {};\n" , 266 serde_json:: to_string ( & library. to_string_lossy ()). unwrap () 267) +r#" 268import { dlopen, Pointer, UnsafePointer, UnsafePointerView, UnsafeFnPointer, DynamicLibrary } from "avril:ffi"; 269import * as ffi from "avril:ffi"; 270if (Object.keys(ffi).sort().join() !== "DynamicLibrary,Pointer,UnsafeFnPointer,UnsafePointer,UnsafePointerView,dlopen") { 271throw new Error("public namespace"); 272} 273for (const name of ["Pointer", "UnsafePointerView", "UnsafeFnPointer", "DynamicLibrary"]) { 274const constructor = ffi[name]; 275if (constructor.name !== name || constructor.prototype.constructor !== constructor 276|| Object.hasOwn(constructor, Symbol.hasInstance) 277|| !(Object.create(constructor.prototype) instanceof constructor)) { 278throw new Error(`constructor shape: ${name}`); 279} 280} 281const order = []; 282let iterations = 0; 283const parameters = ["i32", "i32"]; 284parameters[Symbol.iterator] = function* () { 285if (this !== parameters) throw new Error("parameter iterator receiver"); 286iterations++; 287yield "i32"; yield "i32"; 288}; 289const definitions = { 290get add() { 291order.push("symbol"); 292return { 293get parameters() { order.push("parameters"); return parameters; }, 294get result() { order.push("result"); return "i32"; }, 295get nonblocking() { order.push("nonblocking"); return false; }, 296get name() { order.push("name"); return "add_i32"; }, 297}; 298}, 299}; 300const library = dlopen({ 301[Symbol.toPrimitive](hint) { 302if (hint !== "string") throw new Error("path hint"); 303order.push("path"); return libraryPath; 304}, 305}, definitions); 306if (order.join() !== "symbol,parameters,result,nonblocking,name,path" 307|| iterations !== 2 || library.symbols.add(20, 22) !== 42) { 308throw new Error("definition conversion"); 309} 310const pointer = new Pointer("0"); 311if (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)) { 315throw new Error("resource shape"); 316} 317if (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) { 320throw new Error("reflected arity"); 321} 322class DerivedPointer extends Pointer {} 323const derived = new DerivedPointer(0n); 324const proxiedConstructor = new Proxy(Pointer, {}); 325if (!(derived instanceof Pointer) || !(derived instanceof DerivedPointer) 326|| !(derived instanceof proxiedConstructor) || UnsafePointer.value(derived) !== 0n 327|| !(new proxiedConstructor(0n) instanceof Pointer) || !(library instanceof DynamicLibrary)) { 328throw new Error("constructor identity"); 329} 330const fn = new UnsafeFnPointer(pointer, { parameters: [], result: "void" }); 331if (!Object.isFrozen(fn) || fn.address !== 0n || Object.keys(fn).join() !== "address" 332|| !(fn instanceof UnsafeFnPointer) || !(new UnsafePointerView(pointer) instanceof UnsafePointerView)) { 333throw new Error("pointer instance shape"); 334} 335let forgedRejected = false; 336try { UnsafePointer.value(Object.create(Pointer.prototype)); } 337catch (error) { forgedRejected = error instanceof TypeError; } 338if (!forgedRejected) throw new Error("forged pointer accepted"); 339if (library.close() !== undefined || library.close() !== undefined || !library.closed) { 340throw new Error("library close"); 341} 342console.log("preserved"); 343"# ; 344let output =run ( & dir, & source, & []); 345assert! ( 346 output. status . success (), 347"{}" , 348String :: from_utf8_lossy ( & output. stderr ) 349); 350assert_eq! ( output. stdout , b"preserved\n" ); 351}