我有一个C-API的包装器:
#[repr(transparent)]
pub struct Request(http_request_t);
这个包装器提供了几种与请求交互的方法:
impl Request {
pub fn bytes_received(&self) -> usize {
unsafe {
http_request_bytes_received(&self.0 as *const http_request_t)
}
}
}
不幸的是,C-API对const
的正确性不那么严格,因此具有usize http_request_bytes_received(*http_request_t)
的类型签名,该类型签名由bindgen
尽职尽责地转换为http_request_bytes_received(*mut http_request_t) -> usize
。
现在我可以摆脱这种情况,但从&T
到*mut T
的转换很容易导致未定义的行为(这是一个糟糕的转换(。但由于http_request_bytes_received
不突变http_request_t
,这可能是可以的。
一个可能的替代方案是使用UnsafeCell
,因此http_request_t
是内部可变的:
#[repr(transparent)]
pub struct Request(UnsafeCell<http_request_t>);
impl Request {
pub fn bytes_received(&self) -> usize {
unsafe {
http_request_bytes_received(self.0.get())
}
}
}
这种方法合理吗?会有严重的负面影响吗?
(我想这可能会限制一些Rust优化,并使Request
!Sync
(
简单答案:只需将其强制转换为*mut T
并传递给C.
长答案:
最好先了解为什么将*const T
强制转换为*mut T
容易出现未定义的行为。
Rust的内存模型确保&mut T
不会与其他任何东西别名,因此编译器可以自由地完全删除T,然后恢复其内容,而程序员无法观察到这种行为。如果&mut T
和&T
共存并指向同一位置,则会出现未定义的行为,因为如果您从&T
中读取,而编译器会对&mut T
进行破坏,会发生什么?类似地,如果您有&T
,编译器会假设没有人会修改它(通过UnsafeCell
排除内部可变性(,如果它指向的内存被修改,则会出现未定义的行为。
有了背景,很容易理解为什么*const T
到*mut T
是危险的——您不能取消引用生成的指针。如果你取消引用*mut T
,你就得到了一个&mut T
,它将是UB。但是,强制转换操作本身是安全的,您可以安全地将*mut T
强制转换回*const T
并取消引用它
这是Rust语义;在C侧,关于T*
的保证是非常弱的。如果您持有T*
,编译器不能假设没有共享程序。事实上,编译器甚至不能断言它指向有效地址(它可能为null或超过结束指针(。除非代码显式写入指针,否则C编译器无法生成指向内存位置的存储指令。
T*
在C端的语义较弱,这意味着它不会违反Rust关于&T
语义的假设。您可以安全地将&T
强制转换为*mut T
并将其传递给C,前提是C端永远不会修改指针指向的内存。
请注意,您可以指示C编译器,指针不会与T * restrict
的任何其他代码别名,但由于您提到的C代码对const
的正确性并不严格,因此它可能也不使用restrict
。