Rust 调用 C++ 之动态链接
下面讲解 Rust 如何以动态链接的方式调用 C++ 中的方法,以及注意事项。
由于 Rust 和 C++ 之间存在 ABI (Application Binary Interface) 兼容性问题,特别是 C++ 的 name mangling (名称修饰) 和复杂的特性(如类、模板、异常等),Rust 不能直接调用 C++ 的方法。
核心思路是:
- C++ 侧:将需要暴露给 Rust 的 C++ 功能封装在一个或多个
extern "C"
的 C 风格函数中。这些 C 函数将作为两种语言之间的桥梁。 - 编译 C++:将 C++ 代码编译成一个动态链接库(在 Windows 上是
.dll
,在 Linux 上是.so
,在 macOS 上是.dylib
)。 - Rust 侧:使用
libloading
等库,在运行时动态加载这个库,获取 C 风格函数的指针,然后通过这个指针来调用 C++ 的功能。
下面我们通过一个具体的示例来演示这个过程。
# 示例场景
我们将在 C++ 中创建一个简单的计算器类 Calculator
,它有一个 add
方法。然后,我们将在 Rust 中加载这个 C++ 动态库,并调用 add
方法来计算两个数的和。
# 第 1 步:创建 C++ 动态链接库
首先,我们需要创建 C++ 代码并将其编译成动态库。
目录结构:
cpp_calculator/
├── include/
│ └── calculator.h
└── src/
└── calculator.cpp
2
3
4
5
include/calculator.h
(C++ 头文件)
这个文件定义了 Calculator
类和一个 C 风格的接口。
#ifndef CALCULATOR_H
#define CALCULATOR_H
// 定义导出宏,处理不同平台的兼容性
#if defined(_WIN32)
#ifdef BUILDING_DLL
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
#else
#define DLL_EXPORT __attribute__((visibility("default")))
#endif
// 标准的C++类
class Calculator {
public:
int add(int a, int b);
};
// C风格的接口,用于Rust调用
// extern "C" 告诉C++编译器使用C语言的调用约定和名称修饰规则
extern "C" {
// 创建Calculator实例并返回一个不透明指针
DLL_EXPORT Calculator* calculator_new();
// 释放Calculator实例
DLL_EXPORT void calculator_free(Calculator* calc);
// 调用Calculator的add方法
DLL_EXPORT int calculator_add(Calculator* calc, int a, int b);
}
#endif // CALCULATOR_H
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
src/calculator.cpp
(C++ 实现文件)
#include "../include/calculator.h"
int Calculator::add(int a, int b) {
return a + b;
}
// C风格接口的实现
Calculator* calculator_new() {
return new Calculator();
}
void calculator_free(Calculator* calc) {
if (calc != nullptr) {
delete calc;
}
}
int calculator_add(Calculator* calc, int a, int b) {
if (calc != nullptr) {
return calc->add(a, b);
}
return 0; // 或者返回一个错误码
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
编译 C++ 代码
你需要根据你的操作系统和编译器来编译。
在 Linux (使用 g++):
# 编译成位置无关代码 (PIC) g++ -c -fPIC src/calculator.cpp -o calculator.o -I./include # 链接成共享库 g++ -shared -o libcalculator.so calculator.o
1
2
3
4最终会生成
libcalculator.so
文件。在 Windows (使用 MinGW-w64 的 g++):
# 编译并链接成DLL # -DBUILDING_DLL 是为了让 __declspec(dllexport) 生效 g++ -shared -o calculator.dll src/calculator.cpp -I./include -DBUILDING_DLL -Wl,--out-implib,libcalculator.a
1
2
3最终会生成
calculator.dll
文件。在 macOS (使用 clang++):
clang++ -dynamiclib -o libcalculator.dylib src/calculator.cpp -I./include
1最终会生成
libcalculator.dylib
文件。
现在,我们有了一个包含 C 风格接口的动态库。
# 第 2 步:在 Rust 中调用动态库
现在我们来创建 Rust 项目来调用这个库。
创建 Rust 项目:
cargo new rust_caller
cd rust_caller
2
添加依赖:
编辑 Cargo.toml
文件,添加 libloading
crate。
[dependencies]
libloading = "0.8" # 使用你需要的最新版本
2
src-main.rs
(Rust 代码)
use libloading::{Library, Symbol};
use std::ffi::c_void;
// C++中的Calculator类在Rust中表示为一个不透明的结构体
// 我们只持有它的指针,不关心其内部布局
#[repr(C)]
struct Calculator;
// 定义C风格函数的函数签名
// 使用Result来处理可能的加载错误
type CalculatorNew = unsafe extern "C" fn() -> *mut Calculator;
type CalculatorFree = unsafe extern "C" fn(calc: *mut Calculator);
type CalculatorAdd = unsafe extern "C" fn(calc: *mut Calculator, a: i32, b: i32) -> i32;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 根据操作系统确定库文件的名称
let lib_path = if cfg!(target_os = "windows") {
"calculator.dll"
} else if cfg!(target_os = "macos") {
"libcalculator.dylib"
} else {
"libcalculator.so"
};
println!("Loading library from: {}", lib_path);
// unsafe块因为加载外部库和调用外部函数本质上是不安全的
unsafe {
// 加载动态库
let lib = Library::new(lib_path)?;
// 获取函数符号(指针)
let calculator_new: Symbol<CalculatorNew> = lib.get(b"calculator_new\0")?;
let calculator_free: Symbol<CalculatorFree> = lib.get(b"calculator_free\0")?;
let calculator_add: Symbol<CalculatorAdd> = lib.get(b"calculator_add\0")?;
// --- 开始使用C++对象 ---
println!("Creating calculator instance...");
// 调用 C++ 的 `calculator_new` 来创建实例
let calc_ptr = calculator_new();
if calc_ptr.is_null() {
panic!("Failed to create calculator instance.");
}
println!("Calculator instance created at address: {:?}", calc_ptr);
// 调用 C++ 的 `calculator_add` 方法
let a = 10;
let b = 22;
let result = calculator_add(calc_ptr, a, b);
println!("Rust says: {} + {} = {}", a, b, result);
// 调用 C++ 的 `calculator_free` 来释放内存
println!("Freeing calculator instance...");
calculator_free(calc_ptr);
println!("Instance freed.");
}
Ok(())
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
运行 Rust 程序:
- 将前面编译好的动态库文件(
libcalculator.so
,calculator.dll
, 或libcalculator.dylib
)复制到 Rust 项目的target/debug/
目录下,这样程序运行时才能找到它。 - 在
rust_caller
目录下运行cargo run
。
预期输出:
Loading library from: libcalculator.dll # 我在windows上运行这里显示dll
Creating calculator instance...
Calculator instance created at address: 0x55c1b5e3d2a0
Rust says: 10 + 22 = 32
Freeing calculator instance...
Instance freed.
2
3
4
5
6
# 注意事项 (非常重要)
C 语言接口 (
extern "C"
):这是最关键的一点。它禁用了 C++ 的名称修饰,保证了 Rust 可以通过函数名找到对应的函数。ABI 兼容的数据类型:在
extern "C"
接口中,只能使用 C 和 Rust 都理解的、具有稳定 ABI 的数据类型。例如:- 基本整数类型 (
int
,char
,long
等) -> Rust 的i32
,i8
,i64
等。 - 浮点数 (
float
,double
) -> Rust 的f32
,f64
。 - 指针 (
*T
) -> Rust 的*mut T
或*const T
。 - 避免直接传递 C++ 的
std::string
,std::vector
等复杂类型。如果需要传递字符串,应使用 C 风格的const char*
,并在另一端处理内存和编码。如果需要传递数组,应传递指针和长度。
- 基本整数类型 (
内存管理:
- 所有权清晰:遵循“谁创建,谁销毁”的原则。在我们的例子中,C++ 的
calculator_new
分配了内存,所以必须由 C++ 的calculator_free
来释放。Rust 绝对不能尝试用自己的方式(如Box::from_raw
)来销毁一个由 C++new
创建的对象,因为它们的内存分配器可能不同。 - 不透明指针 (Opaque Pointer):在 Rust 中,我们将
Calculator*
当作一个不透明指针 (*mut Calculator
)。Rust 代码只负责传递这个指针,而不去解引用或探究其内部结构。这是一种非常安全和常见的 FFI (Foreign Function Interface) 模式。
- 所有权清晰:遵循“谁创建,谁销毁”的原则。在我们的例子中,C++ 的
异常处理:Rust 没有传统意义上的异常(
try...catch
)。C++ 的异常不能跨越 FFI 边界传递到 Rust 中,否则会导致未定义行为(通常是程序崩溃)。- 解决方案:在
extern "C"
的 C++ 函数中捕获所有可能的异常,并将结果通过返回值(例如,返回错误码)或出参 (out-parameter) 的方式通知给 Rust。
- 解决方案:在
线程安全:如果 C++ 库或 Rust 代码是多线程的,你需要确保 C++ 对象的创建、使用和销毁是线程安全的。可能需要在 C++ 侧添加互斥锁等同步机制。
库的路径:
libloading
需要在运行时能找到你的动态库。通常可以放在以下位置:- 可执行文件所在的目录(如
target/debug/
)。 - 系统的标准库路径下(如 Linux 的
/usr/lib
)。 - 修改环境变量(如 Linux 的
LD_LIBRARY_PATH
,Windows 的PATH
,macOS 的DYLD_LIBRARY_PATH
)。
- 可执行文件所在的目录(如
函数名中的空字符:注意
lib.get(b"calculator_new\0")?
中函数名末尾的\0
。这是 C 语言字符串的结束符,libloading
需要它来确保传递给底层dlsym
或GetProcAddress
的是正确的 C 风格字符串。
# 预处理器宏代码详解
#if defined(_WIN32)
#ifdef BUILDING_DLL
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
#else
#define DLL_EXPORT __attribute__((visibility("default")))
#endif
2
3
4
5
6
7
8
9
这段代码是一个C/C++预处理器宏,用于以跨平台的方式处理动态链接库(Windows上的DLL,Linux/macOS上的共享库)中函数和变量的导出与导入。
它的核心作用是让同一份头文件既可以用于编译库本身,也可以用于给其他程序调用这个库。
# 工作原理
代码通过预处理指令(#if
, #ifdef
, #else
)来检查操作系统和一个特殊的编译标志。
# 在Windows系统 (#if defined(_WIN32)
)
Windows系统明确区分从DLL中“导出”(export)和“导入”(import)符号。
__declspec(dllexport)
: 当你编译动态库(DLL)本身时,这个关键字告诉编译器:“把这个函数放到库的公开导出表中,这样其他程序就能找到并使用它。” 📢__declspec(dllimport)
: 当其他程序使用你的DLL时,这个关键字告诉编译器:“这个函数不在我的代码里,它在外部的一个DLL中,请准备好从那里调用它。” 这有助于链接器生成更高效的代码。
#ifdef BUILDING_DLL
这个判断就是其中的开关。当你编译库项目时,你需要在编译器设置里定义 BUILDING_DLL
这个宏(例如,通过添加编译选项 -DBUILDING_DLL
)。
- 如果
BUILDING_DLL
被定义:DLL_EXPORT
就等同于__declspec(dllexport)
。 - 如果
BUILDING_DLL
未被定义:DLL_EXPORT
就等同于__declspec(dllimport)
。
# 在其他系统(Linux, macOS等) (#else
)
这些系统的模型相对简单一些。
__attribute__((visibility("default")))
: 这是GCC和Clang编译器的一个指令,作用是让一个函数或符号在库的外部可见。虽然在这些系统上,符号默认就是可见的,但显式地标记出来是一种良好的编程实践,特别是当使用-fvisibility=hidden
这个编译选项时(这是一个常见的优化手段,可以减小库文件大小并避免符号冲突)。对于库的使用者来说,不需要像Windows那样有对应的“导入”声明。
# 如何使用
你可以在库的头文件中像下面这样使用这个宏:
my_library.h
#ifndef MY_LIBRARY_H
#define MY_LIBRARY_H
// 把你问题中的那段宏定义代码放在这里
#if defined(_WIN32)
#ifdef BUILDING_DLL
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
#else
#define DLL_EXPORT __attribute__((visibility("default")))
#endif
// 使用这个宏来标记需要导出的函数
DLL_EXPORT int add(int a, int b);
// 也可以用来标记整个类
class DLL_EXPORT MyClass {
public:
void doSomething();
};
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
通过这种方式,my_library.h
这同一份头文件,无需任何修改,就可以完美地同时用于库项目本身和任何使用该库的应用程序。✨