rust和c传递字符串的七种方法--翻译

rust和c传递字符串的七种方法

本文主要介绍了rust和c之间传递字符串的一些方法,属于FFI跨语言调用范围。
知乎原文链接

设计FFI函数的原则

在考虑FFI函数绑定时,考虑如下的原则可以避免很多的内存泄露、程序崩溃的问题:

一个指针对应一个内存分配器

为了更加高效的管理内存,不同的内存分配器使用了不同的分配规则,比如一些内存池算法等。因此使用Rust分配的内存,由Rust进行释放。使用FFI获取到的指针指向的内存,由FFI进行释放。

例如:

  • c语言中使用malloc分配的内存,不要试图使用Rust中Box和Drop函数进行释放。
  • Rust中使用Box::into_raw()获取到的指针,不要使用c中的free函数进行释放。
重视所有权规则

Rust是一门内存安全的语言,拥有明确的所有权。比如你看到Box时,你知道存储Any的这块内存会在Box被drop的时候释放。但是当你看到void *的时候,这个时候你就迷糊了。不知道什么时候调用free释放这块内存,甚至当指针指向栈上内存时,你都不需要去释放它。

在Rust语言中Box、Arc、Cstr、CString等类型,提供了as_ptr()、into_raw()、from_raw()等函数, 但并不是所有的类型都提供了这三个函数。所以学习的过程中会存在一些困惑,但是弄懂它们又确实很关键。

CString

CString以上三个函数都有,但它们在所有权上有很大不同:

  • 调用as_ptr()函数,将会以引用的方式获得&str。也就是说没有发生所有权转移,所有权还是CString对象持有。如果CString对象释放了,获取到的指针就会变成悬垂指针,指向非法内存区域。
  • 调用into_raw()函数会获取指针,并且获取内存的所有权。并不会调用CString的drop函数。当你需要释放这个内存的时候,需要调用from_raw()函数。
    在FFI绑定的时候,有两种方式了。当你调用as_ptr获取到的指针,传递给c语言中的函数,c中的函数不用释放内存,内存还是由rust进行管理。这时在c中就要注意,如果rust释放了这个内存,指针就会无效。而且不要在c中将指针保存到全局变量或者一些结构体中使用,因为它可能已经是悬垂指针了。不要在c中把指针传递给其它线程使用。

使用into_raw()函数,指针指向的内存所有权就交给了C,但是要注意最终指针还是要传递回rust,调用from_raw()函数管理起内存,然后释放内存。

Rust和c中String对象的内存管理区别

在c中,字符串通过char *表示,以'\0'结尾。在rust中,字符串通过char数组和长度表示。

由于以上的区别,在FFI函数调用时,不能直接把rust中String和str类型获取到的指针传递给c使用,因为没有'\0'字符结尾。而是应该使用CString和CStr类型。

通常,CString用于传递rust中字符串到c中,而CStr用于将c中获取到的字符串转换成rust中的&str。使用CStr不会拷贝内存,内存还是由c管理,指向c分配的内存区域。

rust向c传递字符串的五种方法

下面的方法基于将rust编译成lib,然后在c中调用的场景。采用的方法是使用cbingen crate

方法1 在Rust端创建一个Create方法和Delete方法

当我们不知道c需要访问rust中字符串多长时间时,采用这种方法。通过CString调用into_raw方法,将内存所有权交给c。在释放时,rust Detele函数根据c传回的指针,调用CString的from_raw方法,重新接管内存。

rust代码如下:

rust 复制代码
#[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 _);
}

在c中不使用String时,调用free_string函数时避免内存泄露的关键。

c 复制代码
const char* rust_string = create_string();
printf("1. Printed from C: %s\n", rust_string);
free_string(rust_string);

注意不要调用free方法去释放rust_string指针,而且不要试图修改指针指向的内容。

这种方法很方便,但是也存在一些情况不适用。比如当我们在使用这块内存的时候想要卸载rust lib等。

方法2 分配内存并拷贝字符串

根据方法1,如果想要在c语言中,使用malloc申请内存,使用free释放内存。那么我们需要在rust库告诉c,传递的字符串大小,并且提供函数将rust中字符串拷贝到c申请的内存中。

在c中使用的方法如下:

rust中提供了两个函数,get_string_len获取字符串的长度(包括'\0'字符),在copy_string函数中接受c传递的指针,拷贝rust中字符串到内存供c使用。

rust 复制代码
#[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);
}

采用这种方法,由于内存是c malloc分配的,c可以直接修改内存中内容,调用free释放内存,不需要担心释放内存出现错误。

方法3 将c中的内存分配器传递给rust使用

为了避免方法2中调用get_string_len函数,我们可以将c中的内存分配器传递给rust使用。

在rust中代码如下:

rust 复制代码
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
}

在c中使用如下:

c 复制代码
char* rust_string_3 = get_string_with_allocator(malloc);
printf("3. Printed from C: %s\n", rust_string_3);
free(rust_string_3);

我们可以优化一下,避免每次都传递allocator给rust,将分配器函数传递给rust,注册到全局变量中。

方法4 在rust中使用libc包提供的函数

方法3存在一个问题,我们在rust中定义了Allocator类型的函数指针。但是我们怎么能确定c中使用的malloc就是这种定义呢,比如其它的alloc函数。

在rust中提供了libc crate,直接调用libc::malloc,就能解决这个问题。rust代码如下:

rust 复制代码
#[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不需要提供malloc函数了,但是也有一个巨大问题,从这段c代码根本看不出来调用了malloc分配内存。继而也就不知道需要调用free释放内存。可以通过详细的注释来解决,但是也不是一个好方法。

方法5 借用rust中的字符串

以上方法都是将所有权转移到了c代码中,但是在某些情况下,比如只是传递字符串的引用,然后同步调用c代码。这时候就不需要转移所有权到c了,只是借用就可以了。采用CString中的as_ptr方法。

rust 复制代码
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方法会拷贝内存,因为它会在STRING后添加'\0'字符。

c 复制代码
void callback(const char* string) {
    printf("5. Printed from C: %s\n", string);
}

int main() {
    get_string_in_callback(callback);
    return 0;
}

更加推荐采用这种方式,保证了内存不会泄露。

c向rust传递字符串的两种方法

存在两种方法向rust传递字符串:

  • 将其转换成rust中的&str,这时不存在内存拷贝
  • 将其转换成rust中的String,这时需要内存拷贝

比如下面一段c代码, 在堆上分配了13个char的内存(包括结尾的\0字符),将栈上的指针变量传递给了rust函数调用,最后free掉malloc申请的内存。字符串的生命周期由c管理。

c 复制代码
char *test = (char*) malloc(13*sizeof(char));
strcpy(test, "Hello from C");
print_c_string(test);
free(test);

rust代码实现如下:

rust 复制代码
#[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");
    // 如果在此行调用libc::free(ptr as *mut _); 会导致use after free问题
    // calling libc::free(ptr as *mut _); causes use after free vulnerability
    println!("1. Printed from rust: {}", rust_str);
    let owned = rust_str.to_owned();
    // 如果在此行调用libc::free(ptr as *mut _); 不会导致 use after free问题
    // calling libc::free(ptr as *mut _); does not cause after free vulnerability
    println!("2. Printed from rust: {}", owned);
}

这里要注意的是使用了CStr将指针转换成为&str, 而不是CString。除非这个指针ptr是CString::into_raw()返回的,否则不要使用CStr::from_ptr(ptr)转换指针。前面提到了into_raw()是将字符的内存释放从rust移交给c,from_ptr是将字符的内存释放从c移交给rust。

这里获取到的&str的生命周期由c管理,所以使用的时候要注意,如果c那边调用了free函数,&str就是非法指针了。如果想要rust持有&str,需要使用.to_owned方法,获取这个字符串的拷贝。

参考

文章中代码仓库
原文链接

相关推荐
wn5313 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
Hello-Mr.Wang15 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
救救孩子把18 分钟前
Java基础之IO流
java·开发语言
WG_1719 分钟前
C++多态
开发语言·c++·面试
宇卿.26 分钟前
Java键盘输入语句
java·开发语言
Amo Xiang35 分钟前
2024 Python3.10 系统入门+进阶(十五):文件及目录操作
开发语言·python
friklogff1 小时前
【C#生态园】提升C#开发效率:深入了解自然语言处理库与工具
开发语言·c#·区块链
重生之我在20年代敲代码2 小时前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql