Remake typings for FFI dlopen/linkSymbols + introduce Pointer type (#2227)

* Give dlopen & linkSymbols typings for exported functions

* Fix lookup table

* Fully change over to Pointer + fix examples

* add back header for typings

* Fix tsc errors

* Run formatter on ffi.d.ts

* Revert args/return type change

* Add type tests for ffi

---------

Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
This commit is contained in:
u9g
2023-03-02 15:24:43 -05:00
committed by GitHub
parent c81043bf5e
commit 57fcf8f9ba
4 changed files with 222 additions and 28 deletions

Binary file not shown.

View File

@@ -345,6 +345,74 @@ declare module "bun:ffi" {
*/
u64_fast = 16,
}
type UNTYPED = never;
export type Pointer = number & {};
interface FFITypeToType {
[FFIType.char]: number;
[FFIType.int8_t]: number;
[FFIType.i8]: number;
[FFIType.uint8_t]: number;
[FFIType.u8]: number;
[FFIType.int16_t]: number;
[FFIType.i16]: number;
[FFIType.uint16_t]: number;
[FFIType.u16]: number;
[FFIType.int32_t]: number;
[FFIType.i32]: number;
[FFIType.int]: number;
[FFIType.uint32_t]: number;
[FFIType.u32]: number;
[FFIType.int64_t]: UNTYPED;
[FFIType.i64]: UNTYPED;
[FFIType.uint64_t]: UNTYPED;
[FFIType.u64]: UNTYPED;
[FFIType.double]: UNTYPED;
[FFIType.f64]: UNTYPED;
[FFIType.float]: UNTYPED;
[FFIType.f32]: UNTYPED;
[FFIType.bool]: boolean;
[FFIType.ptr]: Pointer;
[FFIType.pointer]: Pointer;
[FFIType.void]: UNTYPED;
[FFIType.cstring]: CString;
[FFIType.i64_fast]: number | bigint;
[FFIType.u64_fast]: number | bigint;
}
interface FFITypeStringToType {
["char"]: FFIType.char;
["int8_t"]: FFIType.int8_t;
["i8"]: FFIType.i8;
["uint8_t"]: FFIType.uint8_t;
["u8"]: FFIType.u8;
["int16_t"]: FFIType.int16_t;
["i16"]: FFIType.i16;
["uint16_t"]: FFIType.uint16_t;
["u16"]: FFIType.u16;
["int32_t"]: FFIType.int32_t;
["i32"]: FFIType.i32;
["int"]: FFIType.int;
["uint32_t"]: FFIType.uint32_t;
["u32"]: FFIType.u32;
["int64_t"]: FFIType.int64_t;
["i64"]: FFIType.i64;
["uint64_t"]: FFIType.uint64_t;
["u64"]: FFIType.u64;
["double"]: FFIType.double;
["f64"]: FFIType.f64;
["float"]: FFIType.float;
["f32"]: FFIType.f32;
["bool"]: FFIType.bool;
["ptr"]: FFIType.ptr;
["pointer"]: FFIType.pointer;
["void"]: FFIType.void;
["cstring"]: FFIType.cstring;
["function"]: FFIType.pointer; // for now
["usize"]: FFIType.uint64_t; // for now
["callback"]: FFIType.pointer; // for now
}
export type FFITypeOrString =
| FFIType
| "char"
@@ -388,12 +456,16 @@ declare module "bun:ffi" {
*
* @example
* From JavaScript:
* ```js
* const lib = dlopen('add', {
* // FFIType can be used or you can pass string labels.
* args: [FFIType.i32, "i32"],
* returns: "i32",
* });
* ```ts
* import { dlopen, FFIType, suffix } from "bun:ffi"
*
* const lib = dlopen(`adder.${suffix}`, {
* add: {
* // FFIType can be used or you can pass string labels.
* args: [FFIType.i32, "i32"],
* returns: "i32",
* },
* })
* lib.symbols.add(1, 2)
* ```
* In C:
@@ -413,7 +485,9 @@ declare module "bun:ffi" {
*
* @example
* From JavaScript:
* ```js
* ```ts
* import { dlopen, CString } from "bun:ffi"
*
* const lib = dlopen('z', {
* version: {
* returns: "ptr",
@@ -470,16 +544,8 @@ declare module "bun:ffi" {
// */
// export function callback(ffi: FFIFunction, cb: Function): number;
export interface Library {
symbols: Record<
string,
CallableFunction & {
/**
* The function without a wrapper
*/
native: CallableFunction;
}
>;
export interface Library<Fns extends Record<string, Narrow<FFIFunction>>> {
symbols: ConvertFns<Fns>;
/**
* `dlclose` the library, unloading the symbols and freeing allocated memory.
@@ -491,6 +557,32 @@ declare module "bun:ffi" {
close(): void;
}
type ToFFIType<T extends FFITypeOrString> = T extends FFIType
? T
: T extends string
? FFITypeStringToType[T]
: never;
type _Narrow<T, U> = [U] extends [T] ? U : Extract<T, U>;
type Narrow<T = unknown> =
| _Narrow<T, 0 | (number & {})>
| _Narrow<T, 0n | (bigint & {})>
| _Narrow<T, "" | (string & {})>
| _Narrow<T, boolean>
| _Narrow<T, symbol>
| _Narrow<T, []>
| _Narrow<T, { [_: PropertyKey]: Narrow }>
| (T extends object ? { [K in keyof T]: Narrow<T[K]> } : never)
| Extract<{} | null | undefined, T>;
type ConvertFns<Fns extends Record<string, FFIFunction>> = {
[K in keyof Fns]: (
...args: Fns[K]["args"] extends infer A extends FFITypeOrString[]
? { [L in keyof A]: FFITypeToType[ToFFIType<A[L]>] }
: never
) => FFITypeToType[ToFFIType<NonNullable<Fns[K]["returns"]>>];
};
/**
* Open a library using `"bun:ffi"`
*
@@ -518,7 +610,10 @@ declare module "bun:ffi" {
* goes to Fabrice Bellard and TinyCC maintainers for making this possible.
*
*/
export function dlopen(name: string, symbols: Symbols): Library;
export function dlopen<Fns extends Record<string, Narrow<FFIFunction>>>(
name: string,
symbols: Fns,
): Library<Fns>;
/**
* Turn a native library's function pointer into a JavaScript function
@@ -548,7 +643,7 @@ declare module "bun:ffi" {
*
*/
export function CFunction(
fn: FFIFunction & { ptr: number | bigint },
fn: FFIFunction & { ptr: Pointer },
): CallableFunction & {
/**
* Free the memory allocated by the wrapping function
@@ -608,7 +703,9 @@ declare module "bun:ffi" {
* goes to Fabrice Bellard and TinyCC maintainers for making this possible.
*
*/
export function linkSymbols(symbols: Symbols): Library;
export function linkSymbols<Fns extends Record<string, Narrow<FFIFunction>>>(
symbols: Fns,
): Library<Fns>;
/**
* Read a pointer as a {@link Buffer}
@@ -626,7 +723,7 @@ declare module "bun:ffi" {
*
*/
export function toBuffer(
ptr: number,
ptr: Pointer,
byteOffset?: number,
byteLength?: number,
): Buffer;
@@ -646,7 +743,7 @@ declare module "bun:ffi" {
* undefined behavior. Use with care!
*/
export function toArrayBuffer(
ptr: number,
ptr: Pointer,
byteOffset?: number,
byteLength?: number,
): ArrayBuffer;
@@ -681,7 +778,7 @@ declare module "bun:ffi" {
export function ptr(
view: TypedArray | ArrayBufferLike | DataView,
byteOffset?: number,
): number;
): Pointer;
/**
* Get a string from a UTF-8 encoded C string
@@ -734,7 +831,7 @@ declare module "bun:ffi" {
* reading beyond the bounds of the pointer will crash the program or cause
* undefined behavior. Use with care!
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number);
constructor(ptr: Pointer, byteOffset?: number, byteLength?: number);
/**
* The ptr to the C string
@@ -743,7 +840,7 @@ declare module "bun:ffi" {
* is safe to continue using this instance after the `ptr` has been
* freed.
*/
ptr: number;
ptr: Pointer;
byteOffset?: number;
byteLength?: number;
@@ -772,7 +869,7 @@ declare module "bun:ffi" {
*
* Becomes `null` once {@link JSCallback.prototype.close} is called
*/
readonly ptr: number | null;
readonly ptr: Pointer | null;
/**
* Can the callback be called from a different thread?

View File

@@ -10,8 +10,9 @@
"fmt": "prettier --write './**/*.{ts,tsx,js,jsx}'"
},
"devDependencies": {
"tsd": "^0.22.0",
"prettier": "^2.4.1"
"conditional-type-checks": "^1.0.6",
"prettier": "^2.4.1",
"tsd": "^0.22.0"
},
"tsd": {
"directory": "tests"

View File

@@ -0,0 +1,96 @@
import { dlopen, FFIType, suffix, CString, Pointer } from "bun:ffi";
import * as tsd from "tsd";
import * as tc from "conditional-type-checks";
// `suffix` is either "dylib", "so", or "dll" depending on the platform
// you don't have to use "suffix", it's just there for convenience
const path = `libsqlite3.${suffix}`;
const lib = dlopen(
path, // a library name or file path
{
sqlite3_libversion: {
// no arguments, returns a string
args: [],
returns: FFIType.cstring,
},
add: {
args: [FFIType.i32, FFIType.i32],
returns: FFIType.i32,
},
allArgs: {
args: [
FFIType.char, // string
FFIType.int8_t,
FFIType.i8,
FFIType.uint8_t,
FFIType.u8,
FFIType.int16_t,
FFIType.i16,
FFIType.uint16_t,
FFIType.u16,
FFIType.int32_t,
FFIType.i32,
FFIType.int,
FFIType.uint32_t,
FFIType.u32,
FFIType.int64_t,
FFIType.i64,
FFIType.uint64_t,
FFIType.u64,
FFIType.double,
FFIType.f64,
FFIType.float,
FFIType.f32,
FFIType.bool,
FFIType.ptr,
FFIType.pointer,
FFIType.void,
FFIType.cstring,
FFIType.i64_fast,
FFIType.u64_fast,
],
returns: FFIType.void,
},
},
);
tsd.expectType<CString>(lib.symbols.sqlite3_libversion());
tsd.expectType<number>(lib.symbols.add(1, 2));
tc.assert<
tc.IsExact<
typeof lib["symbols"]["allArgs"],
[
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
never,
never,
never,
never,
never,
never,
never,
never,
boolean,
Pointer,
Pointer,
never,
CString,
number | bigint,
number | bigint,
]
>
>;