跳转至

多语言

多语言能力让你可以继续用 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,
}

生成结果:

  1. 系统文件(每次可覆盖)
  2. 用户文件(仅首次生成,后续不覆盖)
  3. 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 中直接使用:

let mut ctrl = puppet::CppController::with_gain(2.0);

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 → set_fields → init
  1. create 分配 C++ 对象
  2. set_fields 将 Rust 侧字段值同步到 C++ 对象
  3. init 调用用户初始化逻辑

相关源码

  • codegen.rs write_cpp_ffi():生成 set_fields extern "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 实例属性。

多语言数据

通讯

这里强调两点:

  1. 透明类型用于跨语言布局协作。
  2. 不透明类型在 Rust 侧默认仅保留类型标记,目标语言用户文件负责真实实现。

cccmake 两种后端

通过环境变量选择后端:

# 默认 cc
cargo build -p multi_lang

# 切换 cmake
$env:ROPLAT_NATIVE_BACKEND='cmake'
cargo build -p multi_lang

选择建议:

  1. 简单桥接、依赖少:cc
  2. 依赖复杂(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,
}

生成结果:

  1. 系统文件(每次可覆盖)
  2. 用户文件(仅首次生成,后续不覆盖)
  3. C++ FFI 桥接源码

多语言数据

通讯

这里强调两点:

  1. 透明类型用于跨语言布局协作。
  2. 不透明类型在 Rust 侧默认仅保留类型标记,目标语言用户文件负责真实实现。

cccmake 两种后端

通过环境变量选择后端:

# 默认 cc
cargo build -p multi_lang

# 切换 cmake
$env:ROPLAT_NATIVE_BACKEND='cmake'
cargo build -p multi_lang

选择建议:

  1. 简单桥接、依赖少:cc
  2. 依赖复杂(Eigen/OpenCV 等):cmake

常见问题

  1. 为什么用户文件默认不覆盖

为了保证你写的业务逻辑不被重新生成覆盖。

  1. 为什么 opaque 不再在 Rust 定义字段

为了避免“Rust 与目标语言双重真相”,减少语义冲突与维护成本。

  1. C++/Python 节点为什么还要 Rust 壳

因为系统调度、类型约束与拓扑编排仍由 Rust 统一管理。