diff options
author | 2022-08-09 16:16:51 -0700 | |
---|---|---|
committer | 2022-08-09 16:16:51 -0700 | |
commit | b428227895b0fb29c9d9060b7d12191428e37736 (patch) | |
tree | dc245e0e14b7bde0340a8b105da994498a022156 | |
parent | 6c8ba6c5fc53adc4ae9a9af73f982ff2aee6e196 (diff) | |
download | bun-b428227895b0fb29c9d9060b7d12191428e37736.tar.gz bun-b428227895b0fb29c9d9060b7d12191428e37736.tar.zst bun-b428227895b0fb29c9d9060b7d12191428e37736.zip |
[bun:ffi] Feature: C deallocator callback function for `toBuffer` and `toArrayBuffer`
-rw-r--r-- | README.md | 55 | ||||
-rw-r--r-- | src/bun.js/api/bun.zig | 76 |
2 files changed, 126 insertions, 5 deletions
@@ -1209,8 +1209,7 @@ rm -rf node_modules bun install --backend copyfile ``` -**`symlink`** is typically only used for `file:` dependencies (and eventually `link:`) internally. To prevent infinite loops, it skips symlinking the `node_modules` folder. - +**`symlink`** is typically only used for `file:` dependencies (and eventually `link:`) internally. To prevent infinite loops, it skips symlinking the `node_modules` folder. If you install with `--backend=symlink`, Node.js won't resolve node_modules of dependencies unless each dependency has it's own node_modules folder or you pass `--preserve-symlinks` to `node`. See [Node.js documentation on `--preserve-symlinks`](https://nodejs.org/api/cli.html#--preserve-symlinks). @@ -2832,6 +2831,55 @@ const myPtr = ptr(myTypedArray); myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32); ``` +**Memory management with pointers**: + +`bun:ffi` does not manage memory for you because it doesn't have the information necessary. You must free the memory when you're done with it. + +**From JavaScript**: + +If you want to track when a TypedArray is no longer in use from JavaScript, you can use a [FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry). + +**From FFI (C, Rust, Zig, etc)**: + +<sup>Available in Bun v0.1.8 and later.</sup> + +If you want to track when a TypedArray is no longer in use from C or FFI, you can pass a callback and an optional context pointer to `toArrayBuffer` or `toBuffer`. This function is called at some point later, once the garbage collector frees the underlying `ArrayBuffer` JavaScript object. + +The expected signature is the same as in [JavaScriptCore's C API](https://developer.apple.com/documentation/javascriptcore/jstypedarraybytesdeallocator?language=objc): + +```c +typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext); +``` + +```ts +import { toArrayBuffer } from "bun:ffi"; + +// with a deallocatorContext: +toArrayBuffer( + bytes, + byteOffset, + + byteLength, + + // this is an optional pointer to a callback + deallocatorContext, + + // this is a pointer to a function + jsTypedArrayBytesDeallocator +); + +// without a deallocatorContext: +toArrayBuffer( + bytes, + byteOffset, + + byteLength, + + // this is a pointer to a function + jsTypedArrayBytesDeallocator +); +``` + **Pointers & memory safety** Using raw pointers outside of FFI is extremely not recommended. @@ -3292,7 +3340,7 @@ To get started, in the `bun` repository, locally run: make devcontainer-build ``` -Next, open VS Code in the `bun` repository. +Next, open VS Code in the `bun` repository. To open the dev container, open the command palette (Ctrl + Shift + P) and run: `Remote-Containers: Reopen in Container`. You will then need to clone the GitHub repository inside that container. @@ -3318,7 +3366,6 @@ bun-debug It is very similar to my own development environment (except I use macOS) - ### MacOS Install LLVM 13 and homebrew dependencies: diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 32e0d04e5..d1260b14b 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -2553,18 +2553,62 @@ pub const FFI = struct { return .{ .slice = bun.span(@intToPtr([*:0]u8, addr)) }; } + fn getCPtr(value: JSValue) ?usize { + // pointer to C function + if (value.isNumber()) { + const addr = @bitCast(u64, value.asNumber()); + if (addr > 0) { + return addr; + } + // pointer to C function as a BigInt + } else if (value.isBigInt()) { + const addr = @bitCast(u64, value.toUInt64NoTruncate()); + if (addr > 0) { + return addr; + } + } + + return null; + } + pub fn toArrayBuffer( globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, valueLength: ?JSValue, + finalizationCtxOrPtr: ?JSValue, + finalizationCallback: ?JSValue, ) JSC.JSValue { switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { .err => |erro| { return erro; }, .slice => |slice| { - return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis.ref(), null, null, null); + var callback: JSC.C.JSTypedArrayBytesDeallocator = null; + var ctx: ?*anyopaque = null; + if (finalizationCallback) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @intToPtr(JSC.C.JSTypedArrayBytesDeallocator, callback_ptr); + + if (finalizationCtxOrPtr) |ctx_value| { + if (getCPtr(ctx_value)) |ctx_ptr| { + ctx = @intToPtr(*anyopaque, ctx_ptr); + } else if (!ctx_value.isUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } else if (finalizationCtxOrPtr) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @intToPtr(JSC.C.JSTypedArrayBytesDeallocator, callback_ptr); + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + + return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis.ref(), ctx, callback, null); }, } } @@ -2574,12 +2618,42 @@ pub const FFI = struct { value: JSValue, byteOffset: ?JSValue, valueLength: ?JSValue, + finalizationCtxOrPtr: ?JSValue, + finalizationCallback: ?JSValue, ) JSC.JSValue { switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { .err => |erro| { return erro; }, .slice => |slice| { + var callback: JSC.C.JSTypedArrayBytesDeallocator = null; + var ctx: ?*anyopaque = null; + if (finalizationCallback) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @intToPtr(JSC.C.JSTypedArrayBytesDeallocator, callback_ptr); + + if (finalizationCtxOrPtr) |ctx_value| { + if (getCPtr(ctx_value)) |ctx_ptr| { + ctx = @intToPtr(*anyopaque, ctx_ptr); + } else if (!ctx_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } else if (finalizationCtxOrPtr) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @intToPtr(JSC.C.JSTypedArrayBytesDeallocator, callback_ptr); + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + + if (callback != null or ctx != null) { + return JSC.JSValue.createBufferWithCtx(globalThis, slice, ctx, callback); + } + return JSC.JSValue.createBuffer(globalThis, slice, null); }, } |