编译时通用类型大小检查

时间:2015-05-19 15:57:28

标签: pointers rust ffi

我试图为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项目,但它并不支持我想要的指针存储,而且我将这些新的绑定作为一个学习练习反正。

2 个答案:

答案 0 :(得分:2)

编译时检查?

  

有更好的方法可以做到这一点,还是运行时检查最好的Rust 1.0支持?

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

在您的特定情况下,我还没有找到在编译时执行检查的方法。这里的根本问题是您无法在通用类型上使用mem::size_ofmem::transmute。相关问题:#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,而只在宏调用的具体类型上使用。{/ p>

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

运行时检查怎么样?

正如bluss在评论中所说,在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<bool>案例:您可以看到我们编写了第一条asm!()指令,然后对panic!()进行了无条件调用,之后没有任何内容(ud2只是说“该计划永远不会到达这个地方,panic!()分歧”。)
  • foo<u64>案例:您可以看到#APP - #NO_APP对(两个asm!()表达式),两者之间没有任何内容。

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

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

答案 1 :(得分:-2)

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

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