[嵌入式系统](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(&self) -> Result::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 ... 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 #include #include #include #include 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 { let mut res: c_longlong = 0; let y: Vec = 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 { let mut res: c_ulonglong = 0; let y: Vec = 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::::as_ref(&cc)) .arg(AsRef::::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::::as_ref(&cc)) - .arg(AsRef::::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 = std::env::args().skip(1).collect(); + let mut argw: VecDeque = 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语言头文件,这就需要我们在实际开发中不断调整头文件的编写。