在Deno中使用FFI如何处理C函数返回的结构体?
在Deno FFI中正确映射C的CommandList结构体到TypeScript可遍历接口
问题描述
我有一个返回CommandList结构体的C函数,结构体定义如下:
typedef struct Command { enum CommandId id; const char *arg1; const char *arg2; } Command; typedef struct CommandList { const struct Command *test_commands; uintptr_t len; } CommandList;
我在Deno中通过FFI声明该函数:
const dylib = Deno.dlopen( libName, { load_scenario: { parameters: ["buffer"], result: { struct: CommandList } }, free_test: { parameters: [{ struct: CommandList }], result: "void" }, } as const, );
调用后返回的是一个Uint8Array:
Uint8Array(16) [ 112, 61, 21, 11, 167, 85, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0 ]
已知该列表包含7个Command元素,但当前返回形式无法直接处理。我希望将结构体映射为更易用的TypeScript接口,目标接口和代码形式如下:
interface Command { id: number; arg1: string; arg2: string; } interface CommandList { data: Command[]; len: number; } // 期望的使用方式 const command_list: CommandList = dylib.symbols.load_scenario(buf); for (let i = 0; i < command_list.len; i++) { const command: Command = command_list.data[i]; console.log(command.id, command.arg1, command.arg2); }
我查阅了Deno文档、FFI测试用例并做了大量搜索,未找到解决方案,特此求助。
解决方案
要实现这个需求,需要手动解析Deno FFI返回的结构体数据,利用Deno.UnsafePointerView读取指针指向的内容,步骤如下:
1. 定义TypeScript中的结构体内存布局
先明确C结构体在64位系统中的内存布局(指针为8字节,uintptr_t为8字节,枚举CommandId通常等价于uint32_t):
// 对应C的Command结构体内存布局 const CommandStructLayout = { id: "u32", arg1: "pointer", arg2: "pointer", } as const; // 对应C的CommandList结构体内存布局 const CommandListStructLayout = { test_commands: "pointer", len: "u64", // 64位系统下uintptr_t对应u64 } as const;
2. 调整Deno.dlopen声明
更新函数声明,使用明确的结构体布局描述:
const dylib = Deno.dlopen( libName, { load_scenario: { parameters: ["buffer"], result: { struct: CommandListStructLayout } }, free_test: { parameters: [{ struct: CommandListStructLayout }], result: "void" }, } as const, );
3. 编写结构体解析函数
编写函数将C端返回的原始结构体转换为目标TypeScript接口:
interface Command { id: number; arg1: string; arg2: string; } interface CommandList { data: Command[]; len: number; } function parseCommandList(rawList: { test_commands: Deno.PointerValue; len: bigint }): CommandList { const commandsPtr = rawList.test_commands; const len = Number(rawList.len); const commands: Command[] = []; // 先通过C代码确认sizeof(Command)的实际值,这里按64位系统对齐后的24字节计算 const commandSize = 24; for (let i = 0; i < len; i++) { const cmdPtr = Deno.UnsafePointer.add(commandsPtr, i * commandSize); const cmdView = new Deno.UnsafePointerView(cmdPtr); // 读取Command的各个字段 const id = cmdView.getUint32(0); const arg1Ptr = cmdView.getPointer(4); const arg2Ptr = cmdView.getPointer(12); // 将C风格字符串转换为TypeScript字符串 const arg1 = arg1Ptr ? new Deno.UnsafePointerView(arg1Ptr).getCString() : ""; const arg2 = arg2Ptr ? new Deno.UnsafePointerView(arg2Ptr).getCString() : ""; commands.push({ id, arg1, arg2 }); } return { data: commands, len }; }
4. 调用并使用解析后的结果
现在可以按照期望的方式调用和遍历数据:
// 调用C函数获取原始结构体 const rawCommandList = dylib.symbols.load_scenario(buf); // 解析为易用的TypeScript结构 const commandList = parseCommandList(rawCommandList); // 遍历处理每个命令 for (const cmd of commandList.data) { console.log(cmd.id, cmd.arg1, cmd.arg2); } // 务必记得调用释放函数,避免内存泄漏 dylib.symbols.free_test(rawCommandList);
关键注意事项
- 内存对齐:不同系统的内存对齐规则可能不同,必须通过C代码打印
sizeof(Command)来确认准确的结构体大小,替换代码中的commandSize。 - 字符串安全:确保C端返回的
arg1、arg2是以\0结尾的合法C字符串,否则getCString()会读取到非法内存。 - 内存释放:必须调用C端提供的
free_test函数释放分配的内存,防止内存泄漏。
内容的提问来源于stack exchange,提问作者Jean François
相关产品推荐
相关产品推荐

