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(×tamp, 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(×tamp, 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
相关产品推荐
相关产品推荐

