Rust 中的数据布局--非正常大小的类型
非正常大小的类型
大多数的时候,我们期望类型在编译时能够有一个静态已知的非零大小,但这并不总是 Rust 的常态。
Dynamically Sized Types (DSTs)
Rust 支持动态大小的类型(DST):这些类型没有静态(编译时)已知的大小或者布局。从表面上看这有点离谱:Rust 必须知道一个东西的大小和布局,才能正确地进行处理。从这个角度上看,DST 不是一个普通的类型,因为它们没有编译时静态可知的大小,它们只能存在于一个指针之后。任何指向 DST 的指针都会变成一个包含了完善 DST 类型信息的胖指针(详情见下方)。
Rust 暴露了两种主要的 DST 类型:
Trait 对象代表某种类型,实现了它所指定的 Trait。确切的原始类型被删除,以利于运行时的反射,其中包含使用该类型的所有必要信息的 vtable。补全 Trait 对象指针所需的信息是 vtable 指针,被指向的对象的运行时的大小可以从 vtable 中动态地获取。
一个 slice 只是一些只读的连续存储——通常是一个数组或Vec
。补全一个 slice 指针所需的信息只是它所指向的元素的数量,指针的运行时大小只是静态已知元素的大小乘以元素的数量。
结构实际上可以直接存储一个 DST 作为其最后一个字段,但这也会使它们自身成为一个 DST:
// 不能直接存储在栈上
struct MySuperSlice {
info: u32,
data: [u8],
}
如果这样的类型没有方法来构造它,那么它在很大程度上来看是没啥用的。目前,唯一支持的创建自定义 DST 的方法是使你的类型成为泛型,并执行非固定大小转换(unsizing coercion):
struct MySuperSliceable<T: ?Sized> {
info: u32,
data: T,
}
fn main() {
let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
info: 17,
data: [0; 8],
};
let dynamic: &MySuperSliceable<[u8]> = &sized;
// 输出:"17 [0, 0, 0, 0, 0, 0, 0, 0]"
println!("{} {:?}", dynamic.info, &dynamic.data);
}
(是的,自定义 DST 目前仅仅是一个基本半成品的功能。)
零大小类型 (ZSTs)
Rust 也允许类型指定他们不占空间:
struct Nothing; // 无字段意味着没有大小
// 所有字段都无大小意味着整个结构体无大小
struct LotsOfNothing {
foo: Nothing,
qux: (), // 空元组无大小
baz: [u8; 0], // 空数组无大小
}
就其本身而言,零尺寸类型(ZSTs)由于显而易见的原因是相当无用的。然而,就像 Rust 中许多奇怪的布局选择一样,它们的潜力在通用语境中得以实现。在 Rust 中,任何产生或存储 ZST 的操作都可以被简化为无操作(no-op)。首先,存储它甚至没有意义——它不占用任何空间。另外,这种类型的值只有一个,所以任何加载它的操作都可以直接凭空产生它——这也是一个无操作(no-op),因为它不占用任何空间。
这方面最极端的例子之一是 Set 和 Map。给定一个Map<Key, Value>
,通常可以实现一个Set<Key>
,作为Map<Key, UselessJunk>
的一个薄封装。在许多语言中,这将需要为无用的封装分配空间,并进行存储和加载无用封装的工作,然后将其丢弃。对于编译器来说,证明这一点是不必要的,是一个困难的分析。
然而在 Rust 中,我们可以直接说Set<Key> = Map<Key, ()>
。现在 Rust 静态地知道每个加载和存储都是无用的,而且没有分配有任何大小。其结果是,单例化的代码基本上是 HashSet 的自定义实现,而没有 HashMap 要支持值所带来的开销。
安全的代码不需要担心 ZST,但是不安全的代码必须小心没有大小的类型的后果。特别是,指针偏移是无操作的,而分配器通常需要一个非零的大小。
请注意,对 ZST 的引用(包括空片),就像所有其他的引用一样,必须是非空的,并且适当地对齐。解引用 ZST 的空指针或未对齐指针是未定义的行为,就像其他类型的引用一样。
空类型
Rust 还允许声明不能被实例化的类型。这些类型只能在类型层讨论,而不能在值层讨论。空类型可以通过指定一个没有变体的枚举来声明:
enum Void {} // 没有变体的类型 = 空类型
空类型甚至比 ZST 更加边缘化。空类型的主要作用是为了让某个类型不可达。例如,假设一个 API 需要在一般情况下返回一个结果,但一个特定的情况实际上是不可能的。实际上可以通过返回一个Result<T, Void>
来在类型级别上传达这个信息。API 的消费者可以放心地 unwrap 这样一个结果,因为他们知道这个值在本质上不可能是Err
,因为这需要提供一个Void
类型的值。
原则上,Rust 可以基于这个事实做一些有趣的分析和优化,例如,Result<T, Void>
只表示为T
,因为Err
的情况实际上并不存在(严格来说,这只是一种优化,并不保证,所以例如将一个转化为另一个仍然是 UB)。
比如以下的例子,曾经是可以编译成功的:
enum Void {}
let res: Result<u32, Void> = Ok(0);
// 不存在 Err 的情况,所以 Ok 实际上永远都能匹配成功
let Ok(num) = res;
但现在,已经不让这么玩儿了。
关于空类型的最后一个微妙的细节是,构造一个指向它们的原始指针实际上是有效的,但对它们的解引用是未定义行为,因为那是没有意义的。
我们建议不要用*const Void
来模拟 C 的void*
类型。很多人之前这样做,但很快就遇到了麻烦,因为 Rust 没有任何安全防护措施来防止用不安全的代码来实例化空类型,如果你这样做了,就是未定义行为。因为开发者有将原始指针转换为引用的习惯,而构造一个&Void
也是未定义行为,所以这尤其成问题。
*const ()
(或等价物)对void*
来说效果相当好,可以做成引用而没有任何安全问题。它仍然不能阻止你试图读取或写入数值,但至少它可以编译成一个 no-op 而不是 UB。
外部类型
有一个已被接受的 RFC 来增加具有未知大小的适当类型,称为 extern 类型,这将让 Rust 开发人员更准确地模拟像 C 的void*
和其他“声明但从未定义”的类型。然而,截至 Rust 2018,该功能在size_of_val::<MyExternType>()
应该如何表现方面遇到了一些问题。
Rust 中的数据布局--非正常大小的类型的更多相关文章
- Rust 中的数据布局--可选的数据布局
Rust 允许你指定不同于默认的数据布局策略,并为你提供了不安全代码指南. repr(C) 这是最重要的"repr".它的意图相当简单:做 C 所做的事.字段的顺序.大小和对齐方式 ...
- Rust 中的数据布局-repr
repr(Rust) 首先,所有类型都有一个以字节为单位的对齐方式,一个类型的对齐方式指定了哪些地址可以用来存储该值.一个具有对齐方式n的值只能存储在n的倍数的地址上.所以对齐方式 2 意味着你必须存 ...
- 在 sql server 中,查询 数据库的大小 和 数据库中各表的大小
其实本来只想找一个方法能查询一下 数据库 的大小,没想到这个方法还能查询数据库中 各个数据表 的大小,嗯,挺好玩的,记录一下. MSDN资料:https://msdn.microsoft.com/zh ...
- 借助 SIMD 数据布局模板和数据预处理提高 SIMD 在动画中的使用效率
原文链接 简介 为发挥 SIMD1 的最大作用,除了对其进行矢量化处理2外,我们还需作出其他努力.可以尝试为循环添加 #pragma omp simd3,查看编译器是否成功进行矢量化,如果性能有所提升 ...
- c++继承中的内存布局
今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化 译 译者前言 一个C ...
- 关于SWT中的GridLayout布局方式
GridLayout 布局的功能非常强大,也是笔者常用的一种布局方式.GridLayout是网格式布局,它把父组件分成一个表格,默认情况下每个子组件占据一个单元格的空间,每个子组件按添加到父组件的顺序 ...
- Android中的数据存储
Android中的数据存储主要分为三种基本方法: 1.利用shared preferences存储一些轻量级的键值对数据. 2.传统文件系统. 3.利用SQLite的数据库管理系统. 对SharedP ...
- ELF文件数据布局探索(1)
作为一名Linux小白,第一次看到a.out这个名字,感觉实在是奇怪,搜了一下才知道这是编译器输出的默认可执行文件名 然后vi一下,哇,各种乱码,仔细看看,发现了三个清晰的字符ELF.继续搜索, 第一 ...
- C++继承 派生类中的内存布局(单继承、多继承、虚拟继承)
今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化 译 译者前言 一个C ...
随机推荐
- JVM学习笔记(详细)
目录 01 JVM与Java体系结构 简介 JVM整体架构,HotSpot java代码执行流程 JVM架构模型 JVM生命周期 JVM发展历程 02 类加载子系统 JVM细节版架构 类加载器的作用 ...
- windows server 2012 r2 修改administrator密码
转至:https://jingyan.baidu.com/article/b907e627b615b646e7891cbf.html 1.进入开始面板,点击"管理工具". 2.双击 ...
- Pycharm:使用Edit Custom VM Options导致Pycharm无法启动
解决办法: Edit Custom VM Options用来扩大内存,但是内存设置不当可能会导致Pycharm无法启动 如果无法启动,可以在我的文档中的pycharm201X.X(日期不同名字也不同) ...
- LGP5204题解
@CF1327F 最小值看着有点怪,先转化成最大值吧...反正没啥区别... 考虑把最大值相同的区间和限制为这个最大值的区间都拿出来.然后离散化.问题变为让所有区间都满足最值为 \(c\). 考虑 D ...
- git 回滚方式
git push 命用于从将本地的分支版本上传到远程并合并. 命令格式如下: git push <远程主机名> <本地分支名>:<远程分支名> 如果本地分支名与远程 ...
- python学习之scipy实战1(积分用法)
import numpy as np def main(): #1-- Integral积分 from scipy.integrate import quad, dblquad, nquad prin ...
- JavaWeb 05_JDBC入门及连接MySQL
一.概念 *概念: Java DataBase Connectivity Java数据库连接, Java语言操作数据库* JDBC本质:其实是官方(sun公司)定义的一套操作所有关系型数 ...
- web服务器-nginx反向代理
web服务器-nginx反向代理 一. 代理介绍 代理是网络中使用比较常见的, 比如我们说的最多的就是FQ软件, 比如ss, 蓝灯等这些大家常用的软件,他们就是能改代理大家访问的国内无法访问的一些国外 ...
- .htaccess文件构成的PHP后门
1..htaccess文件 2.文件上传绕过 一般.htaccess可以用来留后门和针对黑名单绕过 创建一个txt写入(png解析为php) AddType application/x-httpd-p ...
- Rsync反弹shell
vulhub环境靶机 : 192.168.91.130 攻击机:kali 192.168.91.128 一.环境搭建 vulhub环境靶机环境搭建 在纯净ubuntu中部署vulhub环境: 1. ...