char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f68be3e
main
1import { ArrayPrototypeMap } from "ext:core/primordials" ; 2import { 3op_ffi_address_of , 4op_ffi_await , 5op_ffi_call , 6op_ffi_call_async_begin , 7op_ffi_call_async_finish , 8op_ffi_call_ptr , 9op_ffi_close , 10op_ffi_dlopen , 11op_ffi_read , 12op_ffi_read_bytes , 13op_ffi_read_cstring , 14op_ffi_write , 15op_ffi_write_bytes , 16} from "ext:core/ops" ; 17 18import type { NativeDef , NativeValue } from "./types.js" ; 19 20export type NativeType = 21| "void" 22| "bool" 23| "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" 24| "isize" | "usize" | "f32" | "f64" 25| "pointer" | "buffer" | "function" ; 26 27export interface ForeignFunction { 28parameters :readonly NativeType []; 29result :NativeType ; 30/** Run on a native thread and return a Promise; buffer arguments are copied in and back. */ 31nonblocking ?:boolean ; 32/** Symbol name in the library, if different from the property key. */ 33name ?:string ; 34} 35 36/** 37* The JS-side argument for a native parameter. Pointer and function 38* arguments accept any view (not just `Uint8Array`) and may be `undefined`. 39*/ 40export type JsArg < T extends NativeType > = T extends "i64" | "u64" | "isize" | "usize" 41 ?bigint | number 42 :T extends "pointer" | "function" ? 43| Pointer 44| UnsafeFnPointer 45| ArrayBufferView 46| ArrayBuffer 47| bigint 48| null 49| undefined 50 :T extends "buffer" ?ArrayBuffer | ArrayBufferView 51 :T extends "bool" ?boolean 52 :number ; 53 54export type JsResult < T extends NativeType > = T extends "void" ?undefined 55 :T extends "i64" | "u64" | "isize" | "usize" ?bigint 56 :T extends "pointer" | "function" ?Pointer | null 57 :T extends "bool" ?boolean 58 :number ; 59 60export type CallArgs < P extends readonly NativeType [] > = P extends readonly [ ...inferK ] 61 ?{ [ I in keyof K ] :K [ I ] extends NativeType ?JsArg < K [ I ]> :never } 62 :JsArg < P [ number ]>[]; 63 64/** 65* A bound symbol. A `boolean`-typed `nonblocking` flag resolves to a 66* sync-or-async result. 67*/ 68export type Callable < F extends ForeignFunction > = ( 69 ...args :CallArgs < F [ "parameters" ]> 70) => F extends { nonblocking :true } ?Promise < JsResult < F [ "result" ]>> 71 :"nonblocking" extends keyof F 72 ?true extends F [ "nonblocking" ] 73 ?JsResult < F [ "result" ]> | Promise < JsResult < F [ "result" ]>> 74 :JsResult < F [ "result" ]> 75 :JsResult < F [ "result" ]>; 76 77// A structurally empty instance type, exactly like the deleted declaration's 78// `class Pointer { private constructor(); }`. Only pointers created by this 79// module satisfy it at runtime; anything else throws on use. 80export interface Pointer {} 81 82export interface UnsafePointerView { 83readonly address :Pointer ; 84getBool ( offset ?:number ) :boolean ; 85getInt8 ( offset ?:number ) :number ; 86getUint8 ( offset ?:number ) :number ; 87getInt16 ( offset ?:number ) :number ; 88getUint16 ( offset ?:number ) :number ; 89getInt32 ( offset ?:number ) :number ; 90getUint32 ( offset ?:number ) :number ; 91getBigInt64 ( offset ?:number ) :bigint ; 92getBigUint64 ( offset ?:number ) :bigint ; 93getFloat32 ( offset ?:number ) :number ; 94getFloat64 ( offset ?:number ) :number ; 95getPointer ( offset ?:number ) :Pointer | null ; 96setBool ( value :boolean , offset ?:number ) :void ; 97setInt8 ( value :number , offset ?:number ) :void ; 98setUint8 ( value :number , offset ?:number ) :void ; 99setInt16 ( value :number , offset ?:number ) :void ; 100setUint16 ( value :number , offset ?:number ) :void ; 101setInt32 ( value :number , offset ?:number ) :void ; 102setUint32 ( value :number , offset ?:number ) :void ; 103setBigInt64 ( value :bigint | number , offset ?:number ) :void ; 104setBigUint64 ( value :bigint | number , offset ?:number ) :void ; 105setFloat32 ( value :number , offset ?:number ) :void ; 106setFloat64 ( value :number , offset ?:number ) :void ; 107setPointer ( value :Pointer | UnsafeFnPointer | bigint | null , offset ?:number ) :void ; 108getArrayBuffer ( byteLength :number , offset ?:number ) :ArrayBuffer ; 109getCString ( offset ?:number ) :string ; 110copyInto ( destination :ArrayBuffer | ArrayBufferView , offset ?:number ) :void ; 111writeFrom ( source :ArrayBuffer | ArrayBufferView , offset ?:number ) :void ; 112} 113 114export interface UnsafeFnPointer < F extends ForeignFunction = ForeignFunction > { 115call ( ...args :CallArgs < F [ "parameters" ]>) :JsResult < F [ "result" ]>; 116} 117 118export interface DynamicLibrary < S extends Record < string , ForeignFunction > > { 119readonly symbols :{ readonly [ K in keyof S ] :Callable < S [ K ]> }; 120readonly closed :boolean ; 121close () :void ; 122[ Symbol . dispose ]() :void ; 123} 124 125type WrappedValue = Pointer | null | NativeValue ; 126 127const TYPES = new Set < unknown >([ 128"void" , 129"bool" , 130"i8" , 131"u8" , 132"i16" , 133"u16" , 134"i32" , 135"u32" , 136"i64" , 137"u64" , 138"isize" , 139"usize" , 140"f32" , 141"f64" , 142"pointer" , 143"buffer" , 144"function" , 145]); 146 147function normalizeDef ( key :string , def :unknown ) :NativeDef { 148if ( def === null || typeof def !== "object" || Array . isArray ( def )) { 149throw new TypeError ( `Symbol ${ key } must be a definition object` ); 150} 151const { parameters, result, nonblocking= false , name} = def as { 152parameters :unknown ; 153result :unknown ; 154nonblocking ?:unknown ; 155name ?:unknown ; 156}; 157if ( ! Array . isArray ( parameters )) { 158throw new TypeError ( `Symbol ${ key } must have a parameters array` ); 159} 160for ( const type of parameters as unknown []) { 161if ( ! TYPES . has ( type )) throw new TypeError ( `Unknown native type: ${ type } ` ); 162if ( type === "void" ) throw new TypeError ( "void is not a parameter type" ); 163} 164if ( ! TYPES . has ( result )) throw new TypeError ( `Unknown native type: ${ result } ` ); 165if ( result === "buffer" ) throw new TypeError ( "buffer is not a result type" ); 166if ( typeof nonblocking !== "boolean" ) { 167throw new TypeError ( "nonblocking must be a boolean" ); 168} 169return { 170symbol :name ?? key , 171params :[ ...parameters ] as NativeDef [ "params" ], 172result :result as NativeDef [ "result" ], 173 nonblocking, 174}; 175} 176 177const addresses = new WeakMap < object , bigint >(); 178 179const PointerImplementation = class Pointer { 180declare private readonly pointerBrand :void ; 181declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isPointer ; 182constructor ( address :bigint | number ) { 183addresses . set ( this , BigInt ( address )); 184Object . freeze ( this ); 185} 186}; 187 188export const Pointer :{ 189prototype :Pointer ; 190[ Symbol . hasInstance ]( value :unknown ) :value isPointer ; 191} = PointerImplementation ; 192 193function addressOf ( pointer :Pointer ) :bigint { 194const address = addresses . get ( pointer ); 195if ( address === undefined ) throw new TypeError ( "Expected a Pointer" ); 196return address ; 197} 198 199function unwrap ( value :unknown ) :unknown { 200if ( value instanceof PointerImplementation ) return addressOf ( value ); 201if ( value instanceof UnsafeFnPointerImplementation ) return value . address ; 202return value ; 203} 204 205export const UnsafePointer :Readonly <{ 206/** The pointer does not keep the view alive; it must not outlive it. */ 207of ( view :ArrayBuffer | ArrayBufferView ) :Pointer ; 208value ( pointer :Pointer ) :bigint ; 209offset ( pointer :Pointer , amount :number | bigint ) :Pointer ; 210create ( length :number ) :[ Pointer , Uint8Array ]; 211equals ( a :Pointer , b :Pointer ) :boolean ; 212}> = Object . freeze ({ 213of ( view :ArrayBuffer | ArrayBufferView ) :Pointer { 214// The resulting Pointer does not keep the view alive; it must not 215// outlive the view it was taken from. 216return new PointerImplementation ( op_ffi_address_of ( view )); 217}, 218value ( pointer :Pointer ) :bigint { 219return addressOf ( pointer ); 220}, 221offset ( pointer :Pointer , amount :number | bigint ) :Pointer { 222return new PointerImplementation ( addressOf ( pointer ) + BigInt ( amount )); 223}, 224create ( length :number ) :[ Pointer , Uint8Array < ArrayBuffer >] { 225const view = new Uint8Array ( length ); 226return [ new PointerImplementation ( op_ffi_address_of ( view )), view ]; 227}, 228equals ( a :Pointer , b :Pointer ) :boolean { 229return addressOf ( a ) === addressOf ( b ); 230}, 231}); 232 233export const UnsafePointerView :{ 234prototype :UnsafePointerView ; 235new ( pointer :Pointer ) :UnsafePointerView ; 236} = class UnsafePointerView { 237 #address:bigint ; 238constructor ( pointer :Pointer | bigint | number ) { 239this . #address= pointer instanceof Pointer 240 ?addressOf ( pointer ) 241 :BigInt ( pointer as bigint | number ); 242} 243get address () :Pointer { 244return new PointerImplementation ( this . #address); 245} 246getBool ( offset = 0 ) { 247return op_ffi_read ( this . #address+ BigInt ( offset ), "bool" ); 248} 249getInt8 ( offset = 0 ) { 250return op_ffi_read ( this . #address+ BigInt ( offset ), "i8" ); 251} 252getUint8 ( offset = 0 ) { 253return op_ffi_read ( this . #address+ BigInt ( offset ), "u8" ); 254} 255getInt16 ( offset = 0 ) { 256return op_ffi_read ( this . #address+ BigInt ( offset ), "i16" ); 257} 258getUint16 ( offset = 0 ) { 259return op_ffi_read ( this . #address+ BigInt ( offset ), "u16" ); 260} 261getInt32 ( offset = 0 ) { 262return op_ffi_read ( this . #address+ BigInt ( offset ), "i32" ); 263} 264getUint32 ( offset = 0 ) { 265return op_ffi_read ( this . #address+ BigInt ( offset ), "u32" ); 266} 267getBigInt64 ( offset = 0 ) { 268return op_ffi_read ( this . #address+ BigInt ( offset ), "i64" ); 269} 270getBigUint64 ( offset = 0 ) { 271return op_ffi_read ( this . #address+ BigInt ( offset ), "u64" ); 272} 273getFloat32 ( offset = 0 ) { 274return op_ffi_read ( this . #address+ BigInt ( offset ), "f32" ); 275} 276getFloat64 ( offset = 0 ) { 277return op_ffi_read ( this . #address+ BigInt ( offset ), "f64" ); 278} 279getPointer ( offset = 0 ) { 280const value = op_ffi_read ( this . #address+ BigInt ( offset ), "pointer" ); 281return value === 0n ?null :new PointerImplementation ( value ); 282} 283setBool ( value :boolean , offset = 0 ) { 284op_ffi_write ( this . #address+ BigInt ( offset ), "bool" , value ); 285} 286setInt8 ( value :number , offset = 0 ) { 287op_ffi_write ( this . #address+ BigInt ( offset ), "i8" , value ); 288} 289setUint8 ( value :number , offset = 0 ) { 290op_ffi_write ( this . #address+ BigInt ( offset ), "u8" , value ); 291} 292setInt16 ( value :number , offset = 0 ) { 293op_ffi_write ( this . #address+ BigInt ( offset ), "i16" , value ); 294} 295setUint16 ( value :number , offset = 0 ) { 296op_ffi_write ( this . #address+ BigInt ( offset ), "u16" , value ); 297} 298setInt32 ( value :number , offset = 0 ) { 299op_ffi_write ( this . #address+ BigInt ( offset ), "i32" , value ); 300} 301setUint32 ( value :number , offset = 0 ) { 302op_ffi_write ( this . #address+ BigInt ( offset ), "u32" , value ); 303} 304setBigInt64 ( value :bigint | number , offset = 0 ) { 305op_ffi_write ( this . #address+ BigInt ( offset ), "i64" , value ); 306} 307setBigUint64 ( value :bigint | number , offset = 0 ) { 308op_ffi_write ( this . #address+ BigInt ( offset ), "u64" , value ); 309} 310setFloat32 ( value :number , offset = 0 ) { 311op_ffi_write ( this . #address+ BigInt ( offset ), "f32" , value ); 312} 313setFloat64 ( value :number , offset = 0 ) { 314op_ffi_write ( this . #address+ BigInt ( offset ), "f64" , value ); 315} 316setPointer ( 317value :Pointer | UnsafeFnPointer | bigint | null , 318offset = 0 , 319) { 320op_ffi_write ( 321this . #address+ BigInt ( offset ), 322"pointer" , 323unwrap ( value ) ?? 0n , 324); 325} 326getArrayBuffer ( byteLength :number , offset = 0 ) :ArrayBuffer { 327const bytes = op_ffi_read_bytes ( this . #address+ BigInt ( offset ), byteLength ); 328return bytes . buffer . slice ( bytes . byteOffset , bytes . byteOffset + byteLength ); 329} 330getCString ( offset = 0 ) :string { 331const bytes = op_ffi_read_cstring ( this . #address+ BigInt ( offset ), 1048576 ); 332return new TextDecoder (). decode ( bytes ); 333} 334copyInto ( destination :ArrayBuffer | ArrayBufferView , offset = 0 ) :void { 335const bytes = op_ffi_read_bytes ( 336this . #address+ BigInt ( offset ), 337destination . byteLength , 338); 339new Uint8Array ( 340(( destination as ArrayBufferView ). buffer ?? 341destination ) as ArrayBufferLike , 342( destination as ArrayBufferView ). byteOffset ?? 0 , 343destination . byteLength , 344). set ( bytes ); 345} 346writeFrom ( source :ArrayBuffer | ArrayBufferView , offset = 0 ) :void { 347op_ffi_write_bytes ( this . #address+ BigInt ( offset ), source ); 348} 349}; 350 351const UnsafeFnPointerImplementation = class UnsafeFnPointer < F extends ForeignFunction = ForeignFunction > { 352 #def:NativeDef ; 353readonly address :bigint ; 354constructor ( pointer :Pointer | bigint | number , def :F ) { 355if ( def ?. nonblocking ) { 356throw new TypeError ( "Function pointers cannot be called nonblocking" ); 357} 358const normalized = normalizeDef ( "function pointer" , { 359parameters :def . parameters , 360result :def . result , 361nonblocking :false , 362}); 363this . #def= normalized ; 364this . address = pointer instanceof Pointer 365 ?addressOf ( pointer ) 366 :BigInt ( pointer as bigint | number ); 367Object . freeze ( this ); 368} 369call ( ...args :CallArgs < F [ "parameters" ]>) :JsResult < F [ "result" ]> { 370if ( this . address === 0n ) throw new TypeError ( "Null function pointer" ); 371return wrapResult ( 372this . #def. result , 373op_ffi_call_ptr ( this . address , this . #def, ArrayPrototypeMap ( args , unwrap )), 374) as JsResult < F [ "result" ]>; 375} 376}; 377 378export const UnsafeFnPointer :{ 379prototype :UnsafeFnPointer ; 380new < F extends ForeignFunction > ( pointer :Pointer , def :F ) :UnsafeFnPointer < F >; 381} = UnsafeFnPointerImplementation ; 382 383function wrapResult ( 384result :NativeDef [ "result" ], 385value :NativeValue , 386) :WrappedValue { 387if ( result === "pointer" || result === "function" ) { 388return value === 0n ?null :new PointerImplementation ( value as bigint ); 389} 390return value ; 391} 392 393const libraryIds = new WeakMap < object , number >(); 394const libraryToken = Symbol ( "library" ); 395 396const DynamicLibraryImplementation = class DynamicLibrary < 397S extends Record < string , ForeignFunction > = Record < string , ForeignFunction >, 398> { 399readonly symbols :{ readonly [ K in keyof S ] :Callable < S [ K ]> }; 400 #closed= false ; 401declare static readonly [ Symbol . hasInstance ] :( value :unknown ) => value isDynamicLibrary ; 402constructor ( 403token :typeof libraryToken , 404id :number , 405symbols :{ [ K in keyof S ] :Callable < S [ K ]> }, 406) { 407if ( token !== libraryToken ) { 408throw new TypeError ( "DynamicLibrary cannot be constructed directly" ); 409} 410libraryIds . set ( this , id ); 411this . symbols = Object . freeze ({ ...symbols }); 412Object . freeze ( this ); 413} 414close () :void { 415if ( this . #closed) return ; 416this . #closed= true ; 417op_ffi_close ( libraryIds . get ( this ) ! ); 418} 419get closed () :boolean { 420return this . #closed; 421} 422[ Symbol . dispose ]() :void { 423this . close (); 424} 425}; 426 427export const DynamicLibrary :{ 428prototype :DynamicLibrary < Record < string , ForeignFunction >>; 429[ Symbol . hasInstance ]( value :unknown ) :value isDynamicLibrary < Record < string , ForeignFunction >>; 430} = DynamicLibraryImplementation ; 431 432/** 433* Loads a shared library and binds its symbols. Native code runs 434* in-process and is not sandboxed; loading requires ffi authority for 435* the library path (a bare loader-searched name requires unrestricted 436* ffi authority). 437*/ 438export function dlopen < const S extends Record < string , ForeignFunction > > ( 439path :string | URL , 440symbols :S , 441) :DynamicLibrary < S > { 442if ( symbols === null || typeof symbols !== "object" ) { 443throw new TypeError ( "dlopen requires a symbols object" ); 444} 445const keys = Object . keys ( symbols ); 446const defs = ArrayPrototypeMap ( 447keys , 448( key ) => normalizeDef ( key , symbols [ key ]), 449); 450const id = op_ffi_dlopen ( 451` ${ path instanceof URL ? path : String ( path )} ` , 452defs , 453); 454const wrapped :Record < 455string , 456( ...args :unknown []) => WrappedValue | Promise < WrappedValue > 457> = {}; 458for ( let index = 0 ; index < keys . length ; index ++ ) { 459const key = keys [ index ]; 460const def = defs [ index ]; 461wrapped [ key ] = ( ...args :unknown []) => { 462if ( args . length !== def . params . length ) { 463throw new TypeError ( 464` ${ key } expects ${ def . params . length } arguments, got ${ args . length } ` , 465); 466} 467const unwrapped = ArrayPrototypeMap ( args , unwrap ); 468if ( def . nonblocking ) { 469return ( async () => { 470const callId = op_ffi_call_async_begin ( id , index , unwrapped ); 471await op_ffi_await ( callId ); 472return wrapResult ( 473def . result , 474op_ffi_call_async_finish ( callId , def . result ), 475); 476})(); 477} 478return wrapResult ( def . result , op_ffi_call ( id , index , unwrapped )); 479}; 480} 481// Dynamic key iteration loses the relation between each definition and its wrapper. 482return new DynamicLibraryImplementation ( 483libraryToken , 484id , 485wrapped as unknown as { [ K in keyof S ] :Callable < S [ K ]> }, 486); 487}