多语言
多语言能力让你可以继续用 Rust 组织系统,同时把部分节点逻辑放到 C++/Python 实现。
一句话流程
Rust 声明镜像 → two-pass 提取与生成 → 本地后端编译 → 统一链接。
多语言节点(puppet)
在 Rust 侧声明节点镜像:
#[roplat::node(
lang = "cpp",
input(sensor_input, SensorData),
output(motor_output, MotorCommand),
)]
pub struct CppController {
pub gain: f64,
pub offset: f64,
}
生成结果:
- 系统文件(每次可覆盖)
- 用户文件(仅首次生成,后续不覆盖)
- C++ FFI 桥接源码
参数化构造器
puppet 节点的 __roplat_ptr 字段为私有,无法在模块外使用结构体更新语法。推荐在 puppet 模块内提供 with_*() 构造器:
impl CppController {
pub fn new() -> Self {
Self { gain: 1.0, offset: 0.0, ..Self::default() }
}
pub fn with_gain(gain: f64) -> Self {
Self { gain, ..Self::default() }
}
}
系统 DSL 中直接使用:
C++ 字段同步(set_fields FFI)
问题
C++ 节点通过 FFI create() 函数创建对象,默认初始化所有字段为零值。当 Rust 侧 puppet 持有非默认字段值时,C++ 对象的字段与 Rust 不一致。
方案
build.rs 代码生成阶段为每个 C++ 节点额外生成 set_fields FFI 函数:
// roplat_gen/<node>_ffi.cpp(自动生成)
extern "C" void roplat_node_cpp_controller_set_fields(
void* ptr, double gain, double offset
) {
auto* n = static_cast<CppController*>(ptr);
n->setGain(gain);
n->setOffset(offset);
}
puppet 宏在 on_init 生命周期中自动调用:
create分配 C++ 对象set_fields将 Rust 侧字段值同步到 C++ 对象init调用用户初始化逻辑
相关源码
- codegen.rs
write_cpp_ffi():生成set_fieldsextern "C" 函数 - codegen.rs
write_cpp_base():在基类头文件声明 FFI 函数 - puppet.rs:生成
set_fields_decl(FFI 声明)和set_fields_call(on_init 调用)
Python 运行时桥接(PyO3)
架构
Python 节点通过 PyO3 在 Rust 进程内嵌入 CPython 解释器。puppet 宏为每个 lang = "py" 节点生成完整的桥接代码。
生命周期
| 阶段 | 动作 |
|---|---|
on_init |
获取 GIL → import Python 模块 → 实例化 Python 对象 → setattr 同步字段 → 调用 init() |
process |
序列化 Rust Input → from_buffer_copy 构造 ctypes 结构体 → 调用 Python process() → 提取 Output 字段 |
on_shutdown |
调用 Python shutdown() → 释放 Python 对象引用 |
透明数据传递
Python 的 process() 接收 ctypes 结构体(与 Rust #[repr(C)] 内存布局一致),通过 from_buffer_copy 零拷贝映射。返回值逐字段提取回 Rust。
不透明数据传递
Python 内部不透明类型通过 py_bridge::store_opaque / take_opaque 使用 thread-local 存储传递。Python 对象不穿越 Rust 边界,仅在同线程的 Python 节点间共享。
字段同步
Python 节点的字段同步在 on_init 中通过 setattr 完成——puppet 宏为每个 Rust 字段生成对应的 setattr 调用,将 Rust 值推送到 Python 实例属性。
多语言数据
见 通讯。
这里强调两点:
- 透明类型用于跨语言布局协作。
- 不透明类型在 Rust 侧默认仅保留类型标记,目标语言用户文件负责真实实现。
cc 与 cmake 两种后端
通过环境变量选择后端:
# 默认 cc
cargo build -p multi_lang
# 切换 cmake
$env:ROPLAT_NATIVE_BACKEND='cmake'
cargo build -p multi_lang
选择建议:
- 简单桥接、依赖少:
cc - 依赖复杂(Eigen/OpenCV 等):
cmake
示例矩阵
| 示例 | 路径 | 描述 |
|---|---|---|
| chain_mixed | examples/chain_mixed |
Rust→C++→C++→Python→Python→Rust 全链路 |
| rs_cpp | examples/multi_lang/examples/rs_cpp.rs |
Rust↔C++ 透明数据 |
| rs_py | examples/multi_lang/examples/rs_py.rs |
Rust↔Python 透明数据 |
| cpp_opaque | examples/multi_lang/examples/cpp_opaque.rs |
C++ 内部不透明数据 |
| py_opaque | examples/multi_lang/examples/py_opaque.rs |
Python 内部不透明数据 |
| cpp_to_py | examples/multi_lang/examples/cpp_to_py.rs |
C++→Python 跨语言 |
| py_to_cpp | examples/multi_lang/examples/py_to_cpp.rs |
Python→C++ 跨语言 |
常见问题
为什么用户文件默认不覆盖?
为了保证你写的业务逻辑不被重新生成覆盖。
为什么 opaque 不再在 Rust 定义字段?
为了避免"Rust 与目标语言双重真相",减少语义冲突与维护成本。
C++/Python 节点为什么还要 Rust 壳?
因为系统调度、类型约束与拓扑编排仍由 Rust 统一管理。
为什么 C++ 需要 set_fields 而 Python 用 setattr?
C++ 对象通过 FFI 创建,字段访问只能经过 setter 方法。Python 通过 GIL 直接 setattr,语义更自然。
在 Rust 侧声明节点镜像:
#[roplat::node(
lang = "cpp",
input(sensor_input, SensorData),
output(motor_output, MotorCommand),
)]
pub struct CppController {
pub gain: f64,
pub offset: f64,
}
生成结果:
- 系统文件(每次可覆盖)
- 用户文件(仅首次生成,后续不覆盖)
- C++ FFI 桥接源码
多语言数据
见 通讯。
这里强调两点:
- 透明类型用于跨语言布局协作。
- 不透明类型在 Rust 侧默认仅保留类型标记,目标语言用户文件负责真实实现。
cc 与 cmake 两种后端
通过环境变量选择后端:
# 默认 cc
cargo build -p multi_lang
# 切换 cmake
$env:ROPLAT_NATIVE_BACKEND='cmake'
cargo build -p multi_lang
选择建议:
- 简单桥接、依赖少:
cc - 依赖复杂(Eigen/OpenCV 等):
cmake
常见问题
- 为什么用户文件默认不覆盖
为了保证你写的业务逻辑不被重新生成覆盖。
- 为什么 opaque 不再在 Rust 定义字段
为了避免“Rust 与目标语言双重真相”,减少语义冲突与维护成本。
- C++/Python 节点为什么还要 Rust 壳
因为系统调度、类型约束与拓扑编排仍由 Rust 统一管理。