Xz's blog Xz's blog
首页
时间序列
多模态
合成生物学
其他方向
生活
工具相关
PyTorch
导航站

Xu Zhen

首页
时间序列
多模态
合成生物学
其他方向
生活
工具相关
PyTorch
导航站
  • Rust

    • Rust 工具链
    • Cargo
    • 变量
    • 函数和控制流
    • 所有权
    • 引用与借用
    • Slice切片
    • 属性 Attribute
    • 闭包
    • Trait—关联类型(Associated Types)
    • 关联类型与泛型
    • 多线程 mpsc::channel
    • Rust 调用 C++ 之静态链接
    • Rust 调用 C++ 之动态链接
      • Rust与C++之间传递数据
    • Rust-Windows 窗口自动化

    • Tauri

    • C++

    • Claude Code

    • Liunx相关

    • Windows相关

    • IDE

    • Conda

    • Docker

    • VMware虚拟机

    • Python常用代码片段

    • 工具相关
    • Rust
    xuzhen
    2025-08-25
    目录

    Rust 调用 C++ 之动态链接

    下面讲解 Rust 如何以动态链接的方式调用 C++ 中的方法,以及注意事项。

    由于 Rust 和 C++ 之间存在 ABI (Application Binary Interface) 兼容性问题,特别是 C++ 的 name mangling (名称修饰) 和复杂的特性(如类、模板、异常等),Rust 不能直接调用 C++ 的方法。

    核心思路是:

    1. C++ 侧:将需要暴露给 Rust 的 C++ 功能封装在一个或多个 extern "C" 的 C 风格函数中。这些 C 函数将作为两种语言之间的桥梁。
    2. 编译 C++:将 C++ 代码编译成一个动态链接库(在 Windows 上是 .dll,在 Linux 上是 .so,在 macOS 上是 .dylib)。
    3. Rust 侧:使用 libloading 等库,在运行时动态加载这个库,获取 C 风格函数的指针,然后通过这个指针来调用 C++ 的功能。

    下面我们通过一个具体的示例来演示这个过程。


    # 示例场景

    我们将在 C++ 中创建一个简单的计算器类 Calculator,它有一个 add 方法。然后,我们将在 Rust 中加载这个 C++ 动态库,并调用 add 方法来计算两个数的和。

    # 第 1 步:创建 C++ 动态链接库

    首先,我们需要创建 C++ 代码并将其编译成动态库。

    目录结构:

    cpp_calculator/
    ├── include/
    │   └── calculator.h
    └── src/
        └── calculator.cpp
    
    1
    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
    
    1
    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; // 或者返回一个错误码
    }
    
    1
    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
    
    1
    2

    添加依赖:

    编辑 Cargo.toml 文件,添加 libloading crate。

    [dependencies]
    libloading = "0.8" # 使用你需要的最新版本
    
    1
    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(())
    }
    
    1
    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 程序:

    1. 将前面编译好的动态库文件(libcalculator.so, calculator.dll, 或 libcalculator.dylib)复制到 Rust 项目的 target/debug/ 目录下,这样程序运行时才能找到它。
    2. 在 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.
    
    1
    2
    3
    4
    5
    6

    # 注意事项 (非常重要)

    1. C 语言接口 (extern "C"):这是最关键的一点。它禁用了 C++ 的名称修饰,保证了 Rust 可以通过函数名找到对应的函数。

    2. 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*,并在另一端处理内存和编码。如果需要传递数组,应传递指针和长度。
    3. 内存管理:

      • 所有权清晰:遵循“谁创建,谁销毁”的原则。在我们的例子中,C++ 的 calculator_new 分配了内存,所以必须由 C++ 的 calculator_free 来释放。Rust 绝对不能尝试用自己的方式(如 Box::from_raw)来销毁一个由 C++ new 创建的对象,因为它们的内存分配器可能不同。
      • 不透明指针 (Opaque Pointer):在 Rust 中,我们将 Calculator* 当作一个不透明指针 (*mut Calculator)。Rust 代码只负责传递这个指针,而不去解引用或探究其内部结构。这是一种非常安全和常见的 FFI (Foreign Function Interface) 模式。
    4. 异常处理:Rust 没有传统意义上的异常(try...catch)。C++ 的异常不能跨越 FFI 边界传递到 Rust 中,否则会导致未定义行为(通常是程序崩溃)。

      • 解决方案:在 extern "C" 的 C++ 函数中捕获所有可能的异常,并将结果通过返回值(例如,返回错误码)或出参 (out-parameter) 的方式通知给 Rust。
    5. 线程安全:如果 C++ 库或 Rust 代码是多线程的,你需要确保 C++ 对象的创建、使用和销毁是线程安全的。可能需要在 C++ 侧添加互斥锁等同步机制。

    6. 库的路径:libloading 需要在运行时能找到你的动态库。通常可以放在以下位置:

      • 可执行文件所在的目录(如 target/debug/)。
      • 系统的标准库路径下(如 Linux 的 /usr/lib)。
      • 修改环境变量(如 Linux 的 LD_LIBRARY_PATH,Windows 的 PATH,macOS 的 DYLD_LIBRARY_PATH)。
    7. 函数名中的空字符:注意 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
    
    1
    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
    
    1
    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 这同一份头文件,无需任何修改,就可以完美地同时用于库项目本身和任何使用该库的应用程序。✨

    #Rust
    上次更新: 2025/08/28, 09:57:26

    ← Rust 调用 C++ 之静态链接 Rust与C++之间传递数据→

    最近更新
    01
    Linux 通过Windows代理上网
    09-18
    02
    vscode远程使用copilot和codex(内网环境)
    09-18
    03
    跨机器克隆环境
    09-18
    更多文章>
    Theme by Vdoing | Copyright © 2025-2025 Xu Zhen | 鲁ICP备2025169719号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式