编译时泛型类型大小检查
Compile-time generic type size check
我正在尝试为 C 集合库 (Judy Arrays [1]) 编写 Rust 绑定,它只为自己提供存储指针宽度值的空间。我公司有大量现有代码使用此 space 直接存储非指针值,例如指针宽度整数和小型结构。我希望我的 Rust 绑定允许使用泛型对此类集合进行类型安全访问,但我无法使指针存储语义正常工作。
我有一个使用 std::mem::transmute_copy()
存储值的基本接口,但该函数明确不执行任何操作以确保源类型和目标类型的大小相同。我能够通过断言在 运行 时验证集合类型参数的大小是否兼容,但我真的希望在编译时以某种方式进行检查。
示例代码:
pub struct Example<T> {
v: usize,
t: PhantomData<T>,
}
impl<T> Example<T> {
pub fn new() -> Example<T> {
assert!(mem::size_of::<usize>() == mem::size_of::<T>());
Example { v: 0, t: PhantomData }
}
pub fn insert(&mut self, val: T) {
unsafe {
self.v = mem::transmute_copy(&val);
mem::forget(val);
}
}
}
是否有更好的方法来做到这一点,或者这是 运行-时间检查最好的 Rust 1.0 支持?
(,解释为什么我不使用 mem::transmute()
。)
[1] 我知道现有的 rust-judy 项目,但它不支持我想要的指针存储,而且我编写这些新绑定主要是作为一种学习练习。
编译时检查?
Is there a better way to do this, or is this run-time check the best Rust 1.0 supports?
一般来说,有一些 hacky 解决方案 可以对任意条件进行某种编译时测试。例如,有 the static_assertions
crate which offers some useful macros (including one macro similar to C++'s static_assert
). However, this is hacky 和 非常有限 。
在您的特定情况下,我还没有找到在编译时执行检查的方法。这里的根本问题是你不能在通用类型上使用mem::size_of
或mem::transmute
。相关问题:#43408 and #47966。因此,static_assertions
箱子也不起作用。
如果你仔细想想,这也会导致一种 Rust 程序员非常不熟悉的错误:在实例化具有特定类型的泛型函数时出错。这对 C++ 程序员来说是众所周知的——Rust 的 trait bounds 用于修复那些通常非常糟糕且无用的错误消息。在 Rust 世界中,需要将您的要求指定为特征限制:类似于 where size_of::<T> == size_of::<usize>()
。
但是,目前这是不可能的。曾经有一个相当有名的"const-dependent type system" RFC which would allow these kinds of bounds, but got rejected for now. Support for these kinds of features are slowly but steadily progressing. "Miri" was merged into the compiler some time ago, allowing much more powerful constant evaluation. This is an enabler for many things, including the "Const Generics" RFC,居然合并了。暂未实施,但预计2018年或2019年落地。
不幸的是,它仍然无法启用您需要的那种绑定。比较两个 const 表达式是否相等,was purposefully left out of the main RFC 将在未来的 RFC 中解决。
因此,可以预料类似于 where size_of::<T> == size_of::<usize>()
的绑定最终将成为可能。但这在不久的将来应该不会发生!
解决方法
在你的情况下,我可能会引入一个 不安全 特征 AsBigAsUsize
。要实现它,您可以编写一个宏 impl_as_big_as_usize
来执行大小检查并实现特征。也许是这样的:
unsafe trait AsBigAsUsize: Sized {
const _DUMMY: [(); 0];
}
macro_rules! impl_as_big_as_usize {
($type:ty) => {
unsafe impl AsBigAsUsize for $type {
const _DUMMY: [(); 0] =
[(); (mem::size_of::<$type>() == mem::size_of::<usize>()) as usize];
// We should probably also check the alignment!
}
}
}
这使用了与 static_assertions
基本相同的技巧。这是可行的,因为我们从不在泛型类型上使用 size_of
,而只在宏调用的具体类型上使用。
所以...这显然远非完美。您的库的用户必须为他们想要在您的数据结构中使用的每种类型调用一次 impl_as_big_as_usize
。但至少它是安全的:只要程序员只使用宏来实现 trait,实际上 trait 只针对大小与 usize
相同的类型实现。另外,错误"trait bound AsBigAsUsize
is not satisfied"很好理解。
运行 时间检查呢?
正如bluss在评论中所说,在你的assert!
代码中,没有运行-time check,因为优化器常量折叠检查。让我们用这段代码测试该语句:
#![feature(asm)]
fn main() {
foo(3u64);
foo(true);
}
#[inline(never)]
fn foo<T>(t: T) {
use std::mem::size_of;
unsafe { asm!("" : : "r"(&t)) }; // black box
assert!(size_of::<usize>() == size_of::<T>());
unsafe { asm!("" : : "r"(&t)) }; // black box
}
疯狂的 asm!()
表达式有两个目的:
- 从 LLVM“隐藏”
t
,这样 LLVM 就不能执行我们不想要的优化(比如删除整个函数)
- 在我们将要查看的结果 ASM 代码中标记特定位置
使用夜间编译器编译它(在 64 位环境中!):
rustc -O --emit=asm test.rs
像往常一样,生成的汇编代码很难阅读;这是重要的地方(经过一些清理):
_ZN4test4main17he67e990f1745b02cE: # main()
subq , %rsp
callq _ZN4test3foo17hc593d7aa7187abe3E
callq _ZN4test3foo17h40b6a7d0419c9482E
ud2
_ZN4test3foo17h40b6a7d0419c9482E: # foo<bool>()
subq , %rsp
movb , 39(%rsp)
leaq 39(%rsp), %rax
#APP
#NO_APP
callq _ZN3std9panicking11begin_panic17h0914615a412ba184E
ud2
_ZN4test3foo17hc593d7aa7187abe3E: # foo<u64>()
pushq %rax
movq , (%rsp)
leaq (%rsp), %rax
#APP
#NO_APP
#APP
#NO_APP
popq %rax
retq
#APP
-#NO_APP
对是我们的asm!()
表达式。
foo<bool>
情况:你可以看到我们的第一个 asm!()
指令被编译,然后无条件地调用 panic!()
然后什么都没有(ud2
只是说“程序永远无法到达这个点,panic!()
发散”)。
foo<u64>
情况:您可以看到两个 #APP
-#NO_APP
对(两个 asm!()
表达式),中间没有任何内容。
是的:编译器完全删除检查。
如果编译器直接拒绝编译代码会更好。但是这样我们 至少 知道,没有 运行 时间开销。
与接受的答案相反,您可以在编译时检查!
诀窍是在使用优化进行编译时,在死代码路径中插入对未定义 C 函数的调用。如果您的断言失败,您将收到链接器错误。
我正在尝试为 C 集合库 (Judy Arrays [1]) 编写 Rust 绑定,它只为自己提供存储指针宽度值的空间。我公司有大量现有代码使用此 space 直接存储非指针值,例如指针宽度整数和小型结构。我希望我的 Rust 绑定允许使用泛型对此类集合进行类型安全访问,但我无法使指针存储语义正常工作。
我有一个使用 std::mem::transmute_copy()
存储值的基本接口,但该函数明确不执行任何操作以确保源类型和目标类型的大小相同。我能够通过断言在 运行 时验证集合类型参数的大小是否兼容,但我真的希望在编译时以某种方式进行检查。
示例代码:
pub struct Example<T> {
v: usize,
t: PhantomData<T>,
}
impl<T> Example<T> {
pub fn new() -> Example<T> {
assert!(mem::size_of::<usize>() == mem::size_of::<T>());
Example { v: 0, t: PhantomData }
}
pub fn insert(&mut self, val: T) {
unsafe {
self.v = mem::transmute_copy(&val);
mem::forget(val);
}
}
}
是否有更好的方法来做到这一点,或者这是 运行-时间检查最好的 Rust 1.0 支持?
(mem::transmute()
。)
[1] 我知道现有的 rust-judy 项目,但它不支持我想要的指针存储,而且我编写这些新绑定主要是作为一种学习练习。
编译时检查?
Is there a better way to do this, or is this run-time check the best Rust 1.0 supports?
一般来说,有一些 hacky 解决方案 可以对任意条件进行某种编译时测试。例如,有 the static_assertions
crate which offers some useful macros (including one macro similar to C++'s static_assert
). However, this is hacky 和 非常有限 。
在您的特定情况下,我还没有找到在编译时执行检查的方法。这里的根本问题是你不能在通用类型上使用mem::size_of
或mem::transmute
。相关问题:#43408 and #47966。因此,static_assertions
箱子也不起作用。
如果你仔细想想,这也会导致一种 Rust 程序员非常不熟悉的错误:在实例化具有特定类型的泛型函数时出错。这对 C++ 程序员来说是众所周知的——Rust 的 trait bounds 用于修复那些通常非常糟糕且无用的错误消息。在 Rust 世界中,需要将您的要求指定为特征限制:类似于 where size_of::<T> == size_of::<usize>()
。
但是,目前这是不可能的。曾经有一个相当有名的"const-dependent type system" RFC which would allow these kinds of bounds, but got rejected for now. Support for these kinds of features are slowly but steadily progressing. "Miri" was merged into the compiler some time ago, allowing much more powerful constant evaluation. This is an enabler for many things, including the "Const Generics" RFC,居然合并了。暂未实施,但预计2018年或2019年落地。
不幸的是,它仍然无法启用您需要的那种绑定。比较两个 const 表达式是否相等,was purposefully left out of the main RFC 将在未来的 RFC 中解决。
因此,可以预料类似于 where size_of::<T> == size_of::<usize>()
的绑定最终将成为可能。但这在不久的将来应该不会发生!
解决方法
在你的情况下,我可能会引入一个 不安全 特征 AsBigAsUsize
。要实现它,您可以编写一个宏 impl_as_big_as_usize
来执行大小检查并实现特征。也许是这样的:
unsafe trait AsBigAsUsize: Sized {
const _DUMMY: [(); 0];
}
macro_rules! impl_as_big_as_usize {
($type:ty) => {
unsafe impl AsBigAsUsize for $type {
const _DUMMY: [(); 0] =
[(); (mem::size_of::<$type>() == mem::size_of::<usize>()) as usize];
// We should probably also check the alignment!
}
}
}
这使用了与 static_assertions
基本相同的技巧。这是可行的,因为我们从不在泛型类型上使用 size_of
,而只在宏调用的具体类型上使用。
所以...这显然远非完美。您的库的用户必须为他们想要在您的数据结构中使用的每种类型调用一次 impl_as_big_as_usize
。但至少它是安全的:只要程序员只使用宏来实现 trait,实际上 trait 只针对大小与 usize
相同的类型实现。另外,错误"trait bound AsBigAsUsize
is not satisfied"很好理解。
运行 时间检查呢?
正如bluss在评论中所说,在你的assert!
代码中,没有运行-time check,因为优化器常量折叠检查。让我们用这段代码测试该语句:
#![feature(asm)]
fn main() {
foo(3u64);
foo(true);
}
#[inline(never)]
fn foo<T>(t: T) {
use std::mem::size_of;
unsafe { asm!("" : : "r"(&t)) }; // black box
assert!(size_of::<usize>() == size_of::<T>());
unsafe { asm!("" : : "r"(&t)) }; // black box
}
疯狂的 asm!()
表达式有两个目的:
- 从 LLVM“隐藏”
t
,这样 LLVM 就不能执行我们不想要的优化(比如删除整个函数) - 在我们将要查看的结果 ASM 代码中标记特定位置
使用夜间编译器编译它(在 64 位环境中!):
rustc -O --emit=asm test.rs
像往常一样,生成的汇编代码很难阅读;这是重要的地方(经过一些清理):
_ZN4test4main17he67e990f1745b02cE: # main()
subq , %rsp
callq _ZN4test3foo17hc593d7aa7187abe3E
callq _ZN4test3foo17h40b6a7d0419c9482E
ud2
_ZN4test3foo17h40b6a7d0419c9482E: # foo<bool>()
subq , %rsp
movb , 39(%rsp)
leaq 39(%rsp), %rax
#APP
#NO_APP
callq _ZN3std9panicking11begin_panic17h0914615a412ba184E
ud2
_ZN4test3foo17hc593d7aa7187abe3E: # foo<u64>()
pushq %rax
movq , (%rsp)
leaq (%rsp), %rax
#APP
#NO_APP
#APP
#NO_APP
popq %rax
retq
#APP
-#NO_APP
对是我们的asm!()
表达式。
foo<bool>
情况:你可以看到我们的第一个asm!()
指令被编译,然后无条件地调用panic!()
然后什么都没有(ud2
只是说“程序永远无法到达这个点,panic!()
发散”)。foo<u64>
情况:您可以看到两个#APP
-#NO_APP
对(两个asm!()
表达式),中间没有任何内容。
是的:编译器完全删除检查。
如果编译器直接拒绝编译代码会更好。但是这样我们 至少 知道,没有 运行 时间开销。
与接受的答案相反,您可以在编译时检查!
诀窍是在使用优化进行编译时,在死代码路径中插入对未定义 C 函数的调用。如果您的断言失败,您将收到链接器错误。