传递字符串

说明

当传递字符串给FFI函数时,有以下4点需要遵守的原则:

  1. 让拥有的字符串生命周期尽可能长。
  2. 在转换时保持最小化unsafe区域代码。
  3. 如果C语言代码会修改字符串数据,那么使用Vec类型而不是CString
  4. 除非外部函数的API需要字符串的所有权,否则不要传给被调用的函数。

出发点

Rust有对C语言风格字符串的内置支持,如CStringCStr类型。不过,有多种不同途径从Rust函数传给FFI函数字符串的方法。

最佳实现是很简单的:用CSring最小化unsafe的代码区域。然而,第二个警告是对象必须生存足够长时间,意味着生命周期应该最大化。此外,在修改后双向传递CStirng类型的对象是未定义行为,这种情况需要额外的操作来完善。

代码示例

pub mod unsafe_module { // other module content extern "C" { fn seterr(message: *const libc::c_char); fn geterr(buffer: *mut libc::c_char, size: libc::c_int) -> libc::c_int; } fn report_error_to_ffi<S: Into<String>>( err: S ) -> Result<(), std::ffi::NulError>{ let c_err = std::ffi::CString::new(err.into())?; unsafe { // SAFETY: calling an FFI whose documentation says the pointer is // const, so no modification should occur seterr(c_err.as_ptr()); } Ok(()) // The lifetime of c_err continues until here } fn get_error_from_ffi() -> Result<String, std::ffi::IntoStringError> { let mut buffer = vec![0u8; 1024]; unsafe { // SAFETY: calling an FFI whose documentation implies // that the input need only live as long as the call let written: usize = geterr(buffer.as_mut_ptr(), 1023).into(); buffer.truncate(written + 1); } std::ffi::CString::new(buffer).unwrap().into_string() } }

优点

样例能保证下面三点:

  1. unsafe代码块尽可能的小。
  2. CString生命周期足够长
  3. 类型转换时发生的错误能够尽早地传播出来。

一个常见(在文档中很常见)的错误是在代码块的开头部分不定义变量。

pub mod unsafe_module { // other module content fn report_error<S: Into<String>>(err: S) -> Result<(), std::ffi::NulError> { unsafe { // SAFETY: whoops, this contains a dangling pointer! seterr(std::ffi::CString::new(err.into())?.as_ptr()); } Ok(()) } }

这样的代码会导致悬垂指针,因为CString的生命周期并没有因为创建指针而延长,不像创建一个引用那样。

另一个经常提到的问题是初始化一个全0的1K长度的向量很慢。然而,最新的Rust版本针对这种情况提供了一个宏调用zmalloc,和操作系统能返回全0内存的速度一样快。(真的很快)

缺点

或许没有?