编译时泛型类型大小检查

Posted

技术标签:

【中文标题】编译时泛型类型大小检查【英文标题】:Compile-time generic type size check 【发布时间】:2015-07-31 13:40:55 【问题描述】:

我正在尝试为 C 集合库 (Judy Arrays [1]) 编写 Rust 绑定,它只为自己提供存储指针宽度值的空间。我的公司有相当多的现有代码使用这个空间来直接存储非指针值,例如指针宽度整数和小型结构。我希望我的 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 支持的吗?

(Related question,解释我为什么不使用mem::transmute()。)

[1] 我知道现有的 rust-judy 项目,但它不支持我想要的指针存储,而且我编写这些新绑定主要是作为学习练习。

【问题讨论】:

这不起作用。它复制出val 的第一个单词并将其存储在v 中。哦,如果您想存储一个指针,请存储一个指向实际存在的东西的指针——例如指向Box&lt;T&gt; 中的T 的指针. “复制val的第一个单词”是我想要的,只要val的类型正好是单词大小。这里的目标是使用如此存储的数据与现有 C 代码进行 FFI 互操作。 我不认为 rust 现在可以限制T 的大小。不过 assert 当然是单态化的,在编译时编译,所以至少没有开销。 关于assert! 在编译时解析为无操作或panic! 的好点。如果这种运行时检查实际上是 Rust 1.0 可以做的最好的,我会接受它作为答案! 你也可以写一些#[test]s,其中包含那些assert!s。 【参考方案1】:

编译时检查?

有没有更好的方法来做到这一点,或者这个运行时检查是最好的 Rust 1.0 支持的吗?

一般来说,有一些 hacky 解决方案可以对任意条件进行某种编译时测试。例如,the static_assertions crate 提供了一些有用的宏(包括一个类似于 C++ 的static_assert 的宏)。但是,这是hacky 并且非常有限

在您的特定情况下,我还没有找到在编译时执行检查的方法。这里的根本问题是您不能在泛型类型上使用mem::size_ofmem::transmute。相关问题:#43408 和 #47966。因此,static_assertions crate 也不起作用。

如果你仔细想想,这也会导致 Rust 程序员非常不熟悉的一种错误:实例化具有特定类型的泛型函数时的错误。这对于 C++ 程序员来说是众所周知的——Rust 的 trait bound 用于修复那些通常非常糟糕和无用的错误消息。在 Rust 世界中,需要将您的要求指定为 trait bound:类似于where size_of::&lt;T&gt; == size_of::&lt;usize&gt;()

但是,目前这是不可能的。曾经有一个相当有名的"const-dependent type system" RFC 允许这种界限,但现在被拒绝了。对这些类型的功能的支持正在缓慢而稳步地进行。 “Miri” 不久前被合并到编译器中,允许更强大的常量评估。这是许多事情的推动者,包括the "Const Generics" RFC,它实际上已被合并。暂未实施,但预计2018年或2019年落地。

不幸的是,它仍然无法启用您需要的那种绑定。比较两个 const 表达式是否相等,was purposefully left out of the main RFC 将在未来的 RFC 中解决。

因此可以预期类似于where size_of::&lt;T&gt; == size_of::&lt;usize&gt;() 的绑定最终将成为可能。但这不应该在不久的将来发生!


解决方法

在你的情况下,我可能会引入一个 unsafe trait 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满足”这个错误也很好理解。


运行时检查呢?

正如 bluss 在 cmets 中所说,在您的 assert! 代码中,没有运行时检查,因为优化器会不断折叠检查。让我们用这段代码测试一下这个语句:

#![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    $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&lt;bool&gt; 案例:您可以看到我们的第一条asm!() 指令已编译,然后对panic!() 进行无条件调用,然后什么也没有(ud2 只是说“程序永远无法到达这个位置, panic!() 发散”)。 foo&lt;u64&gt; 案例:您可以看到两个 #APP-#NO_APP 对(两个 asm!() 表达式),中间没有任何内容。

所以是的:编译器完全删除检查

如果编译器拒绝编译代码会更好。但这种方式我们至少知道,没有运行时开销。

【讨论】:

【参考方案2】:

与接受的答案相反,您可以在编译时检查!

诀窍是在使用优化进行编译时,在死代码路径中插入对未定义 C 函数的调用。如果您的断言失败,您将收到链接器错误。

【讨论】:

虽然这个答案可能是正确的,也可能不是正确的,但就目前的形式而言,它是如此广泛且缺乏一个很难说的例子,这也意味着人们很难实际使用这个建议。

以上是关于编译时泛型类型大小检查的主要内容,如果未能解决你的问题,请参考以下文章

java Java的获取编译时泛型工具

泛型学习笔记

Kotlin泛型总结 ★ ( 泛型类 | 泛型参数 | 泛型函数 | 多泛型参数 | 泛型类型约束 | 可变参数结合泛型 | out 协变 | in 逆变 | reified 检查泛型参数类型 )

Java 泛型泛型用法 ( 泛型类用法 | 泛型方法用法 | 泛型通配符 ? | 泛型安全检查 )

Java之泛型

TypeToken获取运行时泛型类型