且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

编译时通用类型大小检查

更新时间:2021-09-01 01:26:51

编译时检查?

是否有更好的方法来执行此操作,或者此运行时检查是否是Rust 1.0***的支持?

Is there a better way to do this, or is this run-time check the best Rust 1.0 supports?

通常,有一些骇人听闻的解决方案可以对任意条件进行某种编译时测试.例如,有板条箱,其中提供了一些有用的宏(包括一个类似于C ++的宏) static_assert).但是,这是 hacky 非常有限 .

In general, there are some hacky solutions to do some kind of compile time testing of arbitrary conditions. For example, there is the static_assertions crate which offers some useful macros (including one macro similar to C++'s static_assert). However, this is hacky and very limited.

在您的特定情况下,我还没有找到在编译时执行检查的方法.这里的根本问题是您不能在通用类型上使用mem::size_ofmem::transmute .相关问题:#43408

In your particular situation, I haven't found a way to perform the check at compile time. The root problem here is that you can't use mem::size_of or mem::transmute on a generic type. Related issues: #43408 and #47966. For this reason, the static_assertions crate doesn't work either.

如果考虑到这一点,这还将导致Rust程序员非常不熟悉的一种错误:在实例化具有特定类型的泛型函数时发生的错误.这对于C ++程序员是众所周知的-Rust的特征范围用于修复那些通常非常糟糕和无用的错误消息.在Rust的世界中,需要将您的需求指定为特征绑定:类似where size_of::<T> == size_of::<usize>()的东西.

If you think about it, this would also allow a kind of error very unfamiliar to Rust programmers: an error when instantiating a generic function with a specific type. This is well known to C++ programmers -- Rust's trait bounds are used to fix those often very bad and unhelpful error messages. In the Rust world, one would need to specify your requirement as trait bound: something like where size_of::<T> == size_of::<usize>().

但是,目前这是不可能的.曾经有一个相当著名的依赖于常量的类型系统" RFC 允许这样的界限,但现在被拒绝了.对这些功能的支持正在缓慢但稳定地发展. "Miri"在一段时间前已合并到编译器中,从而可以进行更强大的常量评估.这是许多事情的促成因素,包括实际上已合并的"Const Generics" RFC .它尚未实施,但预计将在2018或2019年着陆.

However, this is currently not possible. There once was a fairly famous "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, which was actually merged. It is not yet implemented, but it is expected to land in 2018 or 2019.

不幸的是,它仍然无法启用所需的绑定类型.比较两个const表达式是否相等,

Unfortunately, it still doesn't enable the kind of bound you need. Comparing two const expressions for equality, was purposefully left out of the main RFC to be resolved in a future RFC.

因此,可以预料的是,类似于where size_of::<T> == size_of::<usize>()的绑定最终将是可能的.但这不应该在不久的将来得到预期!

So it is to be expected that a bound similar to where size_of::<T> == size_of::<usize>() will eventually be possible. But this shouldn't be expected in the near future!

在您的情况下,我可能会介绍一个不安全特征AsBigAsUsize.要实现它,您可以编写一个宏impl_as_big_as_usize,它执行大小检查并实现特征.也许是这样的:

In your situation, I would probably introduce an unsafe trait AsBigAsUsize. To implement it, you could write a macro impl_as_big_as_usize which performs a size check and implements the trait. Maybe something like this:

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,而仅在宏调用的具体类型上使用了.

This uses basically the same trickery as static_assertions is using. This works, because we never use size_of on a generic type, but only on concrete types of the macro invocation.

所以...这显然还不完美.库用户必须为要在数据结构中使用的每种类型调用一次impl_as_big_as_usize.但这至少是安全的:只要程序员仅使用宏来隐含该特征,则实际上仅对与usize大小相同的类型实现该特征.此外,错误特质限制AsBigAsUsize不满足"是很容易理解的.

So... this is obviously far from perfect. The user of your library has to invoke impl_as_big_as_usize once for every type they want to use in your data structure. But at least it's safe: as long as programmers only use the macro to impl the trait, the trait is in fact only implemented for types that have the same size as usize. Also, the error "trait bound AsBigAsUsize is not satisfied" is very understandable.

就像评论中所说的那样,在您的assert!代码中,没有运行时检查 ,因为优化程序会不断地折叠检查.让我们用以下代码测试该语句:

As bluss said in the comments, in your assert! code, there is no run-time check, because the optimizer constant-folds the check. Let's test that statement with this code:

#![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!()表达式有两个用途:

The crazy asm!() expressions serve two purposes:

  • 隐藏"来自LLVM的t,这样LLVM无法执行我们不希望的优化(例如删除整个功能)
  • 在我们将要查看的结果ASM代码中标记特定的地方
  • "hiding" t from LLVM, such that LLVM can't perform optimizations we don't want (like removing the whole function)
  • marking specific spots in the resulting ASM code we'll be looking at

使用夜间编译器对其进行编译(在64位环境中!):

Compile it with a nightly compiler (in a 64 bit environment!):

rustc -O --emit=asm test.rs

像往常一样,生成的汇编代码很难阅读;这里是重要的地方(进行一些清理):

As usual, the resulting assembly code is hard to read; here are the important spots (with some cleanup):

_ZN4test4main17he67e990f1745b02cE:  # main()
    subq    $40, %rsp
    callq   _ZN4test3foo17hc593d7aa7187abe3E
    callq   _ZN4test3foo17h40b6a7d0419c9482E
    ud2

_ZN4test3foo17h40b6a7d0419c9482E: # foo<bool>()
    subq    $40, %rsp
    movb    $1, 39(%rsp)
    leaq    39(%rsp), %rax
    #APP
    #NO_APP
    callq   _ZN3std9panicking11begin_panic17h0914615a412ba184E
    ud2

_ZN4test3foo17hc593d7aa7187abe3E: # foo<u64>()
    pushq   %rax
    movq    $3, (%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!()表达式),中间没有任何内容.
  • The foo<bool> case: you can see that our first asm!() instruction is compiled, then an unconditioned call to panic!() is made and afterwards comes nothing (ud2 just says "the program can never reach this spot, panic!() diverges").
  • The foo<u64> case: you can see both #APP-#NO_APP pairs (both asm!() expressions) without anything in between.

是的:编译器完全删除了支票.

如果编译器拒绝编译代码,那就更好了.但是至少我们 这样知道,没有运行时开销.

It would be way better if the compiler would just refuse to compile the code. But this way we at least know, that there's no run-time overhead.