Dora C API 完全指南:用 C 语言开发 Node 与 Operator
【免费下载链接】doraDORA (Dataflow-Oriented Robotic Architecture) is middleware designed to streamline and simplify the creation of AI-based robotic applications. It offers low latency, composable, and distributed dataflow capabilities. Applications are modeled as directed graphs, also referred to as pipelines.项目地址: https://gitcode.com/GitHub_Trending/do/dora
DORA(Dataflow-Oriented Robotic Architecture)以数据流图(pipeline)为核心组织机器人应用,而 C 语言是其多语言支持中与 Rust 运行时衔接最直接的一层。本文以仓库文档 guide/src/languages/c.md 为主体,系统讲解 Dora 提供的两套 C 接口:供独立进程使用的Node API(dora-node-api-c)与供共享库 Operator 使用的Operator API(dora-operator-api-c)。读完本文,你将掌握 C 节点的初始化、事件循环与输出发送,C Operator 的生命周期三函数与事件处理,并能在examples/c-dataflow的真实工程中完成编译、链接与数据流编排。
一、两种 C API 的定位与区别
Dora 的 C 生态包含两个层级不同的 API,理解它们的差异是正确选型的前提:
| 维度 | Node API | Operator API |
|---|---|---|
| 产物形态 | 独立可执行文件(C 进程) | 共享库(.so/.dylib/.dll) |
| 头文件 | apis/c/node/node_api.h | apis/c/operator/operator_api.h、apis/c/operator/operator_types.h |
| Rust crate | dora-node-api-c(构建为staticlib) | dora-operator-api-c |
| 运行载体 | daemon 以外部进程方式 spawn,并通过环境变量传入初始化信息 | 由 Dora 运行时进程在启动时dlopen加载 |
| 入口形态 | 拥有自己的main函数 | 没有main,导出三个生命周期函数供运行时回调 |
Node 是"数据流中的外部参与者":daemon 启动该进程并设置环境变量,节点在init_dora_context_from_env中读取这些变量完成初始化。Operator 则"寄生"在运行时进程内,与运行时共享内存空间,因此调用开销更低、状态传递更直接,但必须严格遵守运行时约定的生命周期与内存所有权规则。
二、Node API 详解(dora-node-api-c)
头文件 apis/c/node/node_api.h 定义了全部 Node API。从源码可以看到该头文件同时是 Rust crate 通过include_str!嵌入、再经no_mangle导出符号的实现源(见 apis/c/node/src/lib.rs),因此头文件与二进制始终同步。
2.1 初始化与销毁
init_dora_context_from_env
void *init_dora_context_from_env();从 daemon 设置的环境变量初始化 Dora 节点上下文,成功返回不透明指针,失败返回NULL。该指针必须传给所有后续 Node API 调用,使用完毕后用free_dora_context释放。从实现看(apis/c/node/src/lib.rs),它内部调用DoraNode::init_from_env()并通过Box::into_raw将 Rust 对象泄漏为裸指针返回,因此NULL之外的所有指针都必须走 API 释放,不能free()。
free_dora_context
void free_dora_context(void *dora_context);释放init_dora_context_from_env创建的上下文。每个上下文恰好释放一次;释放后指针不得再使用。
2.2 事件循环
dora_next_event
void *dora_next_event(void *dora_context);阻塞等待下一个事件。返回不透明事件指针,或当所有事件流关闭时返回NULL(节点通常应据此退出)。返回的指针不能直接解引用,须用read_dora_*系列函数读取类型与载荷,用完调用free_dora_event释放。源码中它对应context.events.recv()的Option映射:Some(event)装箱返回裸指针,None返回空指针(apis/c/node/src/lib.rs)。
free_dora_event
void free_dora_event(void *dora_event);释放dora_next_event返回的事件,每个事件恰好释放一次。释放后,事件指针以及由read_dora_input_id、read_dora_input_data派生的所有指针全部失效——因为那些指针直接指向事件内部内存。
2.3 事件检查
read_dora_event_type
enum DoraEventType read_dora_event_type(void *dora_event);返回事件类型,取值见下文 DoraEventType。实现上它把 Rust 的Event枚举映射为 C 枚举:Stop、Input、InputClosed、Error之外的任意变体统一归为Unknown(apis/c/node/src/lib.rs)。
read_dora_input_id
void read_dora_input_id(void *dora_event, char **out_ptr, size_t *out_len);从DoraEventType_Input事件中读取输入 ID:起始指针写入*out_ptr,字节长度写入*out_len。该字符串是合法 UTF-8 但非 NUL 结尾,必须用out_len界定边界。若事件不是输入事件,则写入*out_ptr = NULL、*out_len = 0。
read_dora_input_data
void read_dora_input_data(void *dora_event, char **out_ptr, size_t *out_len);读取输入事件的原始数据字节。非输入事件或输入无数据时写入NULL/0。
关于数据类型支持,原文档提到"目前仅支持UInt8Arrow 数组,其他 Arrow 类型会导致运行时 panic",而当前源码已经演进了这一行为:从 apis/c/node/src/lib.rs 的实现看,非UInt8/Null的 Arrow 类型不再中止进程,而是记录tracing::error!日志并返回out_ptr == NULL、out_len == 0表示"无数据"。这一点有单元测试read_dora_input_data_non_uint8_returns_null直接佐证(如Int32载荷返回空指针而非崩溃)。因此实际编码时应把"空指针"理解为"无可用原始字节视图",它既可能是真无数据,也可能是载荷类型不受支持——跨语言场景(例如其他节点发来Int32/Float)尤其要留意。零长度UInt8载荷同样返回NULL/0,测试read_dora_input_data_empty_uint8_returns_null验证了该契约。
read_dora_input_timestamp
unsigned long long read_dora_input_timestamp(void *dora_event);返回输入事件元数据中的混合逻辑时钟(hybrid logical clock, HLC)时间戳,以uint64返回;非输入事件返回0。
2.4 发送输出
dora_send_output
int dora_send_output( void *dora_context, const char *id_ptr, size_t id_len, const char *data_ptr, size_t data_len );向所有下游订阅者发送输出数据。id_ptr/id_len必须是合法 UTF-8 字符串,且须与 dataflow YAML 中该节点声明的某个输出 ID 一致;data_ptr/data_len作为原始字节(UInt8 Arrow 数组)发送。成功返回0,失败返回-1,错误经tracing记录;任一指针参数为NULL时立即返回-1。
从实现看(apis/c/node/src/lib.rs),这里有两个值得注意的细节:
- 输出 ID 会经
DataId::from_str解析,解析失败(例如 ID 中含空格)会返回-1而不是跨 FFI 边界 panic; - 发送路径允许
(NULL, 0)表示空消息(data_slice对data_len == 0且指针为空的组合显式放行),这使 C 节点可以用惯用的dora_send_output(ctx, id, id_len, NULL, 0)发送空载荷;但data_len > 0时data_ptr为NULL会被拒绝(有测试data_slice_rejects_null_with_nonzero_len验证)。
2.5 结构化日志
dora_log
int dora_log( void *dora_context, const char *level_ptr, size_t level_len, const char *msg_ptr, size_t msg_len );通过 Dora 日志管线发送结构化日志。level与msg均须为合法 UTF-8 字符串。合法日志级别:"error"、"warn"、"info"、"debug"、"trace"。成功返回0,失败返回-1;任一指针为NULL立即返回-1。
2.6 DoraEventType 枚举
enum DoraEventType { DoraEventType_Stop, // Graceful shutdown requested DoraEventType_Input, // New input data available DoraEventType_InputClosed, // An input stream was closed DoraEventType_Error, // An error occurred DoraEventType_Unknown, // Unrecognized event type };2.7 Node API 的线程安全契约
这是头文件 apis/c/node/node_api.h 中一段容易被忽略、但对多线程 C 节点至关重要的注释,值得单独说明:
- 上下文不线程安全:
dora_next_event、dora_send_output、dora_log都会修改上下文内部状态(事件流游标、发送器状态、日志状态),同一上下文被多线程并发调用属于未定义行为。如需把事件扇出到工作线程,应在单线程内 drain 事件,再按事件类型分发。 - 事件可并发只读:事件指针创建后只读,多个线程可并发读取同一事件的不同字段(各自提供独立的
out_ptr/out_len存储)。 - 释放须独占:
free_dora_context与free_dora_event转移所有权,调用时须保证没有其他线程正在使用该上下文/事件。
这些契约是 dora-rs 对 C API 审计(见头文件注释引用的 issue)后固化下来的,跨 FFI 边界写并发代码前务必通读。
三、Operator API 详解(dora-operator-api-c)
Operator API 面向加载进 Dora 运行时进程的共享库。Operator没有main函数,而是导出三个生命周期函数供运行时在合适的时机回调。头文件 apis/c/operator/operator_api.h 用EXPORT宏(Windows 为__declspec(dllexport),其余平台为visibility("default"))标记导出符号,并包裹extern "C";apis/c/operator/operator_types.h 由safer-ffi自动生成(文件头注明"File auto-generated by::safer_ffi"),定义全部 C 兼容的结构体与枚举。
3.1 生命周期函数
dora_init_operator——运行时加载 Operator 时调用一次:
DoraInitResult_t dora_init_operator(void);在此分配并初始化 Operator 状态,通过operator_context字段返回。运行时会在后续每次调用中把该指针回传。成功时返回result.error == NULL的DoraInitResult_t。
dora_drop_operator——Operator 被卸载时调用一次:
DoraResult_t dora_drop_operator(void *operator_context);释放与operator_context关联的全部资源。成功时返回.error == NULL的DoraResult_t。
3.2 事件处理
dora_on_event
OnEventResult_t dora_on_event( RawEvent_t *event, const SendOutput_t *send_output, void *operator_context );每次有事件到达该 Operator 时由运行时调用。通过检查event各字段判断事件类型:
| 字段条件 | 含义 |
|---|---|
event->input != NULL | 有新输入可用 |
event->stop == true | 请求优雅停机 |
event->error.ptr != NULL | 发生错误(UTF-8 字符串在error.ptr/error.len) |
event->input_closed.ptr != NULL | 某个输入流关闭(输入 ID 在input_closed.ptr/input_closed.len) |
使用send_output向下游发送数据(见dora_send_operator_output),并通过返回OnEventResult_t中适当的DoraStatus_t控制 Operator 生命周期。注意RawEvent_t中多个字段可能同时置位,应按优先级顺序检查。
3.3 读取输入
dora_read_input_id
char *dora_read_input_id(const Input_t *input);返回新建分配、以 NUL 结尾的输入 ID 字符串,调用者必须用dora_free_input_id释放。
dora_read_data
Vec_uint8_t dora_read_data(Input_t *input);将输入数据读为字节数组。该操作会消费底层 Arrow 数组——每个事件的数据只能读取一次,重复读取返回.ptr = NULL并记录"double read"提示;输入无数据或载荷类型不受原始字节 API 支持(API 只读UInt8载荷)时同样返回.ptr = NULL。返回的数据须用dora_free_data释放。
3.4 发送输出
dora_send_operator_output
DoraResult_t dora_send_operator_output( const SendOutput_t *send_output, const char *id, const uint8_t *data_ptr, size_t data_len );向下游订阅者发送输出。id须为 NUL 结尾字符串,且与 Operator 声明的某个输出匹配;data_ptr/data_len在内部转换为 UInt8 Arrow 数组。成功返回.error == NULL的DoraResult_t。与 Node API 对应地,(NULL, 0)空载荷是合法惯用法(类型头文件中的文档注释明确说明了这一契约,并指出它补齐了 2026-04-08 unsafe 审计发现的空指针检查缺口)。
3.5 内存管理规则
Operator API 分配的内存必须用对应的函数释放,这是本 API 最容易踩坑的地方:
| 分配来源 | 释放函数 |
|---|---|
dora_read_input_id | dora_free_input_id |
dora_read_data | dora_free_data |
void dora_free_input_id(char *input_id); void dora_free_data(Vec_uint8_t data);严禁对上述分配调用free()——它们由 Rust 运行时分配,必须通过 API 释放,否则会泄漏或破坏运行时内存布局。
3.6 核心结构体
Vec_uint8_t——Rust 分配的字节向量:
typedef struct Vec_uint8 { uint8_t *ptr; size_t len; size_t cap; } Vec_uint8_t;访问从ptr开始的len个字节即可,不要修改cap,用dora_free_data释放。
DoraResult_t——通用结果类型:
typedef struct DoraResult { Vec_uint8_t *error; // NULL on success, points to error string on failure } DoraResult_t;error为NULL表示成功;非NULL时指向含 UTF-8 错误消息的向量。
DoraInitResult_t——dora_init_operator的返回值:
typedef struct DoraInitResult { DoraResult_t result; void *operator_context; // opaque pointer to operator state } DoraInitResult_t;成功时result.error == NULL,operator_context保存 Operator 状态指针。
OnEventResult_t——dora_on_event的返回值:
typedef struct OnEventResult { DoraResult_t result; DoraStatus_t status; } OnEventResult_t;同时包含成败结果与控制生命周期的状态码。
RawEvent_t——送达 Operator 的事件:
typedef struct RawEvent { Input_t *input; // non-NULL when this is an input event Vec_uint8_t input_closed; // non-empty when an input stream closed bool stop; // true when shutdown is requested Vec_uint8_t error; // non-empty on error } RawEvent_t;多个字段可能同时置位,需按优先级顺序检查。
Input_t/Output_t——两个不透明类型:Input_t表示输入事件的数据,用dora_read_input_id/dora_read_data提取内容;Output_t仅供dora_send_operator_output内部使用,用户代码不直接创建。
SendOutput_t——传给dora_on_event的回调句柄:
typedef struct SendOutput { ArcDynFn1_DoraResult_Output_t send_output; } SendOutput_t;把它传给dora_send_operator_output即可发送数据。不要将其保存超过当前dora_on_event调用的作用域。
Metadata_t——事件元数据:
typedef struct Metadata { Vec_uint8_t open_telemetry_context; } Metadata_t;包含 OpenTelemetry 追踪上下文字符串。
3.7 DoraStatus_t 枚举
enum DoraStatus { DORA_STATUS_CONTINUE = 0, // Keep running DORA_STATUS_STOP = 1, // Stop this operator DORA_STATUS_STOP_ALL = 2, // Stop the entire dataflow }; typedef uint8_t DoraStatus_t;在OnEventResult_t中返回,用于处理完事件后控制 Operator 生命周期:继续运行、停止本 Operator,或停止整个数据流。
四、完整实战:C 节点 + C Operator + 数据流
仓库中的 examples/c-dataflow 是上述两套 API 的完整可运行工程,包含节点、Operator、Sink 三个 C 文件与一份编排三者的 dataflow 配置,下面逐段拆解。
4.1 C 节点(node.c)
参考 examples/c-dataflow/node.c 的骨架:
#include <stdio.h> #include <string.h> #include "node_api.h" int main() { void *dora_context = init_dora_context_from_env(); if (dora_context == NULL) { fprintf(stderr, "failed to init dora context\n"); return 1; } for (int i = 0; i < 100; i++) { void *event = dora_next_event(dora_context); if (event == NULL) break; // all streams closed enum DoraEventType ty = read_dora_event_type(event); if (ty == DoraEventType_Input) { char *id; size_t id_len; read_dora_input_id(event, &id, &id_len); // Send a response char out_id[] = "message"; char out_data[64]; int out_len = snprintf(out_data, sizeof(out_data), "iteration %d", i); dora_send_output(dora_context, out_id, strlen(out_id), out_data, out_len); } else if (ty == DoraEventType_Stop) { free_dora_event(event); break; } free_dora_event(event); } free_dora_context(dora_context); return 0; }核心模式:初始化上下文 → 事件循环(dora_next_event)→ 按类型分发 → 发送输出 → 释放事件 → 最终释放上下文。仓库工程中的sink.c(examples/c-dataflow/sink.c)还演示了InputClosed分支与用fwrite(id, id_len, 1, stdout)按长度打印非 NUL 结尾 ID 的正确姿势。
4.2 C Operator(operator.c)
参考 examples/c-dataflow/operator.c 的完整三函数实现:
#include "operator_api.h" #include <stdio.h> #include <stdlib.h> #include <string.h> DoraInitResult_t dora_init_operator(void) { // Allocate operator state (a simple counter) int *counter = (int *)calloc(1, sizeof(int)); DoraInitResult_t result = {.operator_context = counter}; return result; } DoraResult_t dora_drop_operator(void *operator_context) { free(operator_context); DoraResult_t result = {.error = NULL}; return result; } OnEventResult_t dora_on_event( RawEvent_t *event, const SendOutput_t *send_output, void *operator_context) { OnEventResult_t result = {.status = DORA_STATUS_CONTINUE}; int *counter = (int *)operator_context; if (event->input != NULL) { char *id = dora_read_input_id(event->input); Vec_uint8_t data = dora_read_data(event->input); if (data.ptr != NULL) { *counter += 1; printf("received input '%s', counter: %d\n", id, *counter); // Send counter value as string char buf[64]; int len = snprintf(buf, sizeof(buf), "count=%d", *counter); result.result = dora_send_operator_output( send_output, "counter", (uint8_t *)buf, len); dora_free_data(data); } dora_free_input_id(id); } if (event->stop) { result.status = DORA_STATUS_STOP; } return result; }注意示例中体现的四条铁律:状态经operator_context跨事件保持;dora_read_data返回的Vec_uint8_t用完立即dora_free_data;dora_read_input_id返回的字符串用完dora_free_input_id;即使data.ptr == NULL(空载荷)也要释放id。仓库工程中还用strcmp(id, "message") == 0按输入 ID 分流,展示了多输入 Operator 的常见写法。
4.3 数据流编排(dataflow.yml)
示例的完整编排 examples/c-dataflow/dataflow.yml 将三者串联成流水线:
nodes: - id: c_node path: build/c_node inputs: timer: dora/timer/millis/50 outputs: - message - id: runtime-node operators: - id: c_operator shared-library: build/operator inputs: message: c_node/message outputs: - counter - id: c_sink path: build/c_sink inputs: counter: runtime-node/c_operator/counter要点解读:
c_node是外部 C 进程,path指向编译出的可执行文件;它的输入是 Dora 内置的周期定时器dora/timer/millis/50(每 50 ms 触发一次),输出为message。runtime-node内嵌 C Operator,shared-library: build/operator指向共享库;Operator 的输入message引用c_node/message,输出counter被c_sink订阅。c_sink是终端 C 进程,消费runtime-node/c_operator/counter。- 输出引用采用
node_id/output_id、算子输出引用采用node_id/operator_id/output_id的层级路径。
4.4 一键构建与运行
examples/c-dataflow提供的 run.rs 自动化了完整流程:先cargo build --package dora-node-api-c与dora-operator-api-c,再用 clang 编译node.c、sink.c为可执行文件、编译链接operator.c为共享库(自动适配 Linux/macOS/Windows 的链接参数与 DLL 前后缀),最后通过RunCommand执行dataflow.yml。手动跑通该示例可参考其脚本逻辑:
cargo build -p dora-node-api-c --release cargo build -p dora-operator-api-c --release随后按下一节的方式编译 C 文件。
五、构建与链接指南
5.1 节点(静态库链接)
C 节点链接dora-node-api-c,该 crate 构建为静态库。
第 1 步:构建静态库
cargo build -p dora-node-api-c --release产物为target/release/libdora_node_api_c.a(Windows 上为.lib)。
第 2 步:编译并链接
clang node.c -ldora_node_api_c -L ../../target/release -o build/c_node <FLAGS>平台相关链接参数:
| 平台 | Flags |
|---|---|
| Linux | -lm -lrt -ldl -pthread |
| macOS | -framework CoreServices -framework Security -lSystem -lresolv -lpthread -lc -lm |
| Windows | -ladvapi32 -luserenv -lkernel32 -lws2_32 -lbcrypt -lncrypt -lschannel -lntdll -liphlpapi -lcfgmgr32 -lcredui -lcrypt32 -lcryptnet -lfwpuclnt -lgdi32 -lmsimg32 -lmswsock -lole32 -lopengl32 -lsecur32 -lshell32 -lsynchronization -luser32 -lwinspool -Wl,-nodefaultlib:libcmt -D_DLL -lmsvcrt |
Windows 上输出文件需加.exe扩展名。仓库 examples/c-dataflow/run.rs 的构建逻辑还额外验证了 Linux 下追加-lz、Windows 下追加oleaut32/winhttp/rpcrt4等依赖,当链接期报符号缺失时可对照参考。
5.2 Operator(共享库)
C Operator 编译为共享库,由 Dora 运行时在启动时加载。
第 1 步:编译为目标文件
clang -c operator.c -o build/operator.o -fdeclspec -fPICWindows 上省略-fPIC。
第 2 步:链接为共享库
# Linux clang -shared build/operator.o -o build/liboperator.so # macOS clang -shared build/operator.o -o build/liboperator.dylib # Windows clang -shared build/operator.o -o build/operator.dll注意 Windows 上还需链接dora_operator_api_c及平台系统库(可参照 examples/c-dataflow/run.rs 中的 Windows 分支)。
第 3 步:在 dataflow YAML 中引用
operators: - id: c_operator shared-library: build/operator # without lib prefix or extension inputs: data: source/output outputs: - resultshared-library路径省略平台前缀(lib)与扩展名(.so/.dylib/.dll),运行时按当前平台解析实际文件。
5.3 Include 路径
- Node API 头文件:apis/c/node/node_api.h
- Operator API 头文件:apis/c/operator/operator_api.h 与 apis/c/operator/operator_types.h
# Node clang -I path/to/dora/apis/c/node node.c ... # Operator clang -I path/to/dora/apis/c/operator operator.c ...5.4 C++ 兼容性
两套头文件均可在 C++ 源码中直接#include:Operator 头文件使用extern "C"防护(见 apis/c/operator/operator_api.h 的__cplusplus分支),Node 头文件则采用纯 C 兼容声明。仓库中的 examples/c++-dataflow 与 examples/cmake-dataflow 展示了在 C++ 工程中同时使用 Node API(node-rust-api目录)与 Operator API(operator-rust-api目录)的完整形态,CMake 集成可参考其中的DoraTargets.cmake。
六、从源码理解 API 设计要点
结合 apis/c/node/src/lib.rs、apis/c/operator/operator_types.h 等实现,可以总结出四条贯穿两套 C API 的设计主线:
- 所有权显式化:Node API 采用"借用"模型(读函数返回的指针属于事件,随
free_dora_event失效),Operator API 采用"转移"模型(读函数返回全新分配,须配对释放)。这是两套 API 最本质的差异,混用两套心智模型是常见的 bug 来源。 - 错误不跨 FFI 边界 panic:Node 侧
dora_send_output对非法输出 ID、data_slice对空指针非零长度等场景都显式返回-1而非 unwind;Operator 侧同样以DoraResult_t承载错误。C 调用者应始终检查返回值/error字段。 - 类型能力边界清晰:原始字节 API 目前只暴露
UInt8(与空Null)载荷,其他 Arrow 类型在 Node 侧返回"无数据",在 Operator 侧返回dora_read_data的None(.ptr == NULL)。跨语言传Int32/Float等类型时,需要在上游先序列化为字节,或等待未来版本引入 Arrow C Data Interface 的完整类型支持。 safer-ffi驱动 ABI:Operator 的头文件与结构体布局由safer-ffi自动生成并保证与 Rust 侧#[repr(C)]一致,手动修改生成文件会导致 ABI 错位,这也是operator_types.h顶部注明"Do not manually edit this file"的原因。
七、延伸阅读
- 完整的 C API 参考文档副本:docs/api-c.md
- 可运行示例工程:examples/c-dataflow(含 node.c、operator.c、sink.c、dataflow.yml 与 run.rs)
- C++ 场景扩展:examples/c++-dataflow、examples/cmake-dataflow
- 跨语言数据流示例:examples/cross-language(Rust 与 Python 节点互发消息)
- 其他语言 API:Rust 见 apis/rust/node 与 apis/rust/operator,Python 见 apis/python/node
- Dora 整体架构与数据流概念:docs/architecture.md、docs/extensions.md
【免费下载链接】doraDORA (Dataflow-Oriented Robotic Architecture) is middleware designed to streamline and simplify the creation of AI-based robotic applications. It offers low latency, composable, and distributed dataflow capabilities. Applications are modeled as directed graphs, also referred to as pipelines.项目地址: https://gitcode.com/GitHub_Trending/do/dora
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考