vault backup: 2026-11-07 16:33:44

This commit is contained in:
cheney
2026-11-07 16:33:55 +08:00
commit 7015312702
1208 changed files with 132432 additions and 0 deletions
@@ -0,0 +1,26 @@
初始化创建
```Plain Text
cargo new rust-dylib-test --lib
```
手动修改 Cargo.toml
```Plain Text
[dependencies]
libc = "0.2.133"
[lib]
crate-type = ["cdylib"]
```
[Rust 与 C 之间,传递字符串的 7 种方式!](rust+04e8e86f-2e98-4d42-8ed7-bd3f2faa6b46/Rust+%E4%B8%8E+C+%E4%B9%8B%E9%97%B4%EF%BC%8C%E4%BC%A0%E9%80%92%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84+7+%E7%A7%8D%E6%96%B9%E5%BC%8F%EF%BC%81%20d4df9d63-e517-4dbd-a5b2-2284c128313a.md)
[使用bindgen将C语言头文件转换为Rust接口代码](rust+04e8e86f-2e98-4d42-8ed7-bd3f2faa6b46/%E4%BD%BF%E7%94%A8bindgen%E5%B0%86%EF%BC%A3%E8%AF%AD%E8%A8%80%E5%A4%B4%E6%96%87%E4%BB%B6%E8%BD%AC%E6%8D%A2%E4%B8%BARust%E6%8E%A5%E5%8F%A3%E4%BB%A3%E7%A0%81%2047543bc0-182e-4f47-97da-6b83812741a6.md)
@@ -0,0 +1,26 @@
初始化创建
```Plain Text
cargo new rust-dylib-test --lib
```
手动修改 Cargo.toml
```Plain Text
[dependencies]
libc = "0.2.133"
[lib]
crate-type = ["cdylib"]
```
[Rust 与 C 之间,传递字符串的 7 种方式!](rust+04e8e86f-2e98-4d42-8ed7-bd3f2faa6b46/Rust+%E4%B8%8E+C+%E4%B9%8B%E9%97%B4%EF%BC%8C%E4%BC%A0%E9%80%92%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84+7+%E7%A7%8D%E6%96%B9%E5%BC%8F%EF%BC%81%20d4df9d63-e517-4dbd-a5b2-2284c128313a.md)
[使用bindgen将C语言头文件转换为Rust接口代码](rust+04e8e86f-2e98-4d42-8ed7-bd3f2faa6b46/%E4%BD%BF%E7%94%A8bindgen%E5%B0%86%EF%BC%A3%E8%AF%AD%E8%A8%80%E5%A4%B4%E6%96%87%E4%BB%B6%E8%BD%AC%E6%8D%A2%E4%B8%BARust%E6%8E%A5%E5%8F%A3%E4%BB%A3%E7%A0%81%2047543bc0-182e-4f47-97da-6b83812741a6.md)
@@ -0,0 +1,333 @@
摘要:Rust 得以在编程语言中火速崛起的原因之一,就是它能够与 C 语言进行互操作。因此在本文中,作者介绍了在 Rust 与 C 之间传递字符串的七种方式,这些方法不仅可用于传递字符串,也可用于其他数据。
(英)原文链接:https://dev.to/kgrech/7-ways-to-pass-a-string-between-rust-and-c-4ieb
Rust和C语言能够互操作,这恐怕是Rust最不可思议的功能之一。能够在C语言中调用安全地Rust代码,能够在Rust中通过C接口使用一些流行库,正是Rust能够在整个行业迅速流行的原因之一。此外,我们还可以通过C接口,用不同的语言编写代码,这样凡是能够调用C的语言都可以使用这些代码。
FFI 接口的编写难度很高,新手很难成功。如何处理 into_raw 和 as_ptr 方法,才不会导致内存泄漏或引发安全漏洞?在编写代码时,我们难免会使用一些不安全的关键字,这会令我们不安。我将在本文中介绍有关FFI接口的内存处理,并提供一些我在项目中使用过的有效模式。
> 注意:这里我以字符串作为例进行说明,实际上这些技术也适用于将字节数组或指针传输到 Box 或 Arc 类型的堆结构上。
![87a0c036bc575b09d97517ce0921839b_47d58a77a02edeb8d1972506c9ae6f7c.jpeg](87a0c036bc575b09d97517ce0921839b_47d58a77a02edeb8d1972506c9ae6f7c.jpeg)
## **基本规则**
在学习如何实现FFI函数之前,我想先介绍一些基本的规则。你应该在设计的过程中牢记这些规则,因为缺少其中任何一个都可能引发各种bug,最终导致函数全面崩溃或内存泄漏。
### **规则1:一个指针,一个分配器**
你可能会认为内存分配只不过是调用一些操作系统API。然而实际上,获取一大块内存,写入缓存区是一项复杂且开销很大的操作。编译器和库开发人员很想应用各种优化,比如获得更大的内存块以避免频繁调用操作系统API,而且实现方式也各异。
你不应该假设调用库的人会使用某种类型的内存分配器。他们不一定会使用malloc,而且也不会受限于libc。换句话说,Rust代码分配的内存应该由Rust代码删除,越过FFI边界获取的指针应该交还给创建者去释放。如果使用malloc分配内存,请不要将其转换为Box,然后drop。我们应该通过调用Box::into_raw()获取的指针,不应该通过调用free来释放。
### **规则2:所有权**
Rust是一种内存安全语言,会明确指出所有权。如果在代码中看到Box<dyn Any>,你就知道在你drop Box之后,存储Any的内存会被立即释放。相反,如果看到void*,则无法判断是应该调用free释放内存,还是由其他人来释放这些内存(或许这些内存压根不需要被释放,因为它指向堆栈)。
在Rust中,将结构转化为原始指针的方法有一种命名约定。标准的库,比如Box、Arc、CStr和CString提供了as_ptr,还有一对into_raw和from_raw方法。并非每个结构都提供这三种方法,因此实际情况更加混乱。
我们来具体讨论一下这些库。首先是CString,它提供以上三种方法,as_ptr和into_raw方法都提供了相同类型的指针。然而,就像上面提到的void*一样,这些指针的所有权略有不同。
as_ptr方法以引用的形式接受&self。这意味着,在as_ptr返回后,CString实例依然会留在栈上,而数据的所有权也会保留。换句话说,返回的指针指向的数据仍归CString实例所有。一旦删除实例,指针就会悬空。在删除CString实例后,你永远不应再使用此指针。在安全的Rust中,指针的此属性由引用的生命周期(类似于指针)表示,并由编译器控制,但如果使用原始指针,一切都将变成未知。
与as_ptr不同,into_raw会通过值接受并销毁self。那么,会不会破坏释放内存?事实证明,into_raw不会调用drop方法。它会创建一个自己拥有的指针,然后将Rust分配器提供的内存块“泄漏”出来,脱离Rust编译器的控制范围。如果你只删除该指针,而不调用from_raw方法,就会引发内存泄漏。但是,它永远不会悬空(除非在调用from_raw之前修改或克隆它)。
如果你想让C暂时“借用”Rust的内存,则应该使用as_ptr。它有一个巨大的优势,因为 C 代码不必释放这块内存,而且还会限制指针的生命周期。但请不要将这个指针保存到某个全局结构中,或将其传递给另一个线程,也不应该将这样的指针作为函数调用的结果返回。
into_raw方法会将数据的所有权转移到C中。只要代码需要,它就可以保留指针,但请务必记得将它转移回Rust删除。
### **字符串的内存表示**
不幸的是,在Rust和语言C中,字符串的表示方式不同。C的字符串通常是char*指针,指向以 /0 结尾的char数组。而Rust则会保存字符数组及其长度。
由于这个原因,Rust的String和str类型与原始指针之间不应该互相转换。你应该使用CString和CStr中间类型来实现。通常,我们使用CString将Rust字符串传递给C代码,使用CStr将C的字符串转换为Rust的&str。请注意,这种转换并不一定会复制底层的数据。因此,通过CStr获得的&str会指向C分配的数组,而且它的生命周期与指针绑定。
注意:String:new会复制数据,但CStr::new不会。
## **项目设置**
### **如何将Rust和C连接起来**
网上有很多关于如何构建C代码,以及使用build.rs将C连接到Rust crate的资料,但是如何将Rust代码添加到C项目的文章却很少。相比之下,我更喜欢用C语言实现主要功能,并使用CMake作为构建系统。我希望CMake项目将Rust crate作为库,并根据Rust代码生成C的头文件。
### **通过CMake运行Cargo**
我建立了一个简单的CMake 3控制台应用程序。
首先,我们需要定义构建Rust库的命令和保存Rust成果物的位置:
```SQL
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CARGO_CMD RUSTFLAGS=-Zsanitizer=address cargo build -Zbuild-std --target x86_64-unknown-linux-gnu)
set(TARGET_DIR "x86_64-unknown-linux-gnu/debug")
else ()
set(CARGO_CMD cargo build --release)
set(TARGET_DIR "release")
endif ()
SET(LIB_FILE "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_DIR}/librust_lib.a")
```
对于熟悉Rust的人来说,这个构建crate调试版本的命令可能看起来有点古怪。我们完全可以使用cargo build来代替这个命令,但是我想利用Rust不稳定的地址清理器功能来确保内存不会被泄漏。
其次,我们需要自定义命令和目标,让它们根据命令输出结果。然后,我们可以定义一个名为rust_lib的静态导入库,并根据目标构建它:
```Plain Text
add_custom_command(OUTPUT ${LIB_FILE}
COMMENT "Compiling rust module"
COMMAND CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} ${CARGO_CMD}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/rust_lib)
add_custom_target(rust_lib_target DEPENDS ${LIB_FILE})
add_library(rust_lib STATIC IMPORTED GLOBAL)
add_dependencies(rust_lib rust_lib_target)
```
最后,我们可以使用将二进制文件与Rust库(以及其他必需的系统库)链接在一起。我们还在C代码中启用了地址清理器:
```PHP
target_compile_options(rust_c_interop PRIVATE -fno-omit-frame-pointer -fsanitize=address)
target_link_libraries(rust_c_interop PRIVATE Threads::Threads rust_lib ${CMAKE_DL_LIBS} -fno-omit-frame-pointer -fsanitize=address)
```
如此一来,运行CMake即可自动构建rust create,并与之链接。但是,我们还需要从C代码中调用Rust的方法。
### **生成C的头文件,并将它们添加到CMake项目中**
最简单的在Rust代码中获取C头文件的方法是使用cbingen库。
我们可以将以下代码添加到Rust crate的build.rs文件中,以检测Rust中定义的所有extern "C"函数,为其生成头文件定义,并保存到include/目录下:
```Swift
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let package_name = env::var("CARGO_PKG_NAME").unwrap();
let output_file = PathBuf::from(&crate_dir)
.join("include")
.join(format!("{}.h", package_name));
cbindgen::generate(&crate_dir)
.unwrap()
.write_to_file(output_file);
```
此外,我们还应该在Rust crate的根目录中创建cbindgen.toml文件,并指明language = "C"。
接下来,CMake需要在Rust crate的include文件夹中查找头文件:
```Shell
SET(LIB_HEADER_FOLDER "${CMAKE_CURRENT_SOURCE_DIR}/rust_lib/include")
set_target_properties(rust_lib
PROPERTIES
IMPORTED_LOCATION ${LIB_FILE}
INTERFACE_INCLUDE_DIRECTORIES ${LIB_HEADER_FOLDER})
```
**将Rust字符串传递到C的五种方式**
一切准备就绪。下面,我们来看看如何从Rust的数据中获取字符串,然后在C中使用。我们怎么才能安全地传递字符串,同时不会造成内存泄漏?
### **方法1:提供创建和删除方法**
如果不知道C代码需要使用字符串多久,就可以采用这种方式。为了将所有权移交给C,我们可以构建CString对象,并使用into_raw将其转换为指针。free方法只需要构建CString,再drop这个对象就可以释放内存:
```PHP
#[no_mangle]
pub extern fn create_string() -> *const c_char {
let c_string = CString::new(STRING).expect("CString::new failed");
c_string.into_raw() // Move ownership to C
}
/// # Safety
/// The ptr should be a valid pointer to the string allocated by rust
#[no_mangle]
pub unsafe extern fn free_string(ptr: *const c_char) {
// Take the ownership back to rust and drop the owner
let _ = CString::from_raw(ptr as *mut _);
}
```
不要忘记调用free_string,以避免内存泄漏:
```C++
const char* rust_string = create_string();
printf("1. Printed from C: %s\n", rust_string);
free_string(rust_string);
```
不要调用libc free方法,也不要尝试修改此类指针指向的数据。
这个方法虽然效果很好,但如果我们想在使用内存时释放Rust库,或者在不知道Rust库的代码中释放内存,该怎么办?你可以考虑以下三种方法。
### **方法2:分配缓冲区并复制数据**
还记得规则1吗?如果我们想在C中使用free方法释放内存,就应该使用malloc分配内存。但是,Rust怎么会知道malloc呢?一种解决方案是,“问一问”Rust需要多少内存,然后为它分配一个缓冲区:
```C++
size_t len = get_string_len();
char *buffer = malloc(len);
copy_string(buffer);
printf("4. Printed from C: %s\n", buffer);
free(buffer);
```
Rust只需要告诉我们缓冲区的大小,并小心翼翼地将Rust字符串复制到其中(注意不要漏掉末尾的字节0):
```PHP
#[no_mangle]
pub extern fn get_string_len() -> usize {
STRING.as_bytes().len() + 1
}
/// # Safety
/// The ptr should be a valid pointer to the buffer of required size
#[no_mangle]
pub unsafe extern fn copy_string(ptr: *mut c_char) {
let bytes = STRING.as_bytes();
let len = bytes.len();
std::ptr::copy(STRING.as_bytes().as_ptr().cast(), ptr, len);
std::ptr::write(ptr.offset(len as isize) as *mut u8, 0u8);
}
```
这个方法的优势在于,我们不必实现free_string,可以直接使用free。还有一个优点是,如有需要C代码也可以修改缓冲区(这就是我们使用*mut c_char,而不是*const c_char的原因)。
问题在于,我们仍然需要实现额外的方法get_string_len,而且还需要分配一块新内存,并复制数据(但其实CString::new也需要)。
如果你想将Rust字符串移动到C函数栈上分配的缓冲区,也可以使用此方法,但应该确保有足够的空间。
### **方法3:将内存分配器方法传递给Rust**
我们可以避免使用get_string_len方法吗?有没有其他方法在Rust中分配内存?一种简单的方法是将分配内存函数传递给Rust:
```C#
type Allocator = unsafe extern fn(usize) -> *mut c_void;
/// # Safety
/// The allocator function should return a pointer to a valid buffer
#[no_mangle]
pub unsafe extern fn get_string_with_allocator(allocator: Allocator) -> *mut c_char {
let ptr: *mut c_char = allocator(get_string_len()).cast();
copy_string(ptr);
ptr
}
```
上述示例使用了的copy_string,接下来我们可以使用get_string_with_allocator:
```C++
char* rust_string_3 = get_string_with_allocator(malloc);
printf("3. Printed from C: %s\n", rust_string_3);
free(rust_string_3);
```
这个方法与方法2相同,而且优缺点也一样。
但是,我们现在必须传递额外的参数allocator。其实,我们可以进行一些优化,将其保存到某个全局变量中,就可以避免向每个函数传递。
### **方法4:从Rust调用glibc**
如果我们的C代码会使用malloc/free来分配内存,则可以尝试在Rust代码中引入libc crate,尽管这种方式有点冒险:
```Properties files
#[no_mangle]
pub unsafe extern fn get_string_with_malloc() -> *mut c_char {
let ptr: *mut c_char = libc::malloc(get_string_len()).cast();
copy_string(ptr);
ptr
}
```
C代码不变:
```C++
char* rust_string_4 = get_string_with_malloc();
printf("4. Printed from C: %s\n", rust_string_4);
free(rust_string_4);
```
在这种方式下,我们不需要提供分配内存的方法,但是C代码也会受到很多限制。我们最好做好文档记录,尽量避免使用这种方式,除非我们确定百分百安全。
### **方法5:借用Rust字符串**
以上这些方法都是将数据的所有权传递给C。但如果我们不需要传递所有权呢?举个例子,Rust代码需要同步调用C方法,并向它传递一些数据。这时,可以考虑使用CString的as_ptr:
```C++
type Callback = unsafe extern fn(*const c_char);
#[no_mangle]
pub unsafe extern fn get_string_in_callback(callback: Callback) {
let c_string = CString::new(STRING).expect("CString::new failed");
// as_ptr() keeps ownership in rust unlike into_raw()
callback(c_string.as_ptr())
}
```
不幸的是,即便在这种情况下,CString:new也会复制数据(因为它需要在末尾添加字节0)。
C代码如下:
```C++
void callback(const char* string) {
printf("5. Printed from C: %s\n", string);
}
int main() {
get_string_in_callback(callback);
return 0;
}
```
如果有一个生命周期已知的C指针,则我们应该优先使用这种方式,因为它可以保证没有内存泄漏。
## **将C字符串传递给Rust的两种方法**
下面,我们来介绍两种反向操作的方法,即将C的字符串转换为Rust的类型。主要方法有以下两种:
- 将C字符串转换成&str,不复制数据;
- 复制数据并接收字符串。
这两种方法的示例相同,因为它们非常相似。实际上,方法2需要先使用方法1。
C代码如下。我们在堆上分配数据,但实际上我们也可以将指针传递给栈:
```C++
char *test = (char*) malloc(13*sizeof(char));
strcpy(test, "Hello from C");
print_c_string(test);
free(test);
```
Rust的实现如下:
```Swift
#[no_mangle]
/// # Safety
/// The ptr should be a pointer to valid String
pub unsafe extern fn print_c_string(ptr: *const c_char) {
let c_str = CStr::from_ptr(ptr);
let rust_str = c_str.to_str().expect("Bad encoding");
// calling libc::free(ptr as *mut _); causes use after free vulnerability
println!("1. Printed from rust: {}", rust_str);
let owned = rust_str.to_owned();
// calling libc::free(ptr as *mut _); does not cause after free vulnerability
println!("2. Printed from rust: {}", owned);
}
```
注意,此处我们使用了CStr,而不是CString。如果不是CString::into_raw创建的指针,请不要调用CString:from_raw。
这里还需要注意,&str引用的生命周期不是“静态”的,而是绑定到了c_str对象方法。Rust编译器会阻止你在该方法之外返回&str,或将其移动到全局变量/另一个线程,因为一旦C代码释放内存,&str引用就会变成非法。
如果需要在Rust中长时间保留数据的所有权,只需调用to_owned()即可获取字符串的副本。如果不想复制,则可以使用CStr,但我们应该确保C代码不会在字符串还在使用期间释放内存。
## **总结**
在本文中,我们讨论了Rust与C之间的互操作,并介绍了几种跨FFI边界传递数据的方法。这些方法不仅可用于传递字符串,也可用于其他数据,或者利用FFI将Rust连接到其他编程语言。
+37
View File
@@ -0,0 +1,37 @@
## 擅长领域
- 高性能 Web Service
- WebAssembly
- 命令行工具
- 网络编程
- 嵌入式设备
- 系统编程
## 优点
- 性能高
- 安全性好
- 无所畏惧的并发
## 数据类型
#### 标量类型
- 整数
- 值溢出,debug 模式会panic ,release 模式不会
- 浮点类型
- 布尔
- 字符
#### 复合类型
- 元组 tuple
- 数组
## 所有权
- 每个值同时只能有一个所有者
### 引用
### 借用
@@ -0,0 +1,37 @@
## 擅长领域
- 高性能 Web Service
- WebAssembly
- 命令行工具
- 网络编程
- 嵌入式设备
- 系统编程
## 优点
- 性能高
- 安全性好
- 无所畏惧的并发
## 数据类型
#### 标量类型
- 整数
- 值溢出,debug 模式会panic ,release 模式不会
- 浮点类型
- 布尔
- 字符
#### 复合类型
- 元组 tuple
- 数组
## 所有权
- 每个值同时只能有一个所有者
### 引用
### 借用
@@ -0,0 +1,378 @@
[嵌入式系统](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语言头文件,这就需要我们在实际开发中不断调整头文件的编写。
@@ -0,0 +1,378 @@
[嵌入式系统](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语言头文件,这就需要我们在实际开发中不断调整头文件的编写。