概述

某些情况下,程序的一些功能需要用c语言来实现(比如:处于性能优化考虑),或者需要使用已经存在的第三方c语言函数库,此时就需要实现rust调用c函数库(静态库或动态链接库)。

实验目标

  • c语言生成函数库。
  • 从rust调用c语言函数库。
  • 编译工程

代码工程

f0002
├── Cargo.toml
├── README.md
├── ansic/
│   ├── CMakeLists.txt
│   ├── include/
│   │   └── simplemath.h
│   └── src/
│       └── simplemath.c
├── build.rs
└── src/
    └── main.rs

实验内容

c语言生成函数库

  1. 定义函数接口

    编制头文件暴露相关的函数接口:

    // simplemath.h
    
    #ifndef _SIMPLEMATH_H_
    #define _SIMPLEMATH_H_
    
    int add(int left, int right);  // 加
    int sub(int left, int right);  // 减
    int mul(int left, int right);  // 乘
    int div(int left, int right);  // 除
    
    #endif
    
  2. 实现函数

    // simplemath.c
    
    #include "simplemath.h"
    
    int add(int left, int right) {
        return left + right;
    }
    
    int sub(int left, int right) {
        return left - right;
    }
    
    int mul(int left, int right) {
        return left * right;
    }
    
    int div(int left, int right) {
        return left / right;
    }
    
  3. 编译生成函数库

    编译c代码生成动态链接库或静态函数库(这里我们选用静态链接的实现方式)。

    # 生成动态链接库
    # gcc -shared -fPIC -o libsimplemath.so -I include src/simplemath.c
    
    # 生成静态函数库
    gcc -c -o simplemath.o -I include src/simplemath.c
    ar rc libsimplemath.a simplemath.o
    

rust 调用c函数库

  1. 接口绑定

    // binding.rs
    
    // 链接动态库
    //#[link(name = "simplemath", kind = "dylib")]
    // 链接静态库
    #[link(name = "simplemath", kind = "static")]
    extern "C" {
        fn add(left: isize, right: isize) -> isize;
        fn sub(left: isize, right: isize) -> isize;
        fn mul(left: isize, right: isize) -> isize;
        fn div(left: isize, right: isize) -> isize;
    }
    
  2. 调用绑定的接口函数

    
    mod binding;
    
    fn main() {
        let left = 10isize;
        let right = 3isize;
    
        println!("{} + {} = {}", left, right, unsafe { binding.add(left, right) });
        println!("{} - {} = {}", left, right, unsafe { binding.sub(left, right) });
        println!("{} * {} = {}", left, right, unsafe { binding.mul(left, right) });
        println!("{} / {} = {}", left, right, unsafe { binding.div(left, right) });
    }
    
  3. 编译运行

    将第一步生成的函数库拷贝到rust工程的输出目录中( target/debugtarget/release ),然后运行以下命令即可观察到rust成功调用c函数库。

    cargo run
    

    注意:若以动态链接库的形式调用,则需要将动态链接库文件的路径加入环境变量 LD_LIBRARY_PATH;而若以静态库的方式进行链接,则相应的函数实现已经被编译到了执行码中,运行时就不需要额外指定任何参数了。

编译工程

为了实现自动编译,需要利用 cargo 提供的 build.rs 机制:若工程目录下是否存在名为 build.rs 的文件,则 cargo 会先编译并执行 build.rs,并将其输出的结果加入 rustc 的编译参数,然后再编译 rust 工程的代码。

利用这个机制,我们可以在编译 rust 代码之前,先编译 c 代码生成函数库,并传递给 rustc 编译器。具体有以下两种实现方式:

  • 利用 Makefile 和 make
  • 利用 CMakeLists.txt 和 cmake

利用 Makefile 和 make

实现思路是,先准备好 Makefile ,然后在 build.rs 中调用 make 编译c代码生成函数库。

这种实现方式的优点是实现简单,缺点是若想兼容各种平台,Makefile写起来很麻烦。

  1. 创建 Makefile 文件

    此Makefile利用gcc编译c代码生成函数库,并放置到rust的taget目录下。

    PROJECT_DIR := ..
    PROFILE ?= debug
    TARGET_DIR := $(PROJECT_DIR)/target/$(PROFILE)
    
    ifeq ($(PROFILE), "release")
        CFLAGS := -fPIC -O2
    else
        CFLAGS := -fPIC
    endif
    
    build: src/simplemath.c include/simplemath.h
        mkdir -p $(TARGET_DIR)
        gcc $(CFLAGS) -I include -o $(TARGET_DIR)/simplemath.o src/simplemath.c
        ar rc $(TARGET_DIR)/libsimplemath.a $(TARGET_DIR)/simplemath.o
    
  2. 创建 build.rs 文件

    在编译rust代码前,会先调用build.rs进行前处理:编译生成c函数库,并将相关参数传递给rustc。

    // build.rs
    
    fn main() {
        // ## 通过命令行调用make编译c代码并生成函数库
        use std::process::Command;
        use std::path::{Path, PathBuf};
    
        // 获取相关路径
        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
        let rust_dir = PathBuf::from(manifest_dir);
        let ansic_dir = rust_dir.parent().unwrap().join("ansic");
        let profile = std::env::var("PROFILE").unwrap();
    
        // 编译生成ansic动态链接库
        let _make_result = Command::new("make")
            .arg("build")
            .env("PROFILE", &profile)
            .current_dir(ansic_dir.as_os_str().to_str().unwrap())
            .status()
            .expect("Failed to execute make command");
    
        // ## 输出cargo链接参数
        println!("cargo:rerun-if-changed=build.rs");
        println!("cargo:rustc-link-search=native=target/{}", &profile);
        println!("cargo:rustc-link-lib=dylib=simplemath");
    }
    
  3. 编译运行

    cargo run
    

利用 CMakeLists.txt 和 cmake

上一种方法是基于 Linux 来实现的,但是如果将工程放在Windows环境下编译,会导致失败。如果不想花费很大的精力制作并维护一份兼容各种平台的Makefile,那么可以考虑利用cmake工具来实现兼容各种平台。实现思路是,先准备好 CMakeLists.txt文件,然后利用 cmake 自动生成兼容当前平台的 Makefile,最后再利用 make 编译c代码生成函数库。

采用这种实现方式的优点是不需要操心平台兼容的问题,缺点是需要安装 cmake 工具,并且要学习 CMakeLists.txt 的语法。

  1. 创建 CMakeLists.txt 文件

    文件如下:

    # 设置允许的cmake最低版本
    cmake_minimum_required(VERSION 3.12)
    
    # 设置目标名称
    set(TARGET f0002_ansic)
    
    # 设置项目名称及使用的开发语言
    project(${TARGET} LANGUAGES C)
    
    # 设置C语言标准
    set(CMAKE_C_STANDARD 23)
    
    # 生成静态库,及相应的c代码
    add_library(${TARGET} STATIC "src/simplemath.c")
    
    # 设置头文件搜索目录
    target_include_directories(${TARGET} PRIVATE "include")
    
    # 设置目标生成目录
    install(TARGETS ${TARGET} DESTINATION .)
    
  2. 创建 build.rs 文件

    在编译rust代码前,会先调用build.rs进行前处理:编译生成c函数库,并将相关参数传递给rustc。

    // build.rs
    
    fn main() {
        // ## 事先做成CMakeLists.txt,利用cmake编译c代码并生成函数库
        use cmake::Config;
        let dst = Config::new("ansic").build();
    
        // ## 生成cargo链接参数
        // 若build.rs有任何修改,则重新编译
        println!("cargo:rerun-if-changed=build.rs");
        // 搜索生成的函数库
        println!("cargo:rustc-link-search=native={}", dst.display());
        // 链接生成的函数库
        println!("cargo:rustc-link-lib=static=simplemath");
    }
    

    因为这里使用了第三方库 cmake-rs,所以需要在 Cargo.toml 文件中添加相关依赖:

    [package]
    name = "f0002"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    
    [build-dependencies]
    cmake = "0.1"
    

    注意:如果不想使用第三方库,也可以采用命令行调用的方式,相关的命令如下(这里就没有写出在 build.rs 中的实现了):

    mkdir -p tmp
    ( cd tmp && cmake ../ansic && make )
    cp tmp/libsimplemath.a ../target/debug
    rm -rf tmp
    
  3. 安装相关软件

    如果是在 Linux 环境下,通过以下命令安装:

    sudo yum install cmake
    cmake --version
    

    如果是在 Windows MinGW-w64 环境下,通过以下命令安装:

    pacman -S mingw-w64-x86_64-cmake
    cmake --version
    

    注意:

    1. 实验环境采用的是 Windows + MingGW-W64 + gcc。如果想使用msvc编译器,则需要做相关调整。
    2. 如果安装的版本是 pacman -S cmake,则编译时会出现各种奇怪的问题并导致失败。
  4. 编译运行

    cargo run
    

    以下是 Linux 平台下的运行结果:

    以下是 Windows 平台的运行结果:

遗留问题

  • c代码本身有任何变更时,cargo不会重新编译生成c语言函数库(除非运行 cargo clean 清除所有缓存)
  • 在 Windows 环境下编译时,会报warning,虽然并不影响编译运行。