【发布时间】:2020-07-21 05:06:42
【问题描述】:
我正在为(主要是 C 风格的)C++ 插件 SDK 编写 Rust 包装器。插件宿主是一个运行事件循环的图形桌面应用程序。该插件作为该事件循环的一部分定期被调用。每当发生这种情况时,插件都有控制权并可以调用任意主机函数。
我要包装的一个 C 函数返回一个原始指针。在该函数返回后,指针被保证是一个有效的 C 字符串,因此取消引用它是安全的。但是,在插件回调返回后(从而将控制权交还给主机),指针可能会失效。我如何为此编写一个符合人体工程学的函数包装器,它不会在某些时候导致未定义的行为,例如当消费者尝试在下一个事件循环周期中访问字符串时?
我考虑过以下方法:
1。返回一个拥有的字符串
我可以立即取消引用指针并将内容复制到拥有的CString:
pub fn get_string_from_host() -> CString {
let ptr: *const c_char = unsafe { ffi.get_string() };
unsafe { CStr::from_ptr(ptr).to_owned() }
}
这是冒昧的——也许我的包装器的消费者对获取拥有的字符串不感兴趣,因为他们只想进行比较(这甚至是我要说的主要用例)。复制字符串完全是浪费。
2。返回原始指针
pub fn get_string_from_host() -> *const c_char {
unsafe { ffi.get_string() }
}
这只是将问题转移给消费者。
3。返回一个CStr 引用(不安全的方法)
pub unsafe fn get_string_from_host<'a>() -> &'a CStr {
let ptr: *const c_char = ffi.get_string();
CStr::from_ptr(ptr)
}
这是不安全的,因为引用的生命周期不准确。在稍后的时间点访问引用可能会导致未定义的行为。将问题转移给消费者的另一种方式。
4。关闭而不是返回一些东西
pub fn with_string_from_host<T>(f: impl Fn(&CStr) -> T) -> T {
let ptr: *const c_char = unsafe { ffi.get_string() };
f(unsafe { CStr::from_ptr(ptr) })
}
pub fn consuming_function() {
let length = with_string_from_host(|s| s.to_bytes().len());
}
这行得通,但确实需要习惯。
这些解决方案都不是真正令人满意的。
有没有办法确保“立即”使用返回值,这意味着它不会存储在任何地方或永远不会超出调用者的范围?
这听起来像是引用/生命周期的工作,但我不知道有任何生命周期注释意味着“仅在当前堆栈帧中有效”。如果有,我会使用它(仅用于说明):
pub fn get_string_from_host() -> &'??? CStr {
let ptr: *const c_char = unsafe { ffi.get_string() };
unsafe { CStr::from_ptr(ptr) }
}
pub fn consuming_function() {
// For example, this shouldn't be possible in this case
let prolonged: &'static CStr = get_string_from_host();
// But this should
let owned = get_string_from_host().to_owned();
}
【问题讨论】:
-
如果在某些其他操作发生后该值变得无效,则调用者有责任正确使用它。 “立即”可能需要将其传递给函数进行处理。此功能不应规定无关紧要的规则。为了消除任何可能的失效情况,您必须制作一份副本并将其传回可以无限期使用的地方。
-
@tadman 因为如果这个函数保证它的返回值永远不会离开调用者的作用域,它可以安全地返回字符串作为引用(指向 C 内存)。它可以保证引用在很短的生命周期内保持有效这一事实。因此不必将其标记为
unsafe,这对 API 人体工程学非常有利。 -
复制对性能的影响有多大?您是否还可以考虑一下您正在使用这些数据做什么,并可能编写包装函数来安全地执行这些操作,从而将这个特定问题抽象出来?
-
@tadman 公平地说,对于许多实际用例来说,性能损失可能可以忽略不计,因为这些字符串相当小,而且这个函数不是从实时线程调用的。但它是一个通用库,所以我无法预测所有用例。出于同样的原因,我也发现很难预测消费者将如何处理这些数据并编写完美定制的包装函数。另外,不仅仅是这一个功能,实际上还有很多都遵循相同的模式。所以如果有更好的方法,我肯定会更喜欢。
-
我认为你被困在这里,要么以非常繁重的方式束缚调用者,使这个结果的限制已知(不安全),要么制作一个可以使用的安全副本然而。副本,如果便宜的话,肯定是最好的方法。