概述
某些情况下,程序的一些功能需要用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语言生成函数库
-
定义函数接口
编制头文件暴露相关的函数接口:
// 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
-
实现函数
// 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; }
-
编译生成函数库
编译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函数库
-
接口绑定
// 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; }
-
调用绑定的接口函数
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) }); }
-
编译运行
将第一步生成的函数库拷贝到rust工程的输出目录中(
target/debug
或target/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写起来很麻烦。
-
创建
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
-
创建
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"); }
-
编译运行
cargo run
利用 CMakeLists.txt 和 cmake
上一种方法是基于 Linux 来实现的,但是如果将工程放在Windows环境下编译,会导致失败。如果不想花费很大的精力制作并维护一份兼容各种平台的Makefile,那么可以考虑利用cmake工具来实现兼容各种平台。实现思路是,先准备好 CMakeLists.txt
文件,然后利用 cmake
自动生成兼容当前平台的 Makefile
,最后再利用 make
编译c代码生成函数库。
采用这种实现方式的优点是不需要操心平台兼容的问题,缺点是需要安装 cmake
工具,并且要学习 CMakeLists.txt
的语法。
-
创建
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 .)
-
创建
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
-
安装相关软件
如果是在 Linux 环境下,通过以下命令安装:
sudo yum install cmake cmake --version
如果是在 Windows MinGW-w64 环境下,通过以下命令安装:
pacman -S mingw-w64-x86_64-cmake cmake --version
注意:
- 实验环境采用的是 Windows + MingGW-W64 + gcc。如果想使用msvc编译器,则需要做相关调整。
- 如果安装的版本是
pacman -S cmake
,则编译时会出现各种奇怪的问题并导致失败。
-
编译运行
cargo run
以下是 Linux 平台下的运行结果:
以下是 Windows 平台的运行结果:
遗留问题
- c代码本身有任何变更时,cargo不会重新编译生成c语言函数库(除非运行
cargo clean
清除所有缓存) - 在 Windows 环境下编译时,会报warning,虽然并不影响编译运行。