Rust跨语言FFI工具库diplomat_core的使用:简化Rust与其他语言交互的代码生成与绑定

Rust跨语言FFI工具库diplomat_core的使用:简化Rust与其他语言交互的代码生成与绑定

简介

Diplomat是一个实验性的Rust工具,用于生成FFI(外部函数接口)定义,允许其他语言调用Rust代码。使用Diplomat,您可以简单地定义要通过FFI公开的Rust API,并自动获得高级的C、C++和JavaScript绑定!

Diplomat支持从Rust生成以下语言的绑定:

  • C
  • C++
  • Dart
  • JavaScript/TypeScript
  • Kotlin (使用JNA)
  • Python (使用nanobind)

安装

首先安装用于生成绑定的CLI工具:

$ cargo install diplomat-tool

然后将Diplomat宏和运行时添加为项目的依赖项:

diplomat = "0.10.0"
diplomat-runtime = "0.10.0"

完整示例

以下是一个使用Diplomat生成跨语言绑定的完整示例:

  1. 首先创建一个Rust库项目:
$ cargo new --lib my_ffi_lib
$ cd my_ffi_lib
  1. 编辑Cargo.toml添加依赖:
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
diplomat = "0.10.0"
diplomat-runtime = "0.10.0"
  1. 创建src/lib.rs定义API:
use diplomat_core::diplomat_runtime::{DiplomatResult, DiplomatWriteable};
use std::fmt::Write;

#[diplomat::bridge]
pub mod ffi {
    use super::*;
    
    #[diplomat::opaque]
    pub struct MyString(String);

    impl MyString {
        /// 创建一个新的MyString
        pub fn new(s: &str) -> Box<MyString> {
            Box::new(MyString(s.to_string()))
        }

        /// 获取字符串长度
        pub fn len(&self) -> usize {
            self.0.len()
        }

        /// 将字符串内容写入提供的Writeable
        pub fn write(&self, w: &mut DiplomatWriteable) -> DiplomatResult<(), ()> {
            write!(w, "{}", self.0).map_err(|_| ())
        }
    }

    /// 计算两个数的和
    pub fn add_numbers(a: i32, b: i32) -> i32 {
        a + b
    }
}
  1. 生成绑定代码:
$ diplomat-tool c ./generated/c --docs ./generated/docs
$ diplomat-tool cpp ./generated/cpp --docs ./generated/docs
  1. 现在可以在其他语言中使用生成的绑定了:

C++示例:

#include <iostream>
#include "generated/cpp/my_ffi_lib.hpp"

int main() {
    auto str = my_ffi_lib::MyString::new_("Hello from Rust!");
    std::cout << "String length: " << str->len() << std::endl;
    
    int sum = my_ffi_lib::add_numbers(5, 7);
    std::cout << "5 + 7 = " << sum << std::endl;
    
    return 0;
}

架构与测试

Diplomat使用快照测试来检查宏和代码生成逻辑。当代码生成逻辑更改需要更新快照时,运行cargo insta review(运行cargo install cargo-insta获取工具)查看更改并更新快照。

JavaScript/WASM支持

对于wasm32-unknown-unknown目标的JavaScript绑定,有两个选项:

  1. 使用nightly Rust并启用-Zwasm-c-abi=spec标志
  2. 配置JS后端使用旧版绑定

总结

Diplomat通过以下方式简化了Rust与其他语言的交互:

  1. 提供简单的属性宏来标记要导出的API
  2. 自动生成类型安全的绑定代码
  3. 支持多种目标语言
  4. 处理复杂的FFI边界问题

这使得在Rust中编写高性能核心逻辑,同时提供多语言接口变得非常简单。

完整示例demo

基于上述内容,这里提供一个更完整的Rust FFI示例:

  1. 首先创建项目结构:
cargo new --lib rust_ffi_demo
cd rust_ffi_demo
mkdir -p generated/{c,cpp}
  1. 编辑Cargo.toml:
[package]
name = "rust_ffi_demo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
diplomat = "0.10.0"
diplomat-runtime = "0.10.0"
serde = { version = "1.0", features = ["derive"] }
  1. 创建src/lib.rs:
use diplomat_core::diplomat_runtime::{DiplomatResult, DiplomatWriteable};
use std::fmt::Write;
use serde::{Serialize, Deserialize};

#[diplomat::bridge]
pub mod ffi {
    use super::*;
    
    #[diplomat::opaque]
    #[derive(Serialize, Deserialize)]
    pub struct Person {
        name: String,
        age: u32,
    }

    impl Person {
        /// 创建一个新的Person
        pub fn new(name: &str, age: u32) -> Box<Person> {
            Box::new(Person {
                name: name.to_string(),
                age,
            })
        }

        /// 获取年龄
        pub fn get_age(&self) -> u32 {
            self.age
        }

        /// 将个人信息转为JSON字符串
        pub fn to_json(&self, w: &mut DiplomatWriteable) -> DiplomatResult<(), ()> {
            let json = serde_json::to_string(self).map_err(|_| ())?;
            write!(w, "{}", json).map_err(|_| ())
        }

        /// 从JSON字符串创建Person
        pub fn from_json(json: &str) -> DiplomatResult<Box<Person>, ()> {
            serde_json::from_str(json)
                .map(Box::new)
                .map_err(|_| ())
        }
    }

    /// 计算斐波那契数列
    pub fn fibonacci(n: u32) -> u64 {
        if n == 0 {
            return 0;
        }
        let mut a = 0;
        let mut b = 1;
        for _ in 1..n {
            let c = a + b;
            a = b;
            b = c;
        }
        b
    }
}
  1. 生成绑定代码:
diplomat-tool c ./generated/c
diplomat-tool cpp ./generated/cpp
  1. C++调用示例 (main.cpp):
#include <iostream>
#include "generated/cpp/rust_ffi_demo.hpp"

int main() {
    // 创建Person对象
    auto person = rust_ffi_demo::Person::new_("Alice", 30);
    std::cout << "Age: " << person->get_age() << std::endl;
    
    // 序列化为JSON
    rust_ffi_demo::DiplomatWriteable writeable;
    person->to_json(&writeable);
    std::cout << "JSON: " << writeable.to_string_view() << std::endl;
    
    // 从JSON反序列化
    auto json_str = "{\"name\":\"Bob\",\"age\":25}";
    auto new_person = rust_ffi_demo::Person::from_json(json_str).unwrap();
    std::cout << "New person age: " << new_person->get_age() << std::endl;
    
    // 计算斐波那契数列
    std::cout << "Fibonacci(10): " << rust_ffi_demo::fibonacci(10) << std::endl;
    
    return 0;
}
  1. 编译和运行:
# 编译Rust库
cargo build --release

# 编译C++程序 (假设使用g++)
g++ -std=c++17 -I./generated/cpp main.cpp -L./target/release -lrust_ffi_demo -o demo

# 运行程序
LD_LIBRARY_PATH=./target/release ./demo

这个完整示例展示了:

  • 定义复杂数据结构(Person)
  • 使用serde进行JSON序列化/反序列化
  • 导出方法和函数到C++
  • 处理字符串和错误返回
  • 完整的构建和运行流程

1 回复

Rust跨语言FFI工具库diplomat_core使用指南

介绍

diplomat_core是一个用于简化Rust与其他语言交互的代码生成工具库,它专注于提供安全、高效的跨语言FFI(外部函数接口)解决方案。该工具由Rust核心团队开发,旨在解决传统FFI绑定中的复杂性和安全性问题。

主要特点:

  • 自动生成多种语言的绑定代码
  • 提供类型安全的接口
  • 减少手动编写FFI代码的工作量
  • 支持多种目标语言(如C, C++, JavaScript等)
  • 内置内存安全保证

安装

首先在Cargo.toml中添加依赖:

[dependencies]
diplomat_core = "0.7"

基本使用方法

1. 定义接口

创建一个Rust模块并使用diplomat::bridge宏标记需要导出的API:

#[diplomat::bridge]
mod ffi {
    pub struct MyStruct {
        value: i32,
    }

    impl MyStruct {
        pub fn new(value: i32) -> MyStruct {
            MyStruct { value }
        }

        pub fn get_value(&self) -> i32 {
            self.value
        }

        pub fn double(&mut self) {
            self.value *= 2;
        }
    }
}

2. 生成绑定代码

使用Diplomat CLI工具生成目标语言的绑定代码:

cargo install diplomat-tool
diplomat-tool gen --language=cpp --out=bindings/

3. 使用生成的绑定

C++示例

#include "bindings/my_struct.hpp"
#include <iostream>

int main() {
    auto my_struct = my_namespace::MyStruct::new_(42);
    std::cout << "Value: " << my_struct.get_value() << std::endl;
    
    my_struct.double();
    std::cout << "Doubled: " << my_struct.get_value() << std::endl;
    
    return 0;
}

JavaScript示例

const { MyStruct } = require('./bindings/my_struct');

const myStruct = MyStruct.new(42);
console.log(`Value: ${myStruct.getValue()}`);

myStruct.double();
console.log(`Doubled: ${myStruct.getValue()}`);

高级功能

1. 错误处理

#[diplomat::bridge]
mod ffi {
    #[derive(Debug)]
    pub enum MyError {
        InvalidInput,
        OperationFailed,
    }

    impl MyStruct {
        pub fn try_operation(&self) -> Result<(), MyError> {
            if self.value < 0 {
                Err(MyError::InvalidInput)
            } else {
                Ok(())
            }
        }
    }
}

2. 复杂类型支持

#[diplomat::bridge]
mod ffi {
    pub struct ComplexData {
        data: Vec<String>,
    }

    impl ComplexData {
        pub fn new() -> Self {
            ComplexData { data: Vec::new() }
        }

        pub fn add_item(&mut self, item: &str) {
            self.data.push(item.to_string());
        }

        pub fn get_items(&self) -> Vec<&str> {
            self.data.iter().map(|s| s.as_str()).collect()
        }
    }
}

3. 回调函数

#[diplomat::bridge]
mod ffi {
    pub type Callback = Box<dyn Fn(i32) -> i32>;

    impl MyStruct {
        pub fn apply_callback(&self, callback: Callback) -> i32 {
            callback(self.value)
        }
    }
}

配置选项

可以在项目的diplomat.toml中配置生成选项:

[bindings.cpp]
namespace = "my_project"
include_prefix = "my_project"

[bindings.js]
module_name = "my-project"

最佳实践

  1. 保持FFI接口简单,复杂的逻辑应该在Rust侧实现
  2. 尽量减少跨语言边界的数据传输
  3. 使用Diplomat提供的类型而不是原始类型
  4. 为所有可能失败的操作提供明确的错误处理
  5. 编写跨语言测试确保绑定正常工作

限制

  1. 目前仍在积极开发中,API可能会有变化
  2. 某些高级Rust特性可能不受支持
  3. 性能敏感场景可能需要手动优化

通过使用diplomat_core,开发者可以大大简化Rust与其他语言交互的复杂性,同时保持类型安全和内存安全。

完整示例Demo

下面是一个完整的Rust项目示例,展示如何使用diplomat_core创建跨语言FFI绑定:

Rust项目结构

my_ffi_project/
├── Cargo.toml
├── diplomat.toml
├── src/
│   ├── lib.rs
└── bindings/

Cargo.toml

[package]
name = "my_ffi_project"
version = "0.1.0"
edition = "2021"

[dependencies]
diplomat_core = "0.7"

diplomat.toml

[bindings.cpp]
namespace = "my_ffi"
include_prefix = "my_ffi"

[bindings.js]
module_name = "my-ffi"

src/lib.rs

#[diplomat::bridge]
mod ffi {
    pub struct Calculator {
        value: f64,
    }

    impl Calculator {
        pub fn new(initial: f64) -> Self {
            Calculator { value: initial }
        }

        pub fn add(&mut self, x: f64) {
            self.value += x;
        }

        pub fn subtract(&mut self, x: f64) {
            self.value -= x;
        }

        pub fn multiply(&mut self, x: f64) {
            self.value *= x;
        }

        pub fn divide(&mut self, x: f64) -> Result<(), CalcError> {
            if x == 0.0 {
                Err(CalcError::DivisionByZero)
            } else {
                self.value /= x;
                Ok(())
            }
        }

        pub fn get_result(&self) -> f64 {
            self.value
        }
    }

    #[derive(Debug)]
    pub enum CalcError {
        DivisionByZero,
        InvalidOperation,
    }
}

生成绑定

cargo install diplomat-tool
diplomat-tool gen --language=cpp --out=bindings/
diplomat-tool gen --language=js --out=bindings/

C++使用示例

#include "bindings/calculator.hpp"
#include <iostream>

int main() {
    auto calc = my_ffi::Calculator::new_(10.0);
    
    calc.add(5.0);
    calc.multiply(2.0);
    
    if (auto result = calc.divide(3.0); result.is_err()) {
        std::cerr << "Calculation error!" << std::endl;
        return 1;
    }
    
    std::cout << "Result: " << calc.get_result() << std::endl;
    return 0;
}

JavaScript使用示例

const { Calculator, CalcError } = require('./bindings/calculator');

const calc = Calculator.new(10.0);

calc.add(5.0);
calc.multiply(2.0);

try {
    calc.divide(3.0);
    console.log(`Result: ${calc.getResult()}`);
} catch (e) {
    if (e === CalcError.DivisionByZero) {
        console.error("Cannot divide by zero!");
    } else {
        console.error("Calculation error!");
    }
}

这个完整示例展示了如何创建一个简单的计算器FFI接口,并在C++和JavaScript中使用它,包括错误处理功能。

回到顶部