jlbun
    Preparing search index...

    jlbun

    jlbun - Using Julia in Bun

    Version NPM Build Status Coverage

    Logo of jlbun

    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()


    Requirements: Bun, CMake, and Julia >=1.10

    npm install jlbun
    

    Note: Use npm install instead of bun install as 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 arithmetic

    All 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 uses safe mode 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.0

    Julia 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=8
    
    import { 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() and store() 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)

    Star History Chart