如何将 Rust 的不可变引用传递给不使用 const(即使它应该)的 C-API?
How do I pass a non-mutable reference from Rust to a C-API that doesn't use const (even though it should)?
我有一个 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
-correctness and thus has a type signature of usize http_request_bytes_received(*http_request_t)
which is dutifully converted by bindgen
到 http_request_bytes_received(*mut http_request_t) -> usize
没有那么严格。
现在我可以摆脱它了,但是从 &T
到 *mut T
的转换可以 easily lead to undefined behaviour (这是一个令人讨厌的转换)。但它可能没问题,因为 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*
,编译器不能假设没有共享者。事实上,编译器甚至不能断言它指向有效地址(它可能是空指针或结束指针)。除非代码明确写入指针,否则 C 编译器无法生成存储指令到内存位置。
T*
在C端的较弱含义意味着它不会违反Rust关于&T
语义的假设。您可以安全地将 &T
转换为 *mut T
并将其传递给 C,前提是 C 端从不修改指针指向的内存。
请注意,您可以指示 C 编译器该指针不会与 T * restrict
的任何其他内容混为一谈,但由于您提到的 C 代码对 const
的正确性并不严格,它可能也不使用 restrict
。
我有一个 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
-correctness and thus has a type signature of usize http_request_bytes_received(*http_request_t)
which is dutifully converted by bindgen
到 http_request_bytes_received(*mut http_request_t) -> usize
没有那么严格。
现在我可以摆脱它了,但是从 &T
到 *mut T
的转换可以 easily lead to undefined behaviour (这是一个令人讨厌的转换)。但它可能没问题,因为 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*
,编译器不能假设没有共享者。事实上,编译器甚至不能断言它指向有效地址(它可能是空指针或结束指针)。除非代码明确写入指针,否则 C 编译器无法生成存储指令到内存位置。
T*
在C端的较弱含义意味着它不会违反Rust关于&T
语义的假设。您可以安全地将 &T
转换为 *mut T
并将其传递给 C,前提是 C 端从不修改指针指向的内存。
请注意,您可以指示 C 编译器该指针不会与 T * restrict
的任何其他内容混为一谈,但由于您提到的 C 代码对 const
的正确性并不严格,它可能也不使用 restrict
。