
内置函数在标准库中有一个std::intrinsics模块它里面包含了一系列的编译器内置函 数。这些函数都有一个externrust-intrinsic修饰它们看起来都像一种特殊的 FFI 外部函数大家打开标准库的源代码src/core/intrinsics.rs,可以看到这些函数 根本没有函数体因为它们的实现是在编译器内部而不是在标准库内部。调用它们的时候 都必须使用unsafe 才可以。编译器见到这些函数就知道应该生成什么样的代码而不是像 普通函数调用一样处理。另外intrinsics 是藏在一个feature gate后面的这个feature 可能 永远不会稳定这些函数就不是准备直接提供给用户使用的。一般标准库会在这些函数基础 上做一个更合适的封装给用户使用。下面就在这些函数中挑一部分作介绍。transmutefn transmuteT,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 类型的二进制表示是怎样的fnmain(){letxvec![1,2,3,4,5];unsafe{lett:(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 内部的具体内存表示了。执行结果为639392055intrinsics模块里面有几个与内存读写相关的函数比如copy 、copy_nonoverla-pping 、write_bytes 、move_val_init 、volatile_load 等。这些函数又在std::ptr/std::mem 模块中做了个简单封装然后暴露出来给用户使用。下面挑其中几个重要 的函数介绍。1.copycopy 的完整签名是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_placeT:?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 swapT(x:mutT,y:mutT){letz:T*x;*x*y;*yZ;}编译不通过。因为let z *x;执行的是move 语义编译器不允许我们把x 指向的 内容move 出来这只是一个借用而已。如果允许执行这样的操作会导致原来的借用指针 x 指向非法数据。但是我们知道我们这个函数整体上是可以保证安全的因为我们把x 指 向的内容move 出来之后会用其他的正确数据填回去最终可以保证函数执行完之后 x 和y 都是一个正常的状态。这种时候我们就需要动用unsafe了代码如下fn swapT(x:mutT,y:mutT){unsafe{letmut t:Tmem::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函 数 。