3个版本

0.1.2 2022年2月23日
0.1.1 2022年2月21日
0.1.0 2022年2月21日

#1697 in 数据结构


2 crate 中使用

BSD-3-Clause

165KB
1.5K SLoC

varlen

人体工学可变长度类型。

  1. 摘要
  2. 动机
  3. 示例
  4. 类型概述
  5. Pin 的使用
  6. 功能标志

摘要

varlen 定义了在 Rust 中处理可变长度类型的基础类型和特质。

可变长度类型的主要例子是一个将动态大小数组直接存储在其存储中的结构,而不需要指向单独内存分配的指针。 varlen 帮助您定义此类类型,并允许您构建它们的任意连接和结构。此外,它还提供了标准库中的 Box<T>Vec<T> 类型的等价物,这些类型被调整以很好地与可变长度类型一起工作。

如果您想通过直接在对象中存储可变大小数组来减少类型中的指针间接数,那么 varlen 是您的库。

动机

传统上,当我们使用如字符串或数组这样的可变大小数据时,我们使用指向单独分配对象的指针。例如,以下对象...

type Person = (/* age */ usize, /* name */ Box<str>, /* email */ Box<str>);
let person: Person = 
    (16, Box::from("Harry Potter"), Box::from("[email protected]"));

...在内存中的表示如下,有三个单独分配的对象

"Person"
+------------+--------------------+-------------------+--------------------+-------------------+
| "16 (age)" | "0x1234 (str ptr)" | "12 (str length)" | "0x5678 (str ptr)" | "24 (str length)" |
+------------+--------------------+-------------------+--------------------+-------------------+
                |                                       |
                |                                       |
    .-----------'                         .-------------'
    |                                     |
    v "str payload"                       v "str payload"
    +------------------+                  +------------------------------+
    | "'Harry Potter'" |                  | "'[email protected]'" |
    +------------------+                  +------------------------------+

有时我们可以通过将可变长度存储直接引入父对象来减少对象分配的数量,可能具有如下的内存布局

+-----+
| ptr |
+-----+
  | 
  |
  |  "Person"
  |  +-------------+-------------------+------------------+-------------------+------------------------------+
  '->| "16 (age)"  | "12 (str length)" | "'Harry Potter'" | "24 (str length)" | "'[email protected]'" |
     +-------------+-------------------+------------------+-------------------+------------------------------+

此布局将对象分配的数量从3减少到2,可能提高了内存分配器的性能,也可能提高了 CPU缓存局部性。它还减少了指针的数量,从3减少到2,节省了内存。

这种布局的主要缺点是 Person 对象的大小和布局在编译时不可知;它仅在运行时可知,即字符串长度已知时。在纯 Rust 中使用此类布局很麻烦,还需要使用不安全代码来完成必要的指针算术。

varlen 允许您轻松定义和使用此类类型,无需您编写任何不安全的代码。以下代码将创建一个具有上述内存布局的对象

use varlen::prelude::*;
type Person = Tup3</* age */ FixedLen<usize>, /* name */ Str, /* email */ Str>;
let person: VBox<Person> = VBox::new(tup3::Init(
    FixedLen(16),
    Str::copy("Harry Potter"),
    Str::copy("[email protected]"),
));

示例

use varlen::prelude::*;

// Define a variable-length tuple:
type MyTuple = Tup3<FixedLen<usize>, Str, Array<u16>>;
let my_tuple: VBox<MyTuple> = VBox::new(tup3::Init(
    FixedLen(16), Str::copy("hello"), Array::copy(&[1u16, 2])));

// Put multiple objects in a sequence, with tightly packed memory layout:
let sequence: Seq<MyTuple> = seq![my_tuple.vcopy(), my_tuple.vcopy()];

// Or arena-allocate them, if the "bumpalo" crate feature is enabled:
let arena = bumpalo::Bump::new();
let arena_tuple: Owned<MyTuple> = Owned::new_in(my_tuple.vcopy(), &arena);

// Define a newtype wrapper for the tuple:
define_varlen_newtype! {
    #[repr(transparent)]
    pub struct MyStruct(MyTuple);

    with init: struct MyStructInit<_>(_);
    with inner_ref: fn inner(&self) -> &_;
    with inner_mut: fn inner_mut(self: _) -> _;
}
let my_struct: VBox<MyStruct> = VBox::new(MyStructInit(my_tuple));

// Define a variable-length struct via a procedural macro, if the "macro"
// crate feature is enabled.
#[define_varlen]
struct MyMacroStruct {
    age: usize,
    #[varlen]
    name: Str,
    #[varlen]
    email: Str,
    #[varlen]
    child: MyStruct,
}
let s: VBox<MyMacroStruct> = VBox::new(
    my_macro_struct::Init{
        age: 16,
        name: Str::copy("Harry Potter"),
        email: Str::copy("[email protected]"),
        child: my_struct,
    }
);

// #[define_varlen] also let you directly specify array lengths:
#[define_varlen]
struct MultipleArrays {
    #[controls_layout]
    len: usize,

    #[varlen_array]
    array1: [u16; *len],

    #[varlen_array]
    array2: [u8; *len],

    #[varlen_array]
    half_array: [u16; (*len) / 2],
}
let base_array = vec![0u16, 64000, 13, 105];
let a: VBox<MultipleArrays> = VBox::new(multiple_arrays::Init{
    len: base_array.len(),
    array1: FillSequentially(|i| base_array[i]),
    array2: FillSequentially(|i| base_array[base_array.len() - 1 - i] as u8),
    half_array: FillSequentially(|i| base_array[i * 2]),
});

类型概述

varlen 提供了各种标准库类型和特质的可变长度版本。下表给出了对应关系

名称 固定长度类型 T 可变长度类型 T 说明
不可变引用 &T &T
可变引用 &mutT 固定<&mutT> Pin<> 为安全性所必需,见下文
拥有,非分配 T 拥有<'存储, T> Owned<T> 仍然是 T 有效载荷的指针
拥有,已分配 Box<T> VBox<T>
序列 Vec<T> Seq<T> Seq 有紧密排列的 可变大小 元素。随机访问受到一定限制
字符串 字符串 Str 字符串有效载荷紧随大小之后,没有后续指针
数组(固定大小元素) Box<[u16]> 数组<u16> 数组有效载荷紧随大小之后,没有后续指针
元组 (T,U) Tup2<T, U> 字段 U 可能不在对象开始的静态已知偏移量处
克隆 克隆::clone() VClone::vclone()
复制 VCopy::vcopy()

使用 Pin

对可变长度类型的可变引用使用 Pin<&mut T> 而不是 &mut T。通过这样做,我们防止了对可变长度类型调用 std::mem::swap 等模式。这样的模式会是一个安全隐患,因为当调用 std::mem::swap 时,Rust 编译器所知的类型部分仅仅是类型的 "固定大小头部"。然而,几乎所有可变长度类型还有 Rust 编译器不知道的 "可变大小尾部"。只交换头部而不交换尾部可能会违反类型的不变量,从而破坏安全性。

如果您从未编写过 unsafe 代码,您不必担心这个问题。唯一的实际后果是,对可变长度类型的可变访问始终通过 Pin<&mut T> 而不是 &mut T 进行中介,并且您将不得不与稍微有些繁琐的 Pin API 一起工作。

另一方面,如果你编写了 unsafe 代码,你可能需要了解以下不变量。如果 T 是一个变长类型,我们要求任何引用 &T 都指向一个“有效”的 T,我们认为这是一个具有固定长度头部(大小为 std::mem::size_of::<T>)的头部,之后是一个变长尾部,并且头部和尾部是“一致”的。在这里,“一致”意味着它们是通过调用该类型的 Initializer 实例生成的。在 unsafe 代码中,你可能可以访问到 &mut T(没有 Pin),你必须避免修改头部而不相应修改尾部的代码模式。

功能标志

此包没有 必需 依赖项。以下功能标志可以打开一些依赖项。

  • bumpalo。启用在 bumpalo::Bump 区域分配 Owned<T> 的支持。添加对 bumpalo 的依赖。
  • macro。启用过程宏支持,使用 #[define_varlen] 定义变长结构体。添加对 varlen_macrosynquote 的依赖。
  • doc。启用在文档中启用精美的 SVG 图表。添加大量依赖项。

依赖项

~0–1.6MB
~30K SLoC