Rust FFI - 悬挂指针
Rust FFI - Dangling pointer
我在 Rust
库中工作,通过 C
headers,在 Swift
UI.
我可以从 Rust 中的 Swift 读取,但我不能立即将我刚读到的内容写入 Swift(因此来自 Rust)。
--
基本上,我在 String
中成功转换 *const i8
说 hello world
。
但是相同的 String
无法被 as_ptr()
一致地处理(因此在 Swift 中被解析为 UTF-8)=>
Swift
发送 hello world
作为 *const i8
Rust
通过 let input: &str
成功处理它(#1 print in get_message()
)=> 正确打印 hello world
- 现在我无法再次将此
input
&str
转换为指针:
- 指针无法被
Swift
解码
- “指针编码”在每次函数调用时都会发生变化(应该始终是相同的输出,对于
"hello world".as_ptr()
)
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
,而是指向程序自带的一些静态数据
我在 Rust
库中工作,通过 C
headers,在 Swift
UI.
我可以从 Rust 中的 Swift 读取,但我不能立即将我刚读到的内容写入 Swift(因此来自 Rust)。
--
基本上,我在 String
中成功转换 *const i8
说 hello world
。
但是相同的 String
无法被 as_ptr()
一致地处理(因此在 Swift 中被解析为 UTF-8)=>
Swift
发送hello world
作为*const i8
Rust
通过let input: &str
成功处理它(#1 print inget_message()
)=> 正确打印hello world
- 现在我无法再次将此
input
&str
转换为指针:
- 指针无法被
Swift
解码
- “指针编码”在每次函数调用时都会发生变化(应该始终是相同的输出,对于
"hello world".as_ptr()
)
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 printinginput
rightly returnshello 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
,而是指向程序自带的一些静态数据