Rust FFI - 悬挂指针

Rust FFI - Dangling pointer

我在 Rust 库中工作,通过 C headers,在 Swift UI.

我可以从 Rust 中的 Swift 读取,但我不能立即将我刚读到的内容写入 Swift(因此来自 Rust)。

--

基本上,我在 String 中成功转换 *const i8hello world

但是相同的 String 无法被 as_ptr() 一致地处理(因此在 Swift 中被解析为 UTF-8)=>

  1. Swift 发送 hello world 作为 *const i8
  2. Rust 通过 let input: &str 成功处理它(#1 print in get_message())=> 正确打印 hello world
  3. 现在我无法再次将此 input &str 转换为指针:

Basically, why

  • "hello world".as_ptr() always have the same output and can be decoded by Swift
  • when input.as_ptr() has a different output every time called and can't never be decoded by Swift (where printing input rightly returns hello world)?

你们有什么想法吗?

#[derive(Debug)]
#[repr(C)]
pub struct MessageC {
    pub message_bytes: *const u8,
    pub message_len: libc::size_t,
}

/// # Safety
/// call of c_string_safe from Swift
/// => https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr
unsafe fn c_string_safe(cstring: *const i8) -> String {
    CStr::from_ptr(cstring).to_string_lossy().into_owned()
}

/// # Safety
/// call of c_string_safe from Swift
/// => https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr
/// on `async extern "C"` => <
#[no_mangle]
#[tokio::main] // allow async function, needed to call here other async functions (not this example but needed)
pub async unsafe extern "C" fn get_message(
    user_input: *const i8,
) -> MessageC {
    let input: &str = &c_string_safe(user_input);
    println!("from Swift: {}", input); // [consistent] from Swift: hello world
    println!("converted to ptr: {:?}", input.as_ptr()); // [inconsistent] converted to ptr: 0x60000079d770 / converted to ptr: 0x6000007b40b0
    println!("directly to ptr: {:?}", "hello world".as_ptr()); // [consistent] directly to ptr: 0x1028aaf6f
    MessageC {
        message_bytes: input.as_ptr(),
        message_len: input.len() as libc::size_t,
    }
}

您构建 MessageC 的方式不合理,return 是一个悬空指针。 get_message()中的代码等同于:

pub async unsafe extern "C" fn get_message(user_input: *const i8) -> MessageC {
    let _invisible = c_string_safe(user_input);
    let input: &str = &_invisible;
    // let's skip the prints
    let msg = MessageC {
        message_bytes: input.as_ptr(),
        message_len: input.len() as libc::size_t,
    };
    drop(_invisible);
    return msg;
}

希望这个表述突出了这个问题:c_string_safe() return 一个拥有的堆分配 String 在函数结束时被删除(及其数据释放)。 input 是一个切片,它引用由 String 分配的数据。在安全的 Rust 中,不允许 return 引用局部变量的切片,例如 input - 你必须 return String 本身或限制自己将切片向下传递给函数。

但是,您没有使用安全的 Rust,而是创建了指向堆分配数据的指针。现在您遇到了问题,因为一旦 get_message() returns,_invisible String 就会被释放,并且您正在 returning 的指针悬空。悬挂指针甚至可能看起来有效,因为释放没有义务从内存中清除数据,它只是将其标记为可用于将来的分配。但是那些未来的分配可以而且将会发生,也许来自不同的线程。因此,引用已释放内存的程序必然会行为不端,通常以不可预测的方式 - 这正是您所观察到的。

在全 Rust 代码中,您可以通过安全地 returning String 来解决问题。但是您正在执行 FFI,因此您必须将字符串缩减为 pointer/length 对。 Rust 允许你这样做,最简单的方法是调用 std::mem::forget() 来防止字符串被释放:

pub async unsafe extern "C" fn get_message(user_input: *const i8) -> MessageC {
    let mut input = c_string_safe(user_input);
    input.shrink_to_fit(); // ensure string capacity == len
    let msg = MessageC {
        message_bytes: input.as_ptr(),
        message_len: input.len() as libc::size_t,
    };
    std::mem::forget(input); // prevent input's data from being deallocated on return
    msg
}

但是现在你有一个不同的问题:get_message() 分配一个字符串,但是你如何解除分配呢?只是删除 MessageC 不会这样做,因为它只包含指针。 (通过实现 Drop 这样做可能是不明智的,因为您将它发送到 Swift 或其他任何东西。)解决方案是提供一个单独的函数,从中重新创建 String MessageC 并立即删除它:

pub unsafe fn free_message_c(m: MessageC) {
    // The call to `shrink_to_fit()` above makes it sound to re-assemble
    // the string using a capacity equal to its length
    drop(String::from_raw_parts(
        m.message_bytes as *mut _,
        m.message_len,
        m.message_len,
    ));
}

您应该在完成 MessageC 后调用此函数,即当 Swift 代码完成其工作时。 (您甚至可以将其设为 extern "C" 并从 Swift 调用它。)

最后,直接使用 "hello world".as_ptr() 是可行的,因为“hello world”是一个静态的 &str,它被嵌入到可执行文件中并且永远不会被释放。也就是说,它不是指向一个String,而是指向程序自带的一些静态数据