Bun ffi native binding
Skill Harmeet10000/skills/skills/backend/Node_Bun/bun-ffi-native-binding
Collection of my Agent Skills and books.
npx -y skills add Harmeet10000/skills --skill bun-ffi-native-bindingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Build high-performance native modules for JavaScript using Bun's FFI (Foreign Function Interface) with Zig or C. Use when optimizing hot paths, integrating system libraries, or requiring native performance for compute-intensive operations.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
4.0 KB, as published. Nobody here has run it
Bun FFI Native Binding Skill
Build native extensions for JavaScript using Bun's tight integration with Zig and C via FFI.
When to Use
- Hot paths: Compute-intensive operations (crypto, compression, math)
- System integration: Direct OS/hardware access
- Large data processing: Batch operations on arrays/buffers
- Legacy libraries: Wrap existing C/Zig libraries
Two Approaches
1. Zig Bindgen (Recommended)
Zig functions compiled directly into Bun with zero-overhead bindings.
Setup:
bun add -d @zig/build
Zig function (src/math.zig):
const std = @import("std");
const jsc = @import("jsc");
pub fn add(global: *jsc.JSGlobalObject, a: i32, b: i32) !i32 {
return std.math.add(i32, a, b) catch {
return global.throwPretty("Integer overflow", .{});
};
}
Binding declaration (src/bindings.ts):
import { t, fn } from "bindgen";
export const add = fn({
args: { global: t.globalObject, a: t.i32, b: t.i32 },
ret: t.i32
});
Usage (index.ts):
import { add } from "bun:math";
console.log(add(2, 3)); // 5
2. C FFI (Dynamic Loading)
Load C libraries at runtime without compilation.
C function (lib.c):
int add(int a, int b) {
return a + b;
}
Compile:
gcc -shared -fPIC -o lib.so lib.c
Load in Bun (index.ts):
import { dlopen, FFIType } from "bun:ffi";
const lib = dlopen("./lib.so", {
add: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
});
console.log(lib.symbols.add(2, 3)); // 5
Performance Considerations
Bridge Cost
- Overhead: 10-100 nanoseconds per call
- Dominates: Tiny functions called repeatedly
- Solution: Batch operations
Data Conversion
- Overhead: Proportional to payload size
- Dominates: Complex object marshaling
- Solution: Use typed arrays, avoid JSON
Rule of Thumb
If work per call > bridge cost → native wins
Critical Edge Cases
See references/EDGE_CASES.md for:
- Exception boundaries (panics crash runtime)
- Memory ownership (who frees allocations?)
- Struct alignment (layout assumptions)
- GC interaction (pinning references)
- Thread safety (event loop constraints)
- ABI compatibility (C calling convention)
Best Practices
- Minimize boundary crossings — batch processing in native code
- Use typed arrays — zero-copy buffer mapping
- Avoid per-call allocation — reuse buffers
- Binary formats — faster than JSON serialization
- Stable APIs — version struct layouts
- Error handling — convert panics to JS exceptions
Optimization Checklist
- Minimize JS → native calls
- Avoid JSON across boundary
- Use typed arrays/buffers
- Batch processing in native
- Convert errors to JS exceptions
- No Zig panics escape to JS
- No global mutable state
- Benchmark boundary latency
- No per-call memory allocation
- Thread safety verified
Example: Batch Array Processing
Zig (src/process.zig):
pub fn processArray(global: *jsc.JSGlobalObject, ptr: [*]u32, len: usize) !u32 {
var sum: u32 = 0;
for (0..len) |i| {
sum +|= ptr[i];
}
return sum;
}
JS (index.ts):
const data = new Uint32Array([1, 2, 3, 4, 5]);
const sum = processArray(data.buffer, data.length);
This avoids 5 separate JS→native calls and marshals data once.