jlbun is a high-performance FFI library that brings Julia's computational power to the Bun JavaScript runtime. Call Julia functions directly from TypeScript with zero-copy array sharing and automatic memory management.
Scope-Based GC & Concurrent Async Support - The v0.2 release brings a complete GC redesign with scope isolation for safe concurrent async operations.
| Feature | Description |
|---|---|
| 🔀 Concurrent Async Scopes | Multiple Julia.scopeAsync() can run in parallel safely |
| 🛡️ Safe Mode | Julia.scope(fn, { mode: "safe" }) for automatic closure safety |
| ⚡ Perf Mode | Julia.scope(fn, { mode: "perf" }) lock-free for single-threaded LIFO |
| 🔒 Thread-Safe GC | All C-layer operations protected by pthread_mutex_t |
| 📦 Scope Isolation | Each scope has unique ID; release order doesn't matter |
- Removed deprecated
GCManager.protect()/GCManager.unprotect()APIs - Removed legacy
GCManager.mark()/GCManager.push()/GCManager.release()APIs - Escaped objects now use FinalizationRegistry for automatic cleanup
| Feature | Description |
|---|---|
🛡️ Julia.scope() |
Automatic tracking and cleanup of Julia objects |
🚀 julia.untracked() |
Opt-out for performance-critical loops |
📊 JuliaRange |
Native support for UnitRange, StepRange, LinRange |
🔍 JuliaSubArray |
Zero-copy array views with view() and slice() |
| ⚡ Multi-dimensional Arrays | Direct N-D array creation with getAt()/setAt() |
- jlbun - Using Julia in Bun
Requirements:
Bun,CMake, andJulia >=1.10
npm install jlbunNote: Use
npm installinstead ofbun installas the latter doesn't run install scripts.
import { Julia } from "jlbun";
Julia.init();
// Evaluate Julia code
Julia.eval('println("Hello from Julia!")');
// Call Julia functions directly
const result = Julia.Base.sum([1, 2, 3, 4, 5]);
console.log(result.value); // 15
Julia.close();See the examples/ directory for complete working examples:
bun examples/01_linear_algebra.ts # Matrix ops, eigenvalues
bun examples/02_monte_carlo.ts # π estimation, random walks
bun examples/03_zero_copy.ts # Memory sharing (core feature)
bun examples/04_complex_numbers.ts # Complex arithmeticAll examples use only Julia stdlib - no package installation required.
All Julia objects created within a scope are automatically tracked and freed when the scope exits.
import { Julia } from "jlbun";
Julia.init();
const result = Julia.scope((julia) => {
// Create matrices - automatically tracked
const a = julia.Base.rand(1000, 1000);
const b = julia.Base.rand(1000, 1000);
const c = julia.Base["*"](a, b);
// Return a JS value - Julia objects auto-released
return julia.Base.sum(c).value;
});
console.log(result); // A number; matrices a, b, c are freed
Julia.close();For async operations:
const result = await Julia.scopeAsync(async (julia) => {
const func = julia.eval("() -> sum(1:1000000)");
const task = JuliaTask.from(func);
return (await task.value).value;
});v0.2 introduces safe mode for code that captures Julia objects in closures or callbacks:
// Default mode: objects released at scope end (fast, but closures need escape())
Julia.scope((julia) => {
const arr = julia.Array.init(julia.Float64, 100);
// arr is valid here
}); // arr released here
// Safe mode: objects released when JS GC runs (closure-safe)
Julia.scope((julia) => {
const arr = julia.Array.init(julia.Float64, 100);
// Safe to capture in closures - no explicit escape() needed!
setTimeout(() => {
console.log(arr.length); // arr is still valid
}, 1000);
}, { mode: "safe" }); // arr stays alive until JS GC| Scenario | Recommended Mode |
|---|---|
| General purpose / unsure | default |
| High-performance batch processing | perf |
| Closures / callbacks (setTimeout, etc.) | safe |
JuliaTask parallelism |
default |
| Simple synchronous loops | perf |
// Set default mode globally
Julia.defaultScopeMode = "perf";
// All subsequent scopes use perf mode
Julia.scope((julia) => { ... });
// Override for specific scope
Julia.scope((julia) => { ... }, { mode: "safe" });Mode characteristics:
| Mode | Thread-Safe | Closure-Safe | Performance |
|---|---|---|---|
perf |
❌ | ❌ | ⚡⚡⚡ Fastest |
default |
✅ | ❌ | ⚡⚡ Good |
safe |
✅ | ✅ | ⚡ Slower |
Note:
Julia.scopeAsync()always usessafemode internally.
To keep a Julia object alive beyond the scope:
// Method 1: Return the JuliaValue directly (auto-escaped)
const arr = Julia.scope((julia) => {
return julia.Base.rand(100); // Survives scope exit
});
console.log(arr.length); // 100
// Method 2: Explicit escape
const sorted = Julia.scope((julia) => {
const temp = julia.Base.rand(100);
const result = julia.Base.sort(temp);
return julia.escape(result); // Only `result` survives
});For high-iteration loops, auto-tracking adds overhead. Use untracked() to temporarily disable it:
Julia.scope((julia) => {
const arr = julia.Array.from(new Float64Array([1, 2, 3, 4, 5, 6, 7, 8]));
// ~300x faster than tracked calls
julia.untracked(() => {
for (let i = 0; i < 10000; i++) {
const range = julia.Base.UnitRange(1, 5);
julia.Base.view(arr, range); // Temporary, not tracked
}
});
return julia.Base.sum(arr).value;
});Key behaviors:
- Only affects current scope (nested
Julia.scope()calls have independent tracking) - Tracking resumes after the block, even if an exception is thrown
JavaScript TypedArray and Julia Array share the same memory:
import { Julia, JuliaArray } from "jlbun";
Julia.init();
const bunArray = new Float64Array([1, 2, 3, 4, 5]);
const juliaArray = JuliaArray.from(bunArray);
// Modify from JS side
bunArray[0] = 100;
Julia.Base.println(juliaArray); // [100.0, 2.0, 3.0, 4.0, 5.0]
// Modify from Julia side (0-indexed API)
juliaArray.set(1, -1);
console.log(bunArray[1]); // -1
Julia.close();Create N-dimensional arrays directly:
const matrix = JuliaArray.init(Julia.Float64, 10, 20); // 2D: 10x20
const tensor = JuliaArray.init(Julia.Float64, 3, 4, 5); // 3D: 3x4x5
console.log(matrix.ndims); // 2
console.log(matrix.size); // [10, 20]
// Multi-dimensional indexing (0-based)
matrix.setAt(2, 3, 42.0);
console.log(matrix.getAt(2, 3).value); // 42.0Julia uses column-major order. Use
getAt()/setAt()for intuitive multi-dimensional access.
Create zero-copy views with view() and slice():
const arr = JuliaArray.from(new Float64Array([1, 2, 3, 4, 5, 6, 7, 8]));
// Range view (0-based, inclusive)
const sub = arr.view([1, 4]);
console.log(sub.value); // Float64Array [2, 3, 4, 5]
// 1D slice (convenience method)
const slice = arr.slice(2, 5);
console.log(slice.value); // Float64Array [3, 4, 5, 6]
// Views share memory - modifications propagate!
sub.set(0, 100);
console.log(arr.get(1).value); // 100
// Multi-dimensional views
const matrix = JuliaArray.init(Julia.Float64, 4, 4);
const row = matrix.view(0, ":"); // Row 0, all columns
const block = matrix.view([1, 2], [0, 2]); // Rows 1-2, cols 0-2
// Create independent copy when needed
const copy = sub.copy();Work with Julia ranges directly:
import { Julia, JuliaRange } from "jlbun";
Julia.init();
// Create ranges
const unit = JuliaRange.from(1, 10); // 1:10
const step = JuliaRange.from(1, 10, 2); // 1:2:10 → [1, 3, 5, 7, 9]
const lin = JuliaRange.linspace(0, 1, 5); // LinRange(0.0, 1.0, 5)
// Properties
console.log(unit.length); // 10
console.log(unit.first.value); // 1n
console.log(step.step.value); // 2n
// Iteration
for (const val of unit) {
console.log(val.value);
}
// Use with Julia functions
console.log(Julia.Base.sum(unit).value); // 55n
Julia.close();// Direct function calls
const result = Julia.Base.sqrt(2);
console.log(result.value); // 1.4142135623730951
// Operators as functions
const product = Julia.Base["*"](matrix1, matrix2);
// Import modules
const LA = Julia.import("LinearAlgebra");
console.log(LA.norm(vector).value);const arr = JuliaArray.from(new Int32Array([1, 10, 20, 30, 100]));
Julia.Base["sort!"].callWithKwargs({ by: Julia.Base.string, rev: true }, arr);
// Result: [30, 20, 100, 10, 1]import { Julia, JuliaArray, JuliaFunction } from "jlbun";
Julia.init();
const negate = JuliaFunction.from((x: number) => -x, {
returns: "i32",
args: ["i32"],
});
const arr = JuliaArray.from(new Int32Array([1, 2, 3]));
const neg = arr.map(negate);
Julia.println(neg); // Int32[-1, -2, -3]
negate.close(); // Optional: auto-cleaned when GC'd
Julia.close();// Install packages
Julia.Pkg.add("CairoMakie");
// Import modules
const Cairo = Julia.import("CairoMakie");
// Use
const plt = Cairo.plot(Julia.Base.rand(10), Julia.Base.rand(10));
Cairo.save("plot.png", plt);Set JULIA_NUM_THREADS before running:
export JULIA_NUM_THREADS=8import { Julia, JuliaFunction, JuliaTask, JuliaValue } from "jlbun";
Julia.init();
const func = Julia.eval(`() -> sum(1:1000)`);
const promises: Promise<JuliaValue>[] = [];
for (let i = 0; i < Julia.nthreads; i++) {
promises.push(JuliaTask.from(func).schedule(i).value);
}
const results = await Promise.all(promises);
console.log(results.map(r => r.value)); // [500500n, 500500n, ...]
Julia.close();JuliaPtr provides access to Julia's Ptr{T} type:
import { Julia, JuliaArray, JuliaPtr } from "jlbun";
Julia.init();
const arr = JuliaArray.from(new Float64Array([1, 2, 3, 4, 5]));
const ptr = JuliaPtr.fromArray(arr);
// Read/write (0-based)
console.log(ptr.load(0).value); // 1.0
ptr.store(99.0, 1);
// Pointer arithmetic
const ptr2 = ptr.offset(2);
console.log(ptr2.load(0).value); // 3.0
Julia.close();
⚠️ Warning:load()andstore()are unsafe operations.
jlbun provides TypeScript wrappers for Julia's primitive types:
| TypeScript | Julia | JS Value |
|---|---|---|
JuliaInt8/16/32 |
Int8/16/32 |
number |
JuliaInt64 |
Int64 |
bigint |
JuliaUInt8/16/32 |
UInt8/16/32 |
number |
JuliaUInt64 |
UInt64 |
bigint |
JuliaFloat16 |
Float16 |
number |
JuliaFloat32 |
Float32 |
number |
JuliaFloat64 |
Float64 |
number |
JuliaComplex |
ComplexF64/F32/F16 |
{re, im} |
JuliaString |
String |
string |
JuliaBool |
Bool |
boolean |
JuliaChar |
Char |
string |
JuliaSymbol |
Symbol |
Symbol |
import { JuliaFloat16, JuliaInt64, JuliaComplex } from "jlbun";
// Create from JavaScript
const f16 = JuliaFloat16.from(3.14); // Float16 with ~3 decimal precision
const i64 = JuliaInt64.from(9007199254740993n); // BigInt for large integers
// Access value
console.log(f16.value); // 3.140625 (Float16 precision)
console.log(i64.value); // 9007199254740993n
// Complex numbers
const c = JuliaComplex.from(3, 4); // 3 + 4im (ComplexF64)
console.log(c.re, c.im); // 3, 4
console.log(c.abs); // 5 (magnitude)
console.log(c.arg); // 0.927... (phase in radians)
console.log(c.value); // { re: 3, im: 4 }
// Different precisions
const c32 = JuliaComplex.fromF32(1, 2); // ComplexF32
const c16 = JuliaComplex.fromF16(1, 2); // ComplexF16
// From polar form
const polar = JuliaComplex.fromPolar(5, Math.PI / 4); // r=5, θ=45°Julia exceptions are automatically mapped to TypeScript error classes:
import { BoundsError, DomainError, JuliaError } from "jlbun";
try {
Julia.Base.sqrt(-1);
} catch (e) {
if (e instanceof DomainError) {
console.log("Domain error:", e.message);
} else if (e instanceof JuliaError) {
console.log("Julia error type:", e.juliaType);
}
}| Error Class | Julia Type | Trigger |
|---|---|---|
MethodError |
MethodError |
No method matches arguments |
BoundsError |
BoundsError |
Array index out of bounds |
DomainError |
DomainError |
Invalid domain (e.g., sqrt(-1)) |
DivideError |
DivideError |
Integer division by zero |
KeyError |
KeyError |
Missing dictionary key |
ArgumentError |
ArgumentError |
Invalid function argument |
TypeError |
TypeError |
Wrong argument type |
UndefVarError |
UndefVarError |
Undefined variable |
DimensionMismatch |
DimensionMismatch |
Array dimension mismatch |
InexactError |
InexactError |
Cannot convert exactly |
UnknownJuliaError |
(other) | Check e.juliaType |
All error classes inherit from JuliaError, which provides a juliaType property containing the original Julia exception type name.
jlbun is optimized for minimal FFI overhead:
| Optimization | Description |
|---|---|
| Type Pointer Comparison | O(1) type checking for primitives |
| Type String Cache | Avoids repeated FFI calls |
| Zero-Copy Arrays | Memory sharing with TypedArray |
| Direct FFI Calls | 8-260x faster than Julia.eval() |
// ❌ Slow: string parsing
const arr = Julia.eval("zeros(1000, 1000)");
// ✅ Fast: direct FFI
const arr = Julia.Base.zeros(Julia.Float64, 1000, 1000);
// ✅ Fastest: uninitialized
const arr = JuliaArray.init(Julia.Float64, 1000, 1000);| Use Case | Recommended |
|---|---|
| Zero-initialized | Julia.Base.zeros(type, dims...) |
| Fill with value | Julia.Base.fill(value, dims...) |
| Will overwrite all | JuliaArray.init(type, dims...) |
| From TypedArray | JuliaArray.from(typedArray) |
