3 个版本 (重大更改)

0.3.0 2021 年 3 月 28 日
0.2.0 2021 年 3 月 27 日
0.1.0 2021 年 3 月 14 日

#1115 in Rust 模式

Download history 3/week @ 2024-02-16 8/week @ 2024-02-23 1/week @ 2024-03-01 40/week @ 2024-03-29 14/week @ 2024-04-05

每月 54 次下载

MIT/Apache

32KB
281

Crates.io

escher

使用 async 栈的自引用结构体

Escher 是一个非常简单的库,提供了一个安全且稳定的 API 来构建自引用结构体。它通过(滥用)rustc 的 async await 转换来工作。如果您想了解更多关于内部工作原理的信息,请查看如何工作部分和源代码。

与最先进的 Escher 相比

  • 只有大约 100 行注释良好的代码
  • 只包含两个经过充分论证的 unsafe 调用
  • 使用 rustc 进行所有分析。如果它编译成功,则自引用是正确的

使用方法

此库提供了Escher 包装类型,可以持有自引用数据并通过as_ref()as_mut() 函数安全地公开。

您通过调用 Escher 的构造函数并提供一个将初始化您的自引用的异步闭包来构建自引用。您的闭包将获得一个捕获器 r,该捕获器具有一个单个 capture() 方法,该方法消耗 r

注意:为了使 escher 正确初始化您的结构体,您必须对 .await 结果调用 .capture()

一旦创建所有数据和引用,您就可以捕获所需的那些。简单指向自有数据的引用可以直接捕获(见第一个示例)。

要捕获多个变量或捕获非自有数据的引用,您必须定义自己的引用结构体,该结构体继承自 Rebindable(见第二个示例)。

示例

&str 简单视图到一个自有 Vec<u8>

使用Escher最简单的方法是创建一些数据的引用,然后捕获它

use escher::Escher;

let escher_heart = Escher::new(|r| async move {
    let data: Vec<u8> = vec![240, 159, 146, 150];
    let sparkle_heart = std::str::from_utf8(&data).unwrap();

    r.capture(sparkle_heart).await;
});

assert_eq!("💖", *escher_heart.as_ref());

将一个 Vec<u8> 和一个 &str 视图捕获到其中

为了捕获多个东西,您可以定义一个将要用于捕获变量的结构体

use escher::{Escher, Rebindable};

#[derive(Rebindable)]
struct VecStr<'this> {
    data: &'this Vec<u8>,
    s: &'this str,
}

let escher_heart = Escher::new(|r| async move {
    let data: Vec<u8> = vec![240, 159, 146, 150];

    r.capture(VecStr{
        data: &data,
        s: std::str::from_utf8(&data).unwrap(),
    }).await;
});

assert_eq!(240, escher_heart.as_ref().data[0]);
assert_eq!("💖", escher_heart.as_ref().s);

将一个可变 &mut str 视图捕获到一个 Vec<u8>

如果您捕获了某个数据片段的可变引用,那么您就不能像上一个示例那样捕获该数据本身。这是强制性的,因为这样做会导致对同一数据片段创建两个可变引用,这是不允许的。

use escher::Escher;

let mut name = Escher::new(|r| async move {
    let mut data: Vec<u8> = vec![101, 115, 99, 104, 101, 114];
    let name = std::str::from_utf8_mut(&mut data).unwrap();

    r.capture(name).await;
});

assert_eq!("escher", *name.as_ref());
name.as_mut().make_ascii_uppercase();
assert_eq!("ESCHER", *name.as_ref());

捕获多个混合引用

use escher::{Escher, Rebindable};

#[derive(Rebindable)]
struct MyStruct<'this> {
    int_data: &'this Box<i32>,
    int_ref: &'this i32,
    float_ref: &'this mut f32,
}

let mut my_value = Escher::new(|r| async move {
    let int_data = Box::new(42);
    let mut float_data = Box::new(3.14);

    r.capture(MyStruct{
        int_data: &int_data,
        int_ref: &int_data,
        float_ref: &mut float_data,
    }).await;
});

assert_eq!(Box::new(42), *my_value.as_ref().int_data);
assert_eq!(3.14, *my_value.as_ref().float_ref);

*my_value.as_mut().float_ref = (*my_value.as_ref().int_ref as f32) * 2.0;

assert_eq!(84.0, *my_value.as_ref().float_ref);

它是如何工作的

自引用的问题

自引用结构体的主要问题是,如果这样的结构体被构造,编译器就必须静态证明它不会再次移动。这种分析是必要的,因为任何移动都会使自指针无效,因为Rust中的所有指针都是绝对内存地址。

为了说明为什么这是必要的,想象我们定义了一个同时持有Vec和指向它的指针的自引用结构体

struct Foo {
    s: Vec<u8>,
    p: &Vec<u8>,
}

然后,假设我们有一种方法可以得到这个结构体的实例。然后我们可以编写以下代码,在安全Rust中创建一个悬垂指针!

let foo = Foo::magic_construct();

let bar = foo; // move foo to a new location
println!("{:?}", bar.p); // access the self-reference, memory error!

Moves invalidate pointer

栈上的几乎自引用

虽然Rust不允许您明确写出自引用结构体成员并初始化它们,但将成员值单独作为独立的栈绑定写出是完全可以的。这是因为借用检查器在值在栈上时可以进行移动分析。

实际上,我们可以将上面的结构体 Foo 转换为以下单个绑定

fn foo() {
    let s = vec![1, 2, 3];
    let p = &s;
}

然后,我们可以将它们包装在一个只包含引用的结构体中,并用它来代替

struct AlmostFoo<'a> {
    s: &'a Vec<u8>,
    p: &'a Vec<u8>,
}

fn make_foo() {
    let s = vec![1, 2, 3];
    let p = &s;

    let foo = AlmostFoo { s, p };

    do_stuff(foo); // call a function that expects an AlmostFoo
}

当然,make_foo() 不能返回一个 AlmostFoo 实例,因为它会引用栈上的值,但它可以调用其他函数并将一个 AlmostFoo 传递给它们。换句话说,只要使用 AlmostFoo 的代码在 make_foo() 之上,我们就可以使用这种技术并处理几乎自引用。

Almost self-reference

尽管这很受限制。理想情况下,我们希望能够返回一些拥有的值,并自由地将其移动、放入堆中等。

实际返回一个 AlmostFoo

注意:下面的异步栈描述并不是rustc实际发生的事情,但它足以说明这一点。 escher 的API确实使用了所需的值在await点被持有,以强制它们包含在生成的Future中。

如我们所见,无法返回一个 AlmostFoo 实例,因为它引用了栈上的值。但如果我们能够在 AlmostFoo 实例构造后冻结栈,然后返回整个栈会怎样呢?

好吧,没有常规函数可以捕获其自己的栈并返回它,但这正是异步/await转换所做的!让我们将上面的 make_foo 转换为异步,并使其永不终止

async fn make_foo() {
    let s = vec![1, 2, 3];
    let p = &s;
    let foo = AlmostFoo { s, p };
    std::future::pending().await
}

现在当有人调用 make_foo() 时,他们得到的是一个实现了 Future 的结构体。这个结构体实际上是对 make_foo 在其初始状态(即函数尚未被调用时的状态)的表示。

我们现在需要执行返回的 Future 的步骤,直到 AlmostFoo 的实例被构造。在这种情况下,我们知道有一个单一的 await 点,所以我们只需要对 Future 进行一次轮询。但是在做之前,我们需要将其放入一个 Pinned Box 中,以确保在轮询 Future 时不会发生任何移动。这与正常函数的限制相同,但是在异步操作中,它使用 Pin<P> 类型来强制执行。

let foo = make_foo(); // construct a stack that will eventually make an AlmostFoo in it
let mut foo = Box::pin(foo_fut); // pin it so that it never moves again
foo.poll(); // poll it once

// now we know that somewhere inside `foo` there is a valid AlmostFoo instance!

我们几乎完成了!我们现在有一个所有权的值,即 future,它内部有 AlmostFoo 实例的某个地方。然而,我们没有方法检索其确切的内存位置或以任何方式访问它。Future 是不透明的。

Async stack

将所有这些放在一起

escher 基于上述技术,并提供了一种从不透明的 future 结构体中获取指针的解决方案。每个 Escher<T> 实例都包含一个 Pinned Future 和一个指向 T 的原始指针。T 的指针是通过轮询 Future 以构建所需的 T 为止来计算的。

作为其 API,它提供了 as_ref()as_mut() 方法,这些方法不安全地将 T 的原始指针转换为 &T,其生命周期绑定到 Escher<T> 本身的生命周期。这确保了 future 将比任何自我引用的使用都要长寿!

感谢您阅读到这里!如果您想详细了解 escher 如何使用上述概念,请查看其实施。

许可证

根据以下任一许可证授权:

任选其一。

贡献

除非您明确说明,否则根据 Apache-2.0 许可证定义的,您有意提交以包含在作品中的任何贡献,应如上所述双许可,不附加任何额外的条款或条件。

依赖关系

~1.5MB
~34K SLoC