Rust 与 C 之间,传递字符串的 7 种方式!
Posted CSDN资讯
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Rust 与 C 之间,传递字符串的 7 种方式!相关的知识,希望对你有一定的参考价值。
摘要:Rust 得以在编程语言中火速崛起的原因之一,就是它能够与 C 语言进行互操作。因此在本文中,作者介绍了在 Rust 与 C 之间传递字符串的七种方式,这些方法不仅可用于传递字符串,也可用于其他数据。
原文链接:https://dev.to/kgrech/7-ways-to-pass-a-string-between-rust-and-c-4ieb
声明:本文为 CSDN 翻译,未经授权,禁止转载。
作者 | Konstantin Grechishchev译者 | 弯月
出品 | CSDN(ID:CSDNnews)
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成果物的位置:
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的静态导入库,并根据目标构建它:
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代码中启用了地址清理器:
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/目录下:
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文件夹中查找头文件:
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这个对象就可以释放内存:
#[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,以避免内存泄漏:
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需要多少内存,然后为它分配一个缓冲区:
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):
#[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:
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:
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,尽管这种方式有点冒险:
#[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代码不变:
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:
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代码如下:
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代码如下。我们在堆上分配数据,但实际上我们也可以将指针传递给栈:
char *test = (char*) malloc(13*sizeof(char));
strcpy(test, "Hello from C");
print_c_string(test);
free(test);
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");
// 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连接到其他编程语言。
希望本文能对你有所帮助,如有任何问题或反馈,请在下方留言。
— 推荐阅读 —
以上是关于Rust 与 C 之间,传递字符串的 7 种方式!的主要内容,如果未能解决你的问题,请参考以下文章
在C语言中,函数实参与形参之间的数据传递方式是( )传递方式。