首页 文章

编译时通用类型大小检查

提问于
浏览
16

我正在尝试为C集合库(Judy Arrays [1])编写Rust绑定,它只为存储指针宽度值提供了空间 . 我的公司有相当数量的现有代码,它使用这个空间直接存储非指针值,如指针宽度整数和小结构 . 我希望我的Rust绑定允许使用泛型类型安全地访问此类集合,但是无法使指针存储语义正常工作 .

我有一个使用 std::mem::transmute_copy() 来存储值的基本接口,但该函数显式不执行任何操作以确保源和目标类型的大小相同 . 我'm able to verify that collection type parameter is of a compatible size at run-time via an assertion, but I' d非常喜欢检查以某种方式在编译时 .

示例代码:

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项目,但它并不支持我想要的指针存储,而且我正在编写这些新的绑定,主要是作为一种学习练习 .

2 回答

  • 2

    编译时检查?

    有没有更好的方法来执行此操作,还是运行时检查Rust 1.0支持的最佳?

    一般来说, there are some hacky solutions 要做一些任意条件的编译时间测试 . 例如,有the static_assertions crate提供了一些有用的宏(包括一个类似于C的 static_assert 的宏) . 但是,这是hackyvery limited .

    在您的特定情况下,我还没有找到在编译时执行检查的方法 . 这里的根本问题是 you can't use mem::size_of or mem::transmute on a generic type . 相关问题:#43408#47966 . 出于这个原因, static_assertions 箱也不起作用 .

    如果你考虑一下,这也会给Rust程序员带来一些非常不熟悉的错误:在实例化具有特定类型的泛型函数时出错 . 这是C程序员所熟知的 - Rust的特征界限用于修复那些通常非常糟糕且无用的错误消息 . 在Rust世界中,人们需要将您的需求指定为特征限制:类似 where size_of::<T> == size_of::<usize>() .

    但是,目前这是不可能的 . 曾经有一个相当着名的"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::<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 ,而只在宏调用的具体类型上使用 size_of .

    所以......这显然远非完美 . 对于要在数据结构中使用的每种类型,库的用户必须调用一次 impl_as_big_as_usize . 但至少它是安全的:只要程序员只使用宏来实现特征,该特征实际上只适用于与 usize 具有相同大小的类型 . 此外,错误“特质绑定 AsBigAsUsize 不满意”是非常容易理解的 .


    运行时检查怎么样?

    正如bluss在评论中所说,在你的 assert! 代码中,有 no run-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!() 表达式有两个目的:

    • “隐藏” t 来自LLVM,这样LLVM可以't perform optimizations we don' t想要(比如删除整个函数)

    • 标记我们将要查看的生成的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<bool> 案例:您可以看到我们的第一个 asm!() 指令被编译,然后对 panic!() 进行无条件调用,之后什么都没有( ud2 只是说“程序永远不会到达这个位置, panic!() 发散”) .

    • foo<u64> 案例:您可以看到 #APP - #NO_APP 对(两个 asm!() 表达式),两者之间没有任何内容 .


    所以是的:编译器 removes the check completely .

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

  • -2

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

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

相关问题