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
15.7 KiB487 linesraw
1import { ArrayPrototypeMap } from "ext:core/primordials";
2import {
3  op_ffi_address_of,
4  op_ffi_await,
5  op_ffi_call,
6  op_ffi_call_async_begin,
7  op_ffi_call_async_finish,
8  op_ffi_call_ptr,
9  op_ffi_close,
10  op_ffi_dlopen,
11  op_ffi_read,
12  op_ffi_read_bytes,
13  op_ffi_read_cstring,
14  op_ffi_write,
15  op_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 {
28  parameters: readonly NativeType[];
29  result: NativeType;
30  /** Run on a native thread and return a Promise; buffer arguments are copied in and back. */
31  nonblocking?: boolean;
32  /** Symbol name in the library, if different from the property key. */
33  name?: 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 [...infer K]
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 {
83  readonly address: Pointer;
84  getBool(offset?: number): boolean;
85  getInt8(offset?: number): number;
86  getUint8(offset?: number): number;
87  getInt16(offset?: number): number;
88  getUint16(offset?: number): number;
89  getInt32(offset?: number): number;
90  getUint32(offset?: number): number;
91  getBigInt64(offset?: number): bigint;
92  getBigUint64(offset?: number): bigint;
93  getFloat32(offset?: number): number;
94  getFloat64(offset?: number): number;
95  getPointer(offset?: number): Pointer | null;
96  setBool(value: boolean, offset?: number): void;
97  setInt8(value: number, offset?: number): void;
98  setUint8(value: number, offset?: number): void;
99  setInt16(value: number, offset?: number): void;
100  setUint16(value: number, offset?: number): void;
101  setInt32(value: number, offset?: number): void;
102  setUint32(value: number, offset?: number): void;
103  setBigInt64(value: bigint | number, offset?: number): void;
104  setBigUint64(value: bigint | number, offset?: number): void;
105  setFloat32(value: number, offset?: number): void;
106  setFloat64(value: number, offset?: number): void;
107  setPointer(value: Pointer | UnsafeFnPointer | bigint | null, offset?: number): void;
108  getArrayBuffer(byteLength: number, offset?: number): ArrayBuffer;
109  getCString(offset?: number): string;
110  copyInto(destination: ArrayBuffer | ArrayBufferView, offset?: number): void;
111  writeFrom(source: ArrayBuffer | ArrayBufferView, offset?: number): void;
112}
113
114export interface UnsafeFnPointer<F extends ForeignFunction = ForeignFunction> {
115  call(...args: CallArgs<F["parameters"]>): JsResult<F["result"]>;
116}
117
118export interface DynamicLibrary<S extends Record<string, ForeignFunction>> {
119  readonly symbols: { readonly [K in keyof S]: Callable<S[K]> };
120  readonly closed: boolean;
121  close(): 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 {
148  if (def === null || typeof def !== "object" || Array.isArray(def)) {
149    throw new TypeError(`Symbol ${key} must be a definition object`);
150  }
151  const { parameters, result, nonblocking = false, name } = def as {
152    parameters: unknown;
153    result: unknown;
154    nonblocking?: unknown;
155    name?: unknown;
156  };
157  if (!Array.isArray(parameters)) {
158    throw new TypeError(`Symbol ${key} must have a parameters array`);
159  }
160  for (const type of parameters as unknown[]) {
161    if (!TYPES.has(type)) throw new TypeError(`Unknown native type: ${type}`);
162    if (type === "void") throw new TypeError("void is not a parameter type");
163  }
164  if (!TYPES.has(result)) throw new TypeError(`Unknown native type: ${result}`);
165  if (result === "buffer") throw new TypeError("buffer is not a result type");
166  if (typeof nonblocking !== "boolean") {
167    throw new TypeError("nonblocking must be a boolean");
168  }
169  return {
170    symbol: name ?? key,
171    params: [...parameters] as NativeDef["params"],
172    result: result as NativeDef["result"],
173    nonblocking,
174  };
175}
176
177const addresses = new WeakMap<object, bigint>();
178
179const PointerImplementation = class Pointer {
180  declare private readonly pointerBrand: void;
181  declare static readonly [Symbol.hasInstance]: (value: unknown) => value is Pointer;
182  constructor(address: bigint | number) {
183    addresses.set(this, BigInt(address));
184    Object.freeze(this);
185  }
186};
187
188export const Pointer: {
189  prototype: Pointer;
190  [Symbol.hasInstance](value: unknown): value is Pointer;
191} = PointerImplementation;
192
193function addressOf(pointer: Pointer): bigint {
194  const address = addresses.get(pointer);
195  if (address === undefined) throw new TypeError("Expected a Pointer");
196  return address;
197}
198
199function unwrap(value: unknown): unknown {
200  if (value instanceof PointerImplementation) return addressOf(value);
201  if (value instanceof UnsafeFnPointerImplementation) return value.address;
202  return value;
203}
204
205export const UnsafePointer: Readonly<{
206  /** The pointer does not keep the view alive; it must not outlive it. */
207  of(view: ArrayBuffer | ArrayBufferView): Pointer;
208  value(pointer: Pointer): bigint;
209  offset(pointer: Pointer, amount: number | bigint): Pointer;
210  create(length: number): [Pointer, Uint8Array];
211  equals(a: Pointer, b: Pointer): boolean;
212}> = Object.freeze({
213  of(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.
216    return new PointerImplementation(op_ffi_address_of(view));
217  },
218  value(pointer: Pointer): bigint {
219    return addressOf(pointer);
220  },
221  offset(pointer: Pointer, amount: number | bigint): Pointer {
222    return new PointerImplementation(addressOf(pointer) + BigInt(amount));
223  },
224  create(length: number): [Pointer, Uint8Array<ArrayBuffer>] {
225    const view = new Uint8Array(length);
226    return [new PointerImplementation(op_ffi_address_of(view)), view];
227  },
228  equals(a: Pointer, b: Pointer): boolean {
229    return addressOf(a) === addressOf(b);
230  },
231});
232
233export const UnsafePointerView: {
234  prototype: UnsafePointerView;
235  new (pointer: Pointer): UnsafePointerView;
236} = class UnsafePointerView {
237  #address: bigint;
238  constructor(pointer: Pointer | bigint | number) {
239    this.#address = pointer instanceof Pointer
240      ? addressOf(pointer)
241      : BigInt(pointer as bigint | number);
242  }
243  get address(): Pointer {
244    return new PointerImplementation(this.#address);
245  }
246  getBool(offset = 0) {
247    return op_ffi_read(this.#address + BigInt(offset), "bool");
248  }
249  getInt8(offset = 0) {
250    return op_ffi_read(this.#address + BigInt(offset), "i8");
251  }
252  getUint8(offset = 0) {
253    return op_ffi_read(this.#address + BigInt(offset), "u8");
254  }
255  getInt16(offset = 0) {
256    return op_ffi_read(this.#address + BigInt(offset), "i16");
257  }
258  getUint16(offset = 0) {
259    return op_ffi_read(this.#address + BigInt(offset), "u16");
260  }
261  getInt32(offset = 0) {
262    return op_ffi_read(this.#address + BigInt(offset), "i32");
263  }
264  getUint32(offset = 0) {
265    return op_ffi_read(this.#address + BigInt(offset), "u32");
266  }
267  getBigInt64(offset = 0) {
268    return op_ffi_read(this.#address + BigInt(offset), "i64");
269  }
270  getBigUint64(offset = 0) {
271    return op_ffi_read(this.#address + BigInt(offset), "u64");
272  }
273  getFloat32(offset = 0) {
274    return op_ffi_read(this.#address + BigInt(offset), "f32");
275  }
276  getFloat64(offset = 0) {
277    return op_ffi_read(this.#address + BigInt(offset), "f64");
278  }
279  getPointer(offset = 0) {
280    const value = op_ffi_read(this.#address + BigInt(offset), "pointer");
281    return value === 0n ? null : new PointerImplementation(value);
282  }
283  setBool(value: boolean, offset = 0) {
284    op_ffi_write(this.#address + BigInt(offset), "bool", value);
285  }
286  setInt8(value: number, offset = 0) {
287    op_ffi_write(this.#address + BigInt(offset), "i8", value);
288  }
289  setUint8(value: number, offset = 0) {
290    op_ffi_write(this.#address + BigInt(offset), "u8", value);
291  }
292  setInt16(value: number, offset = 0) {
293    op_ffi_write(this.#address + BigInt(offset), "i16", value);
294  }
295  setUint16(value: number, offset = 0) {
296    op_ffi_write(this.#address + BigInt(offset), "u16", value);
297  }
298  setInt32(value: number, offset = 0) {
299    op_ffi_write(this.#address + BigInt(offset), "i32", value);
300  }
301  setUint32(value: number, offset = 0) {
302    op_ffi_write(this.#address + BigInt(offset), "u32", value);
303  }
304  setBigInt64(value: bigint | number, offset = 0) {
305    op_ffi_write(this.#address + BigInt(offset), "i64", value);
306  }
307  setBigUint64(value: bigint | number, offset = 0) {
308    op_ffi_write(this.#address + BigInt(offset), "u64", value);
309  }
310  setFloat32(value: number, offset = 0) {
311    op_ffi_write(this.#address + BigInt(offset), "f32", value);
312  }
313  setFloat64(value: number, offset = 0) {
314    op_ffi_write(this.#address + BigInt(offset), "f64", value);
315  }
316  setPointer(
317    value: Pointer | UnsafeFnPointer | bigint | null,
318    offset = 0,
319  ) {
320    op_ffi_write(
321      this.#address + BigInt(offset),
322      "pointer",
323      unwrap(value) ?? 0n,
324    );
325  }
326  getArrayBuffer(byteLength: number, offset = 0): ArrayBuffer {
327    const bytes = op_ffi_read_bytes(this.#address + BigInt(offset), byteLength);
328    return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + byteLength);
329  }
330  getCString(offset = 0): string {
331    const bytes = op_ffi_read_cstring(this.#address + BigInt(offset), 1048576);
332    return new TextDecoder().decode(bytes);
333  }
334  copyInto(destination: ArrayBuffer | ArrayBufferView, offset = 0): void {
335    const bytes = op_ffi_read_bytes(
336      this.#address + BigInt(offset),
337      destination.byteLength,
338    );
339    new Uint8Array(
340      ((destination as ArrayBufferView).buffer ??
341        destination) as ArrayBufferLike,
342      (destination as ArrayBufferView).byteOffset ?? 0,
343      destination.byteLength,
344    ).set(bytes);
345  }
346  writeFrom(source: ArrayBuffer | ArrayBufferView, offset = 0): void {
347    op_ffi_write_bytes(this.#address + BigInt(offset), source);
348  }
349};
350
351const UnsafeFnPointerImplementation = class UnsafeFnPointer<F extends ForeignFunction = ForeignFunction> {
352  #def: NativeDef;
353  readonly address: bigint;
354  constructor(pointer: Pointer | bigint | number, def: F) {
355    if (def?.nonblocking) {
356      throw new TypeError("Function pointers cannot be called nonblocking");
357    }
358    const normalized = normalizeDef("function pointer", {
359      parameters: def.parameters,
360      result: def.result,
361      nonblocking: false,
362    });
363    this.#def = normalized;
364    this.address = pointer instanceof Pointer
365      ? addressOf(pointer)
366      : BigInt(pointer as bigint | number);
367    Object.freeze(this);
368  }
369  call(...args: CallArgs<F["parameters"]>): JsResult<F["result"]> {
370    if (this.address === 0n) throw new TypeError("Null function pointer");
371    return wrapResult(
372      this.#def.result,
373      op_ffi_call_ptr(this.address, this.#def, ArrayPrototypeMap(args, unwrap)),
374    ) as JsResult<F["result"]>;
375  }
376};
377
378export const UnsafeFnPointer: {
379  prototype: UnsafeFnPointer;
380  new <F extends ForeignFunction>(pointer: Pointer, def: F): UnsafeFnPointer<F>;
381} = UnsafeFnPointerImplementation;
382
383function wrapResult(
384  result: NativeDef["result"],
385  value: NativeValue,
386): WrappedValue {
387  if (result === "pointer" || result === "function") {
388    return value === 0n ? null : new PointerImplementation(value as bigint);
389  }
390  return value;
391}
392
393const libraryIds = new WeakMap<object, number>();
394const libraryToken = Symbol("library");
395
396const DynamicLibraryImplementation = class DynamicLibrary<
397  S extends Record<string, ForeignFunction> = Record<string, ForeignFunction>,
398> {
399  readonly symbols: { readonly [K in keyof S]: Callable<S[K]> };
400  #closed = false;
401  declare static readonly [Symbol.hasInstance]: (value: unknown) => value is DynamicLibrary;
402  constructor(
403    token: typeof libraryToken,
404    id: number,
405    symbols: { [K in keyof S]: Callable<S[K]> },
406  ) {
407    if (token !== libraryToken) {
408      throw new TypeError("DynamicLibrary cannot be constructed directly");
409    }
410    libraryIds.set(this, id);
411    this.symbols = Object.freeze({ ...symbols });
412    Object.freeze(this);
413  }
414  close(): void {
415    if (this.#closed) return;
416    this.#closed = true;
417    op_ffi_close(libraryIds.get(this)!);
418  }
419  get closed(): boolean {
420    return this.#closed;
421  }
422  [Symbol.dispose](): void {
423    this.close();
424  }
425};
426
427export const DynamicLibrary: {
428  prototype: DynamicLibrary<Record<string, ForeignFunction>>;
429  [Symbol.hasInstance](value: unknown): value is DynamicLibrary<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>>(
439  path: string | URL,
440  symbols: S,
441): DynamicLibrary<S> {
442  if (symbols === null || typeof symbols !== "object") {
443    throw new TypeError("dlopen requires a symbols object");
444  }
445  const keys = Object.keys(symbols);
446  const defs = ArrayPrototypeMap(
447    keys,
448    (key) => normalizeDef(key, symbols[key]),
449  );
450  const id = op_ffi_dlopen(
451    `${path instanceof URL ? path : String(path)}`,
452    defs,
453  );
454  const wrapped: Record<
455    string,
456    (...args: unknown[]) => WrappedValue | Promise<WrappedValue>
457  > = {};
458  for (let index = 0; index < keys.length; index++) {
459    const key = keys[index];
460    const def = defs[index];
461    wrapped[key] = (...args: unknown[]) => {
462      if (args.length !== def.params.length) {
463        throw new TypeError(
464          `${key} expects ${def.params.length} arguments, got ${args.length}`,
465        );
466      }
467      const unwrapped = ArrayPrototypeMap(args, unwrap);
468      if (def.nonblocking) {
469        return (async () => {
470          const callId = op_ffi_call_async_begin(id, index, unwrapped);
471          await op_ffi_await(callId);
472          return wrapResult(
473            def.result,
474            op_ffi_call_async_finish(callId, def.result),
475          );
476        })();
477      }
478      return wrapResult(def.result, op_ffi_call(id, index, unwrapped));
479    };
480  }
481  // Dynamic key iteration loses the relation between each definition and its wrapper.
482  return new DynamicLibraryImplementation(
483    libraryToken,
484    id,
485    wrapped as unknown as { [K in keyof S]: Callable<S[K]> },
486  );
487}