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

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 04:11:10