接受字符串

说明

当通过FFI的指针接受字符串时,有两条需要遵守的原则:

  1. 保持对外部字符串的借用,而不是直接复制一份。
  2. 在转换数据类型时最小化unsafe的代码区域。

出发点

Rust有对C语言风格字符串的内置支持,如CStringCStr类型。然而,有多种不同途径接受外部传入的字符串。

最佳实现是很简单的:用CStr最小化unsafe的代码区域,然后创建一个借用的切片。如果需要拥有其所有权的String,对字符串切片调用to_string()方法。

代码示例

  1. pub mod unsafe_module {
  2. // other module content
  3. #[no_mangle]
  4. pub extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
  5. let level: crate::LogLevel = match level { /* ... */ };
  6. let msg_str: &str = unsafe {
  7. // SAFETY: accessing raw pointers expected to live for the call,
  8. // and creating a shared reference that does not outlive the current
  9. // stack frame.
  10. match std::ffi::CStr::from_ptr(msg).to_str() {
  11. Ok(s) => s,
  12. Err(e) => {
  13. crate::log_error("FFI string conversion failed");
  14. return;
  15. }
  16. }
  17. };
  18. crate::log(msg_str, level);
  19. }
  20. }

优点

样例能保证下面两点:

  1. unsafe代码块尽可能的小。
  2. 无法记录生命周期的指针转变为可以记录追踪的共享引用。

考虑另一种实现,也就是字符串被实际拷贝一份的情况:

  1. pub mod unsafe_module {
  2. // other module content
  3. pub extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
  4. // DO NOT USE THIS CODE.
  5. // IT IS UGLY, VERBOSE, AND CONTAINS A SUBTLE BUG.
  6. let level: crate::LogLevel = match level { /* ... */ };
  7. let msg_len = unsafe { /* SAFETY: strlen is what it is, I guess? */
  8. libc::strlen(msg)
  9. };
  10. let mut msg_data = Vec::with_capacity(msg_len + 1);
  11. let msg_cstr: std::ffi::CString = unsafe {
  12. // SAFETY: copying from a foreign pointer expected to live
  13. // for the entire stack frame into owned memory
  14. std::ptr::copy_nonoverlapping(msg, msg_data.as_mut(), msg_len);
  15. msg_data.set_len(msg_len + 1);
  16. std::ffi::CString::from_vec_with_nul(msg_data).unwrap()
  17. }
  18. let msg_str: String = unsafe {
  19. match msg_cstr.into_string() {
  20. Ok(s) => s,
  21. Err(e) => {
  22. crate::log_error("FFI string conversion failed");
  23. return;
  24. }
  25. }
  26. };
  27. crate::log(&msg_str, level);
  28. }
  29. }

这份代码与第一版相比有两个方面缺点:

  1. 有更多的unsafe代码,更加不灵活。
  2. 由于调用大量的算法,这个版本有一个会导致Rust的未定义行为(undefined behaviour)的bug。

这里的bug是一个简单的指针计算的错误:字符串被拷贝走msg_len个字节。然而没有包括在末尾的NUL终止符。

向量长度将会被设置为未做填充字符串的长度而不是末尾填一个0的调整后大小。因此,向量内的最后一个字节是没有初始化的内存。当最终创建CString时,其读取向量将会导致未定义行为!

像很多问题一样,这是很难查到的。有些时候它因为字符串不是UTF-8编码而产生恐慌,有时它又会在末尾放一个奇怪的字符,有时它会完全崩溃掉。

缺点

或许没有?