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方法,获取这个字符串的拷贝。