Files
Knowledge/0 编程/rust/使用bindgen将C语言头文件转换为Rust接口代码+47543bc0-182e-4f47-97da-6b83812741a6_23013RK75C_Jan-29-0936-2026_1.md
T

379 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[嵌入式系统](https://so.csdn.net/so/search?q=%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F&spm=1001.2101.3001.7020)层及应用层的软件开发,离不开C语言。笔者希望使用一种高效、稳定的开发语言,在一定程度上替代C语言,从而提高开发效率、降低嵌入式软件的扩展、维护成本,同时缩小研发团队规模。[Rust](https://rust-lang.org/)编程语言很好地满足了高效、稳定这两个要求。不过需要在一定程度解决`Rust`调用外部C语言模块的问题:`Rust`语言已提供了完善的解决方案,笔者希望通过本文做一个必要的记录。
笔者在之前一篇[文章](https://blog.csdn.net/yeholmes/article/details/118660405)中简要介绍了[Rust](https://rust-lang.org/)语言调用C语言动态库提供的函数的一般方法。不过随着`Rust`工程依赖的外部的C语言模块越来越来复杂,手工将C语言头文件定义的调用接口转换为`Rust`接口代码变得不具可操作性。幸运的是,一个名为[bindgen](https://github.com/rust-lang/rust-bindgen)的开源项目很好地解决了这个问题,它通过[clang](https://clang.llvm.org/docs/)编译器库对C语言的头文件进行预处理,并生成相应的`Rust`接口代码;本文参考了其[官方文档](https://rust-lang.github.io/rust-bindgen),结合笔者的开发需要作简要的使用说明。
#### Rust语言将字符串转换为整型
笔者在实际开发过程,需要将`Rust`的一个字符串类型转换为整型,`Rust`柡准库已经提供了相应的转换函数[parse](https://doc.rust-lang.org/std/primitive.str.html#method.parse):
```Rust
pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>where
F: FromStr,
...
let four: u32 = "4".parse().unwrap();
assert_eq!(4, four);
12345
```
不过,`parse`函数的缺陷是,它要求输入的字符串是十进制的,对于`"0x1234"`之类的非十六进制数,则不能正确处理。然而,柡准库也提供了另一个函数[from_str_radix](https://doc.rust-lang.org/std/primitive.i64.html#method.from_str_radix),可指定任意仍意进制的字符串到整型:
```Rust
pub fn from_str_radix(src: &str, radix: u32) -> Result<i64, ParseIntError>
...
assert_eq!(i64::from_str_radix("A", 16), Ok(10));
123
```
结合这两个柡准库提供的函数,就可以编写一个纯粹的`Rust`函数,根据字符串的前缀决定调用哪一个转换函数了。不过笔者是怀旧的,希望继续调用C语言柡准库提供的函数`strtoll/strtoull`,这两个函数可以自动判断字符串的进制(尽管仅限于几个进制)。
笔者为`Rust`工程编写的代码如下(完整代码可参考[此处](https://github.com/jaqchen/bindings)):
```C
/* extmodule/extmodule.h */
#ifndef RUST_EXTMODULE_H
#define RUST_EXTMODULE_H 1
int extm_strtol(const char * strp, long long * valp, int base);
int extm_strtoul(const char * strp, unsigned long long * valp, int base);
#endif
/* extmodule/extmodule.c */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
int extm_strtol(const char * strp, long long * valp, int base)
{
long long ret;
int error = EINVAL;
char * strend = NULL;
if (strp == NULL)
return error;
errno = 0;
ret = strtoll(strp, &strend, base);
error = errno;
if (error || strend == strp)
return (error > 0) ? error : EINVAL;
if (valp != NULL)
*valp = ret;
return 0;
}
int extm_strtoul(const char * strp, unsigned long long * valp, int base)
{
int error = EINVAL;
char * strend = NULL;
unsigned long long ret;
if (strp == NULL)
return error;
errno = 0;
ret = strtoull(strp, &strend, base);
error = errno;
if (error || strend == strp)
return (error > 0) ? error : EINVAL;
if (valp != NULL)
*valp = ret;
return 0;
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
```
之后,笔者对这两个函数`extm_strtol/extm_strtoul`进一步封装:
```Rust
// src/lib.rs
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
use std::os::raw::c_int;
use std::os::raw::c_longlong;
use std::os::raw::c_ulonglong;
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
pub fn strtol(x: &str, base: i32) -> Result<i64, std::io::Error> {
let mut res: c_longlong = 0;
let y: Vec<u8> = x.as_bytes().iter().cloned().collect();
let error = unsafe {
let z = std::ffi::CString::from_vec_unchecked(y);
extm_strtol(z.as_ptr(), &mut res as *mut c_longlong, base as c_int)
};
match error {
0 => Ok(res as i64),
_ => Err(std::io::Error::from_raw_os_error(error as i32)),
}
}
pub fn strtoul(x: &str, base: i32) -> Result<u64, std::io::Error> {
let mut res: c_ulonglong = 0;
let y: Vec<u8> = x.as_bytes().iter().cloned().collect();
let error = unsafe {
let z = std::ffi::CString::from_vec_unchecked(y);
extm_strtoul(z.as_ptr(), &mut res as *mut c_ulonglong, base as c_int)
};
match error {
0 => Ok(res as u64),
_ => Err(std::io::Error::from_raw_os_error(error as i32)),
}
}
123456789101112131415161718192021222324252627282930313233343536
```
这样,笔者就得到了两个`Rust`语言版本的字符串到整型的转换函数,`strtol/strtoul`。接下来就要解决编译的问题,即将`extmodule/extmodule.h`头文件转换为`src/lib.rs`包含的接口文件,`bindings.rs`:
```Rust
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
1
```
此外还要求在为嵌入式设备编译构建时,也能够交叉编译`extmodule`模块。
#### 编写build.rs自动化编译外部模块
笔者参考了[bindgen](https://rust-lang.github.io/rust-bindgen/)的相关文档,调用相关的`binding`接口,将`extmodule/extmodule.h`转换为`$(OUT_DIR)`目录下的`bindings.rs`接口代码;之后又调用了[make](https://www.gnu.org/software/make/)命令行工具,实现`extmodule`的(交叉)编译,生成`libextm.so`动态库:
```Rust
extern crate bindgen;
use std::process::Command;
fn main() {
// generate binding.rs for extmodule
let bindings = bindgen::Builder::default()
.header("extmodule/extmodule.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
// invoke make to build external C module
let cc = format!("CC={}", std::env::var("TARGET_CC")
.unwrap_or_else(|_| "cc".to_string()));
let cflags = format!("CFLAGS={}", std::env::var("TARGET_CFLAGS")
.unwrap_or_else(|_| "-Wall -fPIC -D_GNU_SOURCE -Os -ggdb".to_string()));
let okay = Command::new("make")
.arg(AsRef::<std::ffi::OsStr>::as_ref(&cc))
.arg(AsRef::<std::ffi::OsStr>::as_ref(&cflags))
.args(&["-C", "./extmodule", "-j1", "clean", "all"])
.spawn()
.expect("Failed to invoke make utility")
.wait()
.expect("Failed to wait make utility")
.success();
if !okay {
eprintln!("Error, make for external C module has failed!");
std::process::exit(1);
}
println!("cargo:rustc-link-lib=extm");
println!("cargo:rustc-link-search=./extmodule");
println!("cargo:rerun-if-changed=./extmodule/extmodule.h");
println!("cargo:rustc-link-arg-bins=-Wl,-rpath=$ORIGIN");
}
1234567891011121314151617181920212223242526272829303132333435363738
```
在编译之前,需要为系统安装`clang`相关的依赖,这是`bindgen`需要的:
```Shell
sudo apt install clang-14 libclang-14-dev # for ubuntu-22.04
1
```
笔者编译、运行`bindings`工程的输出结果如下:
```Plain Text
yejq@ubuntu:~/program/bindings$ cargo build --release
Compiling bindings v0.1.0 (/home/yejq/program/bindings)
Finished release [optimized] target(s) in 0.73s
yejq@ubuntu:~/program/bindings$ cp -v ./extmodule/libextm.so ./target/release/
'./extmodule/libextm.so' -> './target/release/libextm.so'
yejq@ubuntu:~/program/bindings$ ./target/release/bindings 2099 0x2030
arg0: 2099, arg1: 0x2030
System uptime: 86910
total 0
lrwx------ 1 yejq yejq 64 1月 25 11:35 0 -> /dev/pts/11
lrwx------ 1 yejq yejq 64 1月 25 11:35 1 -> /dev/pts/11
lrwx------ 1 yejq yejq 64 1月 25 11:35 2 -> /dev/pts/11
lr-x------ 1 yejq yejq 64 1月 25 11:35 3 -> /dev/null
lr-x------ 1 yejq yejq 64 1月 25 11:35 4 -> /proc/17644/fd
total 0
lrwx------ 1 yejq yejq 64 1月 25 11:35 0 -> /dev/pts/11
lrwx------ 1 yejq yejq 64 1月 25 11:35 1 -> /dev/pts/11
lrwx------ 1 yejq yejq 64 1月 25 11:35 2 -> /dev/pts/11
lr-x------ 1 yejq yejq 64 1月 25 11:35 3 -> /proc/17645/fd
12345678910111213141516171819
```
可以看到,使用`extmodule`外部模块,可以很好地解决十六进制字符串`0x2030`转换为整型的问题。
#### 使用`bindgen`命令行工具转换接口文件
`Rust`语言、工具链开发者选择`Rust`作为自定义编译构建的语言,相应的代码为工作根目录下的`build.rs`。该代码依赖了`bindgen`库,将`extmodule/extmodule.h`转化为`Rust`编程语言的接口文件,这一依赖在`Cargo.toml`需要指明:
```Toml
[dependencies]
[build-dependencies]
bindgen = "0.62.0"
1234
```
有人可能会提议,使用`build.rs`作为自定义编译构建代码,可能不太方便,因为某些工程不产生以上依赖,而是使用[bindgen](https://rust-lang.github.io/rust-bindgen/command-line-usage.html)命令行工具实现以上C语言头文件到`bindings.rs`接口的转换,那么使用`shell`脚本就更合理。例如`build.rs.sh`脚本实现了目前的`build.rs`所有功能:
```Shell
#!/bin/bash
# Created by yejq.jiaqiang@gmail.com
# Simple build script for bindtest
# 2023/01/24
# generate bindings.rs source file in `$(OUT_DIR) directory
generate_bindings() {
if [ ! -d "${OUT_DIR}" ] ; then
echo "Error, \`\${OUT_DIR} not found." 1>&2
return 1
fi
bindgen -o "${OUT_DIR}/bindings.rs" 'extmodule/extmodule.h'
return $?
}
compile_extmodule() {
local COMPILER="${TARGET_CC:-gcc}"
local C_FLAGS="${TARGET_CFLAGS:--Wall -fPIC -Os -D_GNU_SOURCE -ggdb}"
make "CC=${COMPILER}" "CFLAGS=${C_FLAGS}" -C extmodule -j1 clean all
return $?
}
define_rustc_flags() {
echo "cargo:rustc-link-lib=extm"
echo "cargo:rustc-link-search=./extmodule"
echo "cargo:rerun-if-changed=./build.rs.sh"
echo "cargo:rerun-if-changed=./extmodule/extmodule.h"
echo "cargo:rustc-link-arg-bins=-Wl,-rpath=\$ORIGIN"
return 0
}
generate_bindings || exit $?
compile_extmodule || exit $?
define_rustc_flags ; exit 0
1234567891011121314151617181920212223242526272829303132333435
```
该脚本,即简洁,又具备很强的扩展性,修改起来又比`build.rs`方便很多;确实是这样。那么可以修改`build.rs`脚本,实现对该脚本的一劳永逸的调用:
```diff
diff --git a/Cargo.toml b/Cargo.toml
index a57c279..3b947ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,4 +8,4 @@ edition = "2021"
[dependencies]
[build-dependencies]
-bindgen = "0.62.0"
+libc = { version = "0.2.139" }
diff --git a/build.rs b/build.rs
index 6a2fff1..dddf1f8 100644
--- a/build.rs
+++ b/build.rs
@@ -1,38 +1,17 @@
-extern crate bindgen;
-use std::process::Command;
+use std::ffi::CString;
+use libc::{c_char, execv};
+use std::collections::VecDeque;
fn main() {
- // generate binding.rs for extmodule
- let bindings = bindgen::Builder::default()
- .header("extmodule/extmodule.h")
- .parse_callbacks(Box::new(bindgen::CargoCallbacks))
- .generate()
- .expect("Unable to generate bindings");
- let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
- bindings.write_to_file(out_path.join("bindings.rs"))
- .expect("Couldn't write bindings!");
-
- // invoke make to build external C module
- let cc = format!("CC={}", std::env::var("TARGET_CC")
- .unwrap_or_else(|_| "cc".to_string()));
- let cflags = format!("CFLAGS={}", std::env::var("TARGET_CFLAGS")
- .unwrap_or_else(|_| "-Wall -fPIC -D_GNU_SOURCE -Os -ggdb".to_string()));
- let okay = Command::new("make")
- .arg(AsRef::<std::ffi::OsStr>::as_ref(&cc))
- .arg(AsRef::<std::ffi::OsStr>::as_ref(&cflags))
- .args(&["-C", "./extmodule", "-j1", "clean", "all"])
- .spawn()
- .expect("Failed to invoke make utility")
- .wait()
- .expect("Failed to wait make utility")
- .success();
- if !okay {
- eprintln!("Error, make for external C module has failed!");
- std::process::exit(1);
- }
-
- println!("cargo:rustc-link-lib=extm");
- println!("cargo:rustc-link-search=./extmodule");
- println!("cargo:rerun-if-changed=./extmodule/extmodule.h");
- println!("cargo:rustc-link-arg-bins=-Wl,-rpath=$ORIGIN");
+ // invoke build.rs.sh script instead
+ let argv: Vec<String> = std::env::args().skip(1).collect();
+ let mut argw: VecDeque<CString> = argv.iter()
+ .map(|x| CString::new(x.as_bytes()).unwrap()).collect();
+ argw.push_front(CString::new("./build.rs.sh").unwrap());
+ let mut argx: Vec<*const c_char> = argw.iter().map(|y| y.as_ptr()).collect();
+ argx.push(std::ptr::null());
+ unsafe { execv(argx[0], argx.as_mut_ptr()) };
+ eprintln!("Error, failed to invoke ./build.rs.sh: {:?}",
+ std::io::Error::last_os_error());
+ std::process::exit(1);
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
```
#### 简单的C语言头文件
以上的编译构建,考虑到了对嵌入式设备支持。主要是在`build.rs`(或`build.rs.sh`)访问`TARGET_CC`/`TARGET_CFLAGS`两个与交叉编译相关的环境变量。不过,值得说明的是,对于简单的C语言头文件(例如笔者编写的`extmodule/extmodule.h`)可以这样转换,但对于复杂的开源库,交叉编译时,因其头文件比较复杂,这种基于`bindgen`的接口转换常常是不可用的。举个例子,对于开源的[paho.mqtt.rust](https://github.com/eclipse/paho.mqtt.rust/tree/master/paho-mqtt-sys/bindings)软件,因其依赖了`paho.mqtt.c`库,在交叉编译时,就会使用该工程自己维护的`bindings`接口代码,而不是使用`bindgen`来转换:
```Plain Text
yejq@ubuntu:~/program/paho.mqtt.rust/paho-mqtt-sys/bindings$ ls -lh
total 1.7M
-rw-rw-r-- 1 ubuntu ubuntu 267K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-aarch64-unknown-linux-gnu.rs
-rw-rw-r-- 1 ubuntu ubuntu 216K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-armv7-unknown-linux-gnueabihf.rs
-rw-rw-r-- 1 ubuntu ubuntu 216K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-default-32.rs
-rw-rw-r-- 1 ubuntu ubuntu 265K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-default-64.rs
-rw-rw-r-- 1 ubuntu ubuntu 281K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-x86_64-apple-darwin.rs
-rw-rw-r-- 1 ubuntu ubuntu 211K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-x86_64-pc-windows-msvc.rs
-rw-rw-r-- 1 ubuntu ubuntu 265K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-x86_64-unknown-linux-gnu.rs
drwxrwxr-x 2 ubuntu ubuntu 4.0K Jan 25 11:36 old
12345678910
```
虽然如此,我们在[嵌入式软件](https://so.csdn.net/so/search?q=%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%BD%AF%E4%BB%B6&spm=1001.2101.3001.7020)开发时,可以编写易于转换的C语言头文件,这就需要我们在实际开发中不断调整头文件的编写。