vault backup: 2026-11-07 16:33:44
This commit is contained in:
@@ -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 类型的堆结构上。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
## **基本规则**
|
||||
|
||||
在学习如何实现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连接到其他编程语言。
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -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语言头文件,这就需要我们在实际开发中不断调整头文件的编写。
|
||||
|
||||
+378
@@ -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语言头文件,这就需要我们在实际开发中不断调整头文件的编写。
|
||||
|
||||
Reference in New Issue
Block a user