传递字符串

说明

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

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

出发点

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

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

代码示例

  1. pub mod unsafe_module {
  2. // other module content
  3. extern "C" {
  4. fn seterr(message: *const libc::c_char);
  5. fn geterr(buffer: *mut libc::c_char, size: libc::c_int) -> libc::c_int;
  6. }
  7. fn report_error_to_ffi<S: Into<String>>(
  8. err: S
  9. ) -> Result<(), std::ffi::NulError>{
  10. let c_err = std::ffi::CString::new(err.into())?;
  11. unsafe {
  12. // SAFETY: calling an FFI whose documentation says the pointer is
  13. // const, so no modification should occur
  14. seterr(c_err.as_ptr());
  15. }
  16. Ok(())
  17. // The lifetime of c_err continues until here
  18. }
  19. fn get_error_from_ffi() -> Result<String, std::ffi::IntoStringError> {
  20. let mut buffer = vec![0u8; 1024];
  21. unsafe {
  22. // SAFETY: calling an FFI whose documentation implies
  23. // that the input need only live as long as the call
  24. let written: usize = geterr(buffer.as_mut_ptr(), 1023).into();
  25. buffer.truncate(written + 1);
  26. }
  27. std::ffi::CString::new(buffer).unwrap().into_string()
  28. }
  29. }

优点

样例能保证下面三点:

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

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

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

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

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

缺点

或许没有?