aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-08-09 16:16:51 -0700
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-08-09 16:16:51 -0700
commitb428227895b0fb29c9d9060b7d12191428e37736 (patch)
treedc245e0e14b7bde0340a8b105da994498a022156
parent6c8ba6c5fc53adc4ae9a9af73f982ff2aee6e196 (diff)
downloadbun-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.md55
-rw-r--r--src/bun.js/api/bun.zig76
2 files changed, 126 insertions, 5 deletions
diff --git a/README.md b/README.md
index ea85f9f30..d0333c464 100644
--- a/README.md
+++ b/README.md
@@ -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);
},
}