玩转Rust高级应用 怎么理解在标准库中,有一个std::intrinsics模块,它里面包含了一系列的编译器内置函数
内置函数
在标准库中,有一个std::intrinsics模块,它里面包含了一系列的编译器内置函 数。这些函数都有一个extern"rust-intrinsic"修饰,它们看起来都像一种特殊的 FFI 外部函数,大家打开标准库的源代码src/core/intrinsics.rs,可以看到这些函数 根本没有函数体,因为它们的实现是在编译器内部,而不是在标准库内部。
调用它们的时候 都必须使用unsafe 才可以。编译器见到这些函数,就知道应该生成什么样的代码,而不是像 普通函数调用一样处理。另外,intrinsics 是藏在一个feature gate后面的,这个feature 可能 永远不会稳定,这些函数就不是准备直接提供给用户使用的。一般标准库会在这些函数基础 上做一个更合适的封装给用户使用。
下面就在这些函数中挑一部分作介绍。
transmute
fn transmute<T,U>(e:T)->U函数可以执行强制类型转换。把一个T 类型参 数转换为U类型返回值,转换过程中,这个参数的内部二进制表示不变。但是有一个约束条 件,即size_of::()==size_of::() 。 如果不符合这个条件,会发生编译错误。 transmute_copy的作用跟它类似,区别是,参数类型是一个借用为&T。
一般情况下,我们也可以用as 做类型转换,把&T 类型指针转换为裸指针,然后再转换为&U类型的指针。这样也可以实现类似的功能。但是用户自己实现的泛型函数有一 个缺陷,即无法在where 条件中自己表达size_of::()==size_of::() 。而 transmute作为一个内置函数就可以实现这样的约束。
transmute和 transmute_copy在 std::mem 块中重新导出。用户如果需要,请使用这个模块,而不是std::intrinsics模块。下面用一个示例演示一下Vec 类型的二进制表示是怎样的:
fn main(){
let x =vec![1,2,3,4,5];
unsafe{let t:(usize,usize,usize)=std::mem::transmute_copy(&x);println!("{}{}{}",t.0,t.1,t.2);}
}
上面的例子中,我们调用了transmute_copy, 因此参数类型是&Vec 。假如我们用 transmute 函数,参数类型就必须是Vec, 区别在于,参数会被move 进入这个函数中, 在后面就不能继续使用了。在调用transmute_copy 函数的时候,必须显示指定返回值类型,因为它是泛型函数,返回值类型可以有多种多样的无穷变化,只要满足size_of:: ()==size_of::() 条件,都可以完成类型转换。
所以编译器自己是无法自动推理出 返回值类型的。在上例中,我们的返回值类型是包含三个usize 的 tuple类型。这是因为, Vec 中实际包含了3个成员,一个是指向堆上的指针,一个是指向内存空间的总大小,还有 一个是实际使用了的元素个数,因此这个类型转换从编译器看来是满足“占用内存空间相同” 这一条件的。
编译执行,我们就可以看到Vec 内部的具体内存表示了。执行结果为:
639392055
intrinsics模块里面有几个与内存读写相关的函数,比如copy 、copy_nonoverla-pping 、write_bytes 、move_val_init 、volatile_load 等。这些函数又在std::ptr/std::mem 模块中做了个简单封装,然后暴露出来给用户使用。下面挑其中几个重要 的函数介绍。
1.copy
copy 的完整签名是unsafe fn copy(src:*const T,dst:mut T,count:usize)。 它做的就是把src 指向的内容复制到dst 中去。它跟C 语言里面的 memmove类似,都假设src 和 dst 指向的内容可能有重叠。区别是memmove 的参数是 void 类型,第三个参数是字节长度,而ptr::copy 的指针是带类型的,第三个参数是对象的个数。
这个模块中还提供了ptr::copy_nonoverlapping 。它 跟C 语言里面的memcpy 很像,都假设用户已经保证了src 和 dst 指向的内容不可重叠。所以ptr::copy_nonoverlapping的执行速度比 ptr::copy要快一些。
2.write
在ptr 模块中,write 的签名是unsafe fn write(dst:*mut T,src:T), 作用是把变量src 写入到dst 所指向的内存中。注意它的参数src 是使用的类型T, 执行 的 是move 语义。查看源码可知,它是基于intrinsics::move_val_init 实现的。注 意,在写的过程中,不管dst 指向的内容是什么,都会被直接覆盖掉。而src这个对象也 不会执行析构函数。
写内存还有ptr::write_bytes 、ptr::write_unaligned 、ptr::write_vol- atile 等函数。
3.read
在 ptr 模 块 中 ,read 的 签 名 是unsafe fn read(src:*const T)->T, 作用是把src 指向的内容当成类型T 返回去。查看它的内部源码,可见它就是基于 ptr::copy_nonoverlapping 实现的。
读内存还有ptr::read_unaligned 以 及ptr::read_volatile 两个函数,大同 小异。以上这些内存读写函数,都是不管语义,直接操作内存中的字节。所以它们都是用 unsafe 函数。
4.swap
在 ptr 模块中,swap 的签名是unsafe fn swap(x:*mut T,y:*mut T),作 用是把两个指针指向的内容做交换。两个指针所指向的对象都只是被修改,而不会被析构。
这个函数在mem 模块中又做了一次封装,变成了unsafe fn swap(x:&mut T, y:&mut T)供用户使用。签名中的&mut 型引用可以保证这两个指针是当前唯一指向该对 象的指针。
某些特殊类型还有自己的swap 成员函数。比如Cell::swap(&self,other:&Cell)。 这个函数跟其他swap 函数最大的区别在于,它的参数只要求共享引用,不要 求可变引用。这是因为Cell 本身的特殊性。它具备内部可变性,所以这么设计是完全安全 的。我们可以从源码看到,它就是简单地调用了ptr::swap。
5.drop_in_place
在 ptr 模块中,drop_in_place 的签名是unsafe fn drop_in_place<T:?Sized> (to_drop:*mut T)。它的作用是执行当前指向对象的析构函数,如果没有就不执行。
6.uninitialized
在 mem 模块中,uninitialized 的签名是unsafe fn uninitialized()->T。 它是基于intrinsics::uninit 函数实现的。我们知道,Rust 编译器要求每个变量必须在 初始化之后再使用,如果在某些情况下,你确实需要未初始化的变量,那么必须使用unsafe 才能做到。注意,任何时候读取未初始化变量都是未定义行为,请大家不要这么做。即便你 在unsafe代码中创造了未初始化变量,也需要自己在逻辑上保证,读取这个变量之前先为它 合理地赋过值。
另外,这个函数有点像std::mem::forget, 调用这个函数,不仅不会在程序中增加 代码,反而会减少可执行代码。调用forget, 会导致编译器不再插入析构函数调用的代码, 调用uninitialized会导致缺少初始化。它们没有任何运行开销。
uninitialized函数也还没有稳定,它有一些无法克服的缺陷,将来标准库会废弃掉 这个函数,而使用一个新的类型让用户在unsafe代码中创建未初始化变量。
综 合 示 例
下面我们用一个示例来演示一下这些unsafe函数的用途,以及怎样才能正确调用unsafe 代码。示例很简单,就是实现标准库中的内存交换函数std::mem::swap。
我们可以确定这个函数的签名是fn swap(x:&mut T,y:&mut T)。关于泛 型的解释在第21章中有,此处略过不提。先试一个最简单的实现:
fn swap<T>(x:&mut T,y:&mut T){let z:T =*x;*x =*y;*y=Z;
}
编译不通过。因为let z =*x;执行的是move 语义,编译器不允许我们把x 指向的 内容move 出来,这只是一个借用而已。如果允许执行这样的操作,会导致原来的借用指针 x 指向非法数据。但是我们知道,我们这个函数整体上是可以保证安全的,因为我们把x 指 向的内容move 出来之后,会用其他的正确数据填回去,最终可以保证函数执行完之后, x 和y 都是一个正常的状态。这种时候,我们就需要动用unsafe了,代码如下:
fn swap<T>(x:&mut T,y:&mut T){
unsafe{let mut t:T =mem::uninitialized();ptr::copy_nonoverlapping(&*x,&mut t,1);ptr::copy_nonoverlapping(&*y,x,1);ptr::copy_nonoverlapping(&t,y,1);mem::forget();
}
代码逻辑的意思如下。
首先,我们依然需要 一个作为中转的局部变量。这个局部变量该怎么初始化呢?其实我 们不希望它执行初始化,因为我们只需要这部分内存空间而已,它里面的内容马上就会被覆 盖掉,做初始化是浪费性能。况且,我们也不知道用什么通用的办法初始化 一个泛型类型, 它 连Default约束都未必满足。所以我们要用mem::uninitialized函 数 。
