Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions docs/runtime/ffi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const [major, minor, patch] = [lib.symbols.getMajor(), lib.symbols.getMinor(), l
Use `JSCallback` to create JavaScript callback functions that can be passed to C/FFI functions. The C/FFI function can call into the JavaScript/TypeScript code. This is useful for asynchronous code or whenever you want to call into JavaScript code from C.

```ts
import { dlopen, JSCallback, ptr, CString } from "bun:ffi";
import { dlopen, JSCallback, ptr, CString, type Pointer } from "bun:ffi";

const {
symbols: { search },
Expand All @@ -290,10 +290,13 @@ const {
},
});

const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
});
const searchIterator = new JSCallback(
(ptr: Pointer | null, length: bigint) => ptr !== null && /hello/.test(new CString(ptr, 0, Number(length))),
{
returns: "bool",
args: ["ptr", "usize"],
},
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const str = Buffer.from("wwutwutwutwutwutwutwutwutwutwutut\0", "utf8");
if (search(ptr(str), searchIterator)) {
Expand All @@ -316,11 +319,14 @@ When you're done with a JSCallback, you should call `close()` to free the memory
Currently, thread-safe callbacks work best when run from another thread that is running JavaScript code, i.e. a [`Worker`](/runtime/workers). A future version of Bun will enable them to be called from any thread (such as new threads spawned by your native library that Bun is not aware of).

```ts
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true, // Optional. Defaults to `false`
});
const searchIterator = new JSCallback(
(ptr: Pointer | null, length: bigint) => ptr !== null && /hello/.test(new CString(ptr, 0, Number(length))),
{
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true, // Optional. Defaults to `false`
},
);
```

<Note>
Expand Down
148 changes: 124 additions & 24 deletions packages/bun-types/ffi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@ declare module "bun:ffi" {
[FFIType.double]: number;
[FFIType.float]: number;
[FFIType.bool]: boolean;
[FFIType.ptr]: NodeJS.TypedArray | Pointer | CString | null;
[FFIType.ptr]: NodeJS.TypedArray | DataView | ArrayBuffer | Pointer | CString | null;
[FFIType.void]: undefined;
[FFIType.cstring]: NodeJS.TypedArray | Pointer | CString | null;
[FFIType.cstring]: NodeJS.TypedArray | DataView | ArrayBuffer | Pointer | CString | null;
[FFIType.i64_fast]: number | bigint;
[FFIType.u64_fast]: number | bigint;
[FFIType.function]: Pointer | JSCallback; // cannot be null
Expand Down Expand Up @@ -394,6 +394,67 @@ declare module "bun:ffi" {
[FFIType.napi_value]: unknown;
[FFIType.buffer]: NodeJS.TypedArray | DataView;
}
/**
* Values a {@link JSCallback} receives when invoked from native code.
*
* Unlike {@link FFITypeToReturnsType}, a `cstring` argument arrives as a raw
* {@link Pointer} (or `null`), not a {@link CString}. Wrap it yourself with
* `new CString(ptr)` if you need the string contents.
*/
interface FFITypeToJSCallbackArgsType {
[FFIType.char]: number;
[FFIType.int8_t]: number;
[FFIType.uint8_t]: number;
[FFIType.int16_t]: number;
[FFIType.uint16_t]: number;
[FFIType.int32_t]: number;
[FFIType.uint32_t]: number;
[FFIType.int64_t]: bigint;
[FFIType.uint64_t]: bigint;
[FFIType.double]: number;
[FFIType.float]: number;
[FFIType.bool]: boolean;
[FFIType.ptr]: Pointer | null;
[FFIType.void]: undefined;
[FFIType.cstring]: Pointer | null;
[FFIType.i64_fast]: number | bigint;
[FFIType.u64_fast]: number | bigint;
[FFIType.function]: Pointer | null;
[FFIType.napi_env]: unknown;
[FFIType.napi_value]: unknown;
[FFIType.buffer]: NodeJS.TypedArray | DataView;
}
/**
* Values a {@link JSCallback} may return to native code.
*
* Conversion happens without the JavaScript-side coercion that calls into
* native functions get, so pointer-typed returns accept a {@link Pointer},
* a TypedArray or DataView (its backing store address is used), or `null`,
* but not a {@link CString} or {@link JSCallback}.
*/
interface FFITypeToJSCallbackReturnsType {
[FFIType.char]: number;
[FFIType.int8_t]: number;
[FFIType.uint8_t]: number;
[FFIType.int16_t]: number;
[FFIType.uint16_t]: number;
[FFIType.int32_t]: number;
[FFIType.uint32_t]: number;
[FFIType.int64_t]: number | bigint;
[FFIType.uint64_t]: number | bigint;
[FFIType.double]: number;
[FFIType.float]: number;
[FFIType.bool]: boolean;
[FFIType.ptr]: NodeJS.TypedArray | DataView | Pointer | null;
[FFIType.void]: void;
[FFIType.cstring]: NodeJS.TypedArray | DataView | Pointer | null;
[FFIType.i64_fast]: number | bigint;
[FFIType.u64_fast]: number | bigint;
[FFIType.function]: Pointer | null;
[FFIType.napi_env]: unknown;
[FFIType.napi_value]: unknown;
[FFIType.buffer]: NodeJS.TypedArray | DataView;
}
interface FFITypeStringToType {
["char"]: FFIType.char;
["int8_t"]: FFIType.int8_t;
Expand All @@ -420,11 +481,20 @@ declare module "bun:ffi" {
["bool"]: FFIType.bool;
["ptr"]: FFIType.ptr;
["pointer"]: FFIType.pointer;
["void*"]: FFIType.ptr;
["char*"]: FFIType.ptr;
["void"]: FFIType.void;
["cstring"]: FFIType.cstring;
["function"]: FFIType.pointer; // for now
["usize"]: FFIType.uint64_t; // for now
["callback"]: FFIType.pointer; // for now
["i64_fast"]: FFIType.i64_fast;
["u64_fast"]: FFIType.u64_fast;
["function"]: FFIType.function;
["callback"]: FFIType.function;
["fn"]: FFIType.function;
["usize"]: FFIType.uint64_t;
["size_t"]: FFIType.uint64_t;
["isize"]: FFIType.int64_t;
["c_int"]: FFIType.int32_t;
["c_uint"]: FFIType.uint32_t;
["napi_env"]: FFIType.napi_env;
["napi_value"]: FFIType.napi_value;
["buffer"]: FFIType.buffer;
Expand Down Expand Up @@ -538,22 +608,46 @@ declare module "bun:ffi" {
type ToFFIType<T extends FFITypeOrString> = T extends FFIType ? T : T extends string ? FFITypeStringToType[T] : never;

const FFIFunctionCallableSymbol: unique symbol;
type ConvertFn<Fn extends FFIFunction> = {
(
...args: Fn["args"] extends infer A extends readonly FFITypeOrString[]
? { [L in keyof A]: FFITypeToArgsType[ToFFIType<A[L]>] }
: // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
[unknown] extends [Fn["args"]]
? []
: never
): [unknown] extends [Fn["returns"]] // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
? undefined
: FFITypeToReturnsType[ToFFIType<NonNullable<Fn["returns"]>>];
__ffi_function_callable: typeof FFIFunctionCallableSymbol;
};
type ConvertFns<Fns extends Symbols> = {
[K in keyof Fns]: {
(
...args: Fns[K]["args"] extends infer A extends readonly FFITypeOrString[]
? { [L in keyof A]: FFITypeToArgsType[ToFFIType<A[L]>] }
: // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
[unknown] extends [Fns[K]["args"]]
? []
: never
): [unknown] extends [Fns[K]["returns"]] // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
? undefined
: FFITypeToReturnsType[ToFFIType<NonNullable<Fns[K]["returns"]>>];
__ffi_function_callable: typeof FFIFunctionCallableSymbol;
};
[K in keyof Fns]: ConvertFn<Fns[K]>;
};

/**
* The JavaScript function passed to a {@link JSCallback}.
*
* Argument and return types are derived from the `definition`:
* arguments arrive converted per {@link FFITypeToJSCallbackArgsType} and the
* return value must satisfy {@link FFITypeToJSCallbackReturnsType}.
*/
type JSCallbackFunction<Def extends FFIFunction = FFIFunction> = {
// A method signature (vs a function type) keeps parameter checking
// bivariant, so a narrower handwritten `(ptr: Pointer) => void` stays
// assignable where the derived type is `(ptr: Pointer | null) => void`.
fn(
...args: Def["args"] extends infer A extends readonly FFITypeOrString[]
? { [L in keyof A]: FFITypeToJSCallbackArgsType[ToFFIType<A[L]>] }
: // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
[unknown] extends [Def["args"]]
? []
: never
): [unknown] extends [Def["returns"]] // eslint-disable-next-line @definitelytyped/no-single-element-tuple-type
? void
: FFITypeToJSCallbackReturnsType[ToFFIType<NonNullable<Def["returns"]>>];
}["fn"];

/**
* Open a library using `"bun:ffi"`
*
Expand Down Expand Up @@ -582,7 +676,7 @@ declare module "bun:ffi" {
*
* @category FFI
*/
function dlopen<Fns extends Record<string, FFIFunction>>(
function dlopen<const Fns extends Record<string, FFIFunction>>(
name: string | import("bun").BunFile | URL,
symbols: Fns,
): Library<Fns>;
Expand Down Expand Up @@ -621,7 +715,7 @@ declare module "bun:ffi" {
* }
* ```
*/
function cc<Fns extends Record<string, FFIFunction>>(options: {
function cc<const Fns extends Record<string, FFIFunction>>(options: {
/**
* File path to an ISO C11 source file to compile and link
*/
Expand Down Expand Up @@ -723,7 +817,9 @@ declare module "bun:ffi" {
* bun uses [tinycc](https://github.com/TinyCC/tinycc), so a big thanks
* goes to Fabrice Bellard and TinyCC maintainers for making this possible.
*/
function CFunction(fn: FFIFunction & { ptr: Pointer }): CallableFunction & {
function CFunction<const Fn extends FFIFunction & { ptr: Pointer }>(
fn: Fn,
): ConvertFn<Fn> & {
/**
* Free the memory allocated by the wrapping function
*/
Expand Down Expand Up @@ -781,7 +877,7 @@ declare module "bun:ffi" {
* bun uses [tinycc](https://github.com/TinyCC/tinycc), so a big thanks
* goes to Fabrice Bellard and TinyCC maintainers for making this possible.
*/
function linkSymbols<Fns extends Record<string, FFIFunction>>(symbols: Fns): Library<Fns>;
function linkSymbols<const Fns extends Record<string, FFIFunction>>(symbols: Fns): Library<Fns>;

/**
* Read a pointer as a {@link Buffer}
Expand Down Expand Up @@ -1079,14 +1175,18 @@ declare module "bun:ffi" {
/**
* Pass a JavaScript function to FFI (Foreign Function Interface)
*/
class JSCallback {
class JSCallback<const Def extends FFIFunction = FFIFunction> {
/**
* Enable a JavaScript callback function to be passed to C with bun:ffi
*
* The callback's parameter and return types are inferred from
* `definition`, so a mismatch between the declared FFI types and the
* JavaScript function is a type error.
*
* @param callback The JavaScript function to be called
* @param definition The C function definition
*/
constructor(callback: (...args: any[]) => any, definition: FFIFunction);
constructor(callback: JSCallbackFunction<Def>, definition: Def);

/**
* The pointer to the C function
Expand Down
33 changes: 27 additions & 6 deletions src/js/bun/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const FFIType = {
"15": 15,
"16": 16,
"17": 17,
"18": 18,
"19": 19,
"20": 20,
bool: 11,
c_int: 5,
c_uint: 6,
Expand Down Expand Up @@ -45,6 +48,7 @@ const FFIType = {
uint64_t: 8,
uint8_t: 2,
usize: 8,
size_t: 8,
"void*": 12,
ptr: 12,
pointer: 12,
Expand Down Expand Up @@ -283,8 +287,10 @@ Object.defineProperty(globalThis, "__GlobalBunFFIPtrFunctionForWrapper", {
configurable: true,
});
Object.defineProperty(globalThis, "__GlobalBunFFIPtrArrayBufferViewFn", {
value: function isTypedArrayView(val) {
return $isTypedArrayView(val);
value: function isArrayBufferView(val) {
// `$isTypedArrayView` is the fast path, but it excludes DataView, which
// the compiled FFI stubs accept like any other ArrayBufferView.
return $isTypedArrayView(val) || ArrayBuffer.$isView(val);
},
enumerable: false,
configurable: true,
Expand All @@ -304,6 +310,14 @@ ffiWrappers[FFIType.cstring] = ffiWrappers[FFIType.pointer] = `{
return __GlobalBunFFIPtrFunctionForWrapper(val);
}

// A CString (or anything else carrying a numeric "ptr", like a JSCallback).
// CString data starts at ptr + byteOffset, like its arrayBuffer getter.
var valPtr = val.ptr;
if (typeof valPtr === "number") {
var valByteOffset = val.byteOffset;
return valByteOffset ? valPtr + valByteOffset : valPtr;
}

if (typeof val === "string") {
throw new TypeError("To convert a string to a pointer, encode it as a buffer");
}
Expand All @@ -313,7 +327,7 @@ ffiWrappers[FFIType.cstring] = ffiWrappers[FFIType.pointer] = `{

ffiWrappers[FFIType.buffer] = `{
if (!__GlobalBunFFIPtrArrayBufferViewFn(val)) {
throw new TypeError("Expected a TypedArray");
throw new TypeError("Expected a TypedArray or DataView");
}

return val;
Expand All @@ -337,6 +351,12 @@ ffiWrappers[FFIType.function] = `{
return ptr;
}`;

// Node-API arguments are passed through as raw JSValues: the native side reads
// `napi_value` directly and substitutes the module's env for `napi_env`, so
// neither may go through the default `val|0` coercion.
ffiWrappers[FFIType.napi_env] = "val";
ffiWrappers[FFIType.napi_value] = "val";

function FFIBuilder(params, returnType, functionToCall, name) {
const hasReturnType = typeof FFIType[returnType] === "number" && FFIType[returnType as string] !== FFIType.void;
var paramNames = new Array(params.length);
Expand Down Expand Up @@ -497,12 +517,13 @@ function cc(options) {
const result = ccFn(options);
if (Error.isError(result)) throw result;

const symbols = options.symbols;
for (let key in result.symbols) {
var symbol = result.symbols[key];
if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) {
if (symbols[key]?.args?.length || FFIType[symbols[key]?.returns as string] === FFIType.cstring) {
result.symbols[key] = FFIBuilder(
options[key].args ?? [],
options[key].returns ?? FFIType.void,
symbols[key].args ?? [],
symbols[key].returns ?? FFIType.void,
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
symbol,
// in stacktraces:
// instead of
Expand Down
Loading
Loading