You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Bun FFI调用Zig编写的ULID生成函数返回空字符串问题

问题根源:栈内存生命周期失效

你的ULID函数在Zig主函数里能正常运行,但通过Bun FFI调用返回空字符串,核心问题出在局部变量的内存生命周期上:

  • 测试用的"Hello, World!"是编译期常量,存在程序的数据段里,函数返回后内存不会被销毁,所以Bun能正常读取;
  • 但ulid()里的uint8_array是函数内的栈上局部变量,函数执行完毕后栈帧会被系统回收,返回的指针就变成了悬空指针,Bun访问时内存已经被覆盖,所以输出空字符串。
修复方案

方案1:堆分配内存(需手动释放)

用Zig的C标准分配器在堆上创建内存块,这样内存不会随函数栈帧销毁,但要记得在JS侧调用释放函数避免内存泄漏。

修改后的Zig代码:

const std = @import("std");

// 保留reverse和toBase32函数不变
pub fn reverse(source: []const u8, target: []u8) void {
    for (source, 0..) |c, i| {
        target[source.len - 1 - i] = c;
    }
}

fn toBase32(bytes: []const u8, str: []u8) void {
    const BASE_32_CHARACTERS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

    var bitBuffer: u16 = 0;
    var bitCount: u4 = 0;
    var strIndex: usize = 0;

    for (bytes) |byte| {
        bitBuffer = (bitBuffer << 8) | @as(u16, byte);
        bitCount += 8;

        while (bitCount >= 5) {
            bitCount -= 5;
            const index = (bitBuffer >> bitCount) & 0x1F;
            str[strIndex] = BASE_32_CHARACTERS[index];
            strIndex += 1;
        }
    }

    if (bitCount > 0) {
        const index = (bitBuffer >> (5 - bitCount)) & 0x1F;
        str[strIndex] = BASE_32_CHARACTERS[index];
    }
}

pub export fn ulid() [*:0]const u8 {
    const seed = @as(u64, @intCast(std.time.nanoTimestamp()));
    var rng = std.rand.DefaultPrng.init(seed);

    const current_timestamp = std.time.milliTimestamp();
    var timestamp: [6]u8 = undefined;
    reverse(std.mem.toBytes(current_timestamp)[0..6], timestamp[0..]);

    var random_data: [10]u8 = undefined;
    for (0..10) |i| {
        random_data[i] = rng.random().uintAtMost(u8, 255);
    }
    var randomness: [10]u8 = undefined;
    // 修正:直接传random_data的切片,不需要toBytes
    reverse(random_data[0..], randomness[0..]);

    // 用C分配器在堆上分配带空终止符的内存
    const uint8_array = std.heap.c_allocator.allocSentinel(u8, 26, 0) catch unreachable;
    toBase32(&timestamp, uint8_array[0..10]);
    toBase32(&randomness, uint8_array[10..26]);
    return uint8_array.ptr;
}

// 导出释放函数,让JS侧调用
pub export fn ulidFree(ptr: [*:0]u8) void {
    const slice = std.mem.span(ptr);
    std.heap.c_allocator.free(slice);
}

对应JS代码:

import { join } from "path";
import { dlopen, FFIType, suffix } from "bun:ffi";

export const ulid = () => {
    const lib = dlopen(join(process.cwd(), "bin", `libulid.${suffix}`), {
        ulid: {
            args: [],
            returns: FFIType.cstring,
        },
        ulidFree: {
            args: [FFIType.cstring],
            returns: FFIType.void,
        },
    });

    const str = lib.symbols.ulid();
    console.log(str);
    // 使用完毕后释放堆内存
    lib.symbols.ulidFree(str);
};

方案2:JS侧传递缓冲区(更高效,无内存管理负担)

由JS提前分配好缓冲区,传递给Zig函数填充,完全避免堆分配和内存泄漏问题。

修改后的Zig代码:

const std = @import("std");

// 保留reverse和toBase32函数不变
pub fn reverse(source: []const u8, target: []u8) void {
    for (source, 0..) |c, i| {
        target[source.len - 1 - i] = c;
    }
}

fn toBase32(bytes: []const u8, str: []u8) void {
    const BASE_32_CHARACTERS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

    var bitBuffer: u16 = 0;
    var bitCount: u4 = 0;
    var strIndex: usize = 0;

    for (bytes) |byte| {
        bitBuffer = (bitBuffer << 8) | @as(u16, byte);
        bitCount += 8;

        while (bitCount >= 5) {
            bitCount -= 5;
            const index = (bitBuffer >> bitCount) & 0x1F;
            str[strIndex] = BASE_32_CHARACTERS[index];
            strIndex += 1;
        }
    }

    if (bitCount > 0) {
        const index = (bitBuffer >> (5 - bitCount)) & 0x1F;
        str[strIndex] = BASE_32_CHARACTERS[index];
    }
}

// 接收JS传递的缓冲区,要求长度至少27字节(26个字符+1个空终止符)
pub export fn ulid(buffer: [*:0]u8, buffer_len: usize) void {
    if (buffer_len < 27) return; // 缓冲区太小直接返回

    const seed = @as(u64, @intCast(std.time.nanoTimestamp()));
    var rng = std.rand.DefaultPrng.init(seed);

    const current_timestamp = std.time.milliTimestamp();
    var timestamp: [6]u8 = undefined;
    reverse(std.mem.toBytes(current_timestamp)[0..6], timestamp[0..]);

    var random_data: [10]u8 = undefined;
    for (0..10) |i| {
        random_data[i] = rng.random().uintAtMost(u8, 255);
    }
    var randomness: [10]u8 = undefined;
    reverse(random_data[0..], randomness[0..]);

    const slice = buffer[0..26];
    toBase32(&timestamp, slice[0..10]);
    toBase32(&randomness, slice[10..26]);
    buffer[26] = 0; // 写入空终止符
}

对应JS代码:

import { join } from "path";
import { dlopen, FFIType, suffix, ptr } from "bun:ffi";

export const ulid = () => {
    // 分配27字节的缓冲区
    const buffer = new Uint8Array(27);
    const lib = dlopen(join(process.cwd(), "bin", `libulid.${suffix}`), {
        ulid: {
            args: [FFIType.ptr, FFIType.usize],
            returns: FFIType.void,
        },
    });

    lib.symbols.ulid(ptr(buffer), buffer.length);
    // 将缓冲区转为字符串
    const str = new TextDecoder().decode(buffer.subarray(0, 26));
    console.log(str);
};
额外修正点

你原来的reverse(&std.mem.toBytes(random_data), randomness[0..])写法有问题:std.mem.toBytes(random_data)是把整个数组转为字节切片(和原数组内容完全一致),不需要取地址,直接传random_data[0..]即可,否则会导致反转逻辑出错。

内容的提问来源于stack exchange,提问作者Mnengwa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 12:54:56