#shared-ptr #rc #zero-copy #arc #projected #weak-references

genrc

支持子对象指针的引用计数指针类型

4 个版本 (2 个重大更改)

0.3.0 2023 年 7 月 22 日
0.2.1 2023 年 7 月 21 日
0.2.0 2023 年 7 月 20 日
0.1.0 2023 年 7 月 7 日

#625 in Rust 模式

MIT/Apache

69KB
980

genrc

Crates.io MIT/Apache

此软件包提供对 std::sync::Arcstd::rc::Rc 的替代方案,它们是(几乎)直接替换,但允许子对象的引用计数指针,类似于 C++ 的 shared_ptr

主要功能,它增加了令人惊讶的灵活性:如果您有一个 Rc<T>,并且 T 包含一些类型为 U 的子对象,则可以通过调用 Rc::project() 来构造一个与原始对象共享所有权的 Rc<U>

    use genrc::rc::{Rc, Weak};
    let a: Rc<[i32; 3]> = Rc::new([1, 2, 3]);

    // convert the sized array into a slice
    let b: Rc<[i32]> = Rc::project(a, |x| &x[..]);

    // get a reference to one element of the array
    let c: Rc<i32> = Rc::project(b, |x| &x[1]);

还有类型 RcBox<T>(和 ArcBox<T>)从 new_unique() 返回,它们利用新创建的引用计数指针仍然是唯一的这一事实,因此可以用于可变操作。

用途

更简单、更安全的初始化

您可以使用 RcBox<Option<T>> 来表示尚未初始化的类型,而不是使用 std::rc 中的各种不安全的 MaybeInit 相关 API。在对象初始化后,您可以使用 project 将其转换为普通的 Rc<T>

    # use genrc::rc::{Rc, RcBox, Weak};

    // construct the object initially uninitialized
    let mut obj : RcBox<Option<i32>> = Rc::new_unique(None);

    // ... later ...
    // initialize the object
    obj.replace(5);

    // project to the inner value that we just created
    let obj : Rc<i32> = RcBox::project(obj, |x| x.as_ref().unwrap());

    assert_eq!(*obj, 5);

您还可以创建循环数据结构,而不需要使用 RefCellnew_cyclic

    use genrc::rc::{Rc, RcBox, Weak};
    struct Node {
        edges: Vec<Weak<Node>>,
    }

    // Make a graph
    let mut graph: Vec<RcBox<Node>> = (0..5).map(|_| {
        Rc::new_unique(Node { edges: vec![] })
    }).collect();

    // Make some random edges in the graph
    for i in 0..5 {
        for d in 1..3 {
            let j = (i + d) % 5;

            let link = RcBox::downgrade(&graph[j]);
            graph[i].edges.push(link);
        }
    }

    // we still have unique handles on the nodes, so attempting to upgrade
    // weak pointers will fail.
    let p = graph[1].edges[0].clone();
    assert!(p.upgrade().is_none());

    // convert `RcBox` to a normal `Rc` with `into()`.
    let graph: Vec<Rc<Node>> = graph.into_iter().map(Into::into).collect();

    // now the weak pointers are valid - we've made a graph with (weak)
    // cycles, no unsafe or internal mutation required.
    assert!(Rc::ptr_eq(&graph[0].edges[1].upgrade().unwrap(), &graph[2]));

静态数据

std 不同,引用可以指向静态数据而不需要复制,这同样是通过使用 project() 实现的。

    # use genrc::rc::Rc;
    static BIGBUF: [u8; 1024] = [1; 1024];

    let p: Rc<()> = Rc::new(());
    let p: Rc<[u8]> = Rc::project(p, |_| &BIGBUF[..]);

    assert!(std::ptr::eq(&BIGBUF[..], &*p));

因此,您可以使用 Rc 来跟踪可能拥有的、可能静态的数据,类似于 Cow

其他内容

夜间Rust allocator_api 支持

allocator_api 功能启用了不稳定分配器API,允许 RcArc 使用自定义分配器。

Rc::new_in 返回一个类型为 Rc<T, A>Rc,其中分配器是类型的一部分。如果需要,您可以使用 Rc::erase_allocator() 来隐藏分配器。

生命周期

有些令人惊讶的是,std::rc::Rc 允许您创建一个指向局部变量的 Rc。例如,这是合法的

    use std::{cell::Cell, rc::Rc};
    let x = Cell::new(1);
    let y : Rc<&Cell<i32>> = Rc::new(&x);
    x.set(2);
    assert_eq!(y.get(), 2);

这种 Rc 的类型是 Rc<&'a T>,其中 'a 是被引用者的生命周期,因此 Rc 不能比被引用者存活更长时间。

genrc::Rc 也可以这样做。但是,如果您使用 project()Rc<&'a T> 转换为指向同一对象的 Rc<T>,后者类型没有地方放置生命周期 'a,因此如果允许这样,这将导致引用存活时间过长,成为一个鲁棒性错误。

为了避免这种情况,类型 Rcl<'a, T> 将生命周期参数添加到 Rc 中。

(实际上,Rc<T> 只是 Rcl<'static, T> 的别名,而 Arc<T>Arcl<'static, T> 的别名。它们都是 genrc::Genrc 的别名,它是生命周期、引用类型、原子性、分配器和唯一性的泛型。)

要在一个短生命周期引用上使用 project(),您必须使用 Rcl::project(),它返回一个具有非静态生命周期的 Rcl

    use genrc::rc::Rcl;

    // Imagine we have some JSON data that we loaded from a file
    // (or data allocated in an arena, etc)
    let bigdata : Vec<u8> = b"Not really json, use your imagination".to_vec();

    // buf points directly into `bigdata`, not a copy
    let buf : Rcl<[u8]> = Rcl::from_ref(&bigdata[..]);
    assert!(std::ptr::eq(&*buf, &bigdata[..]));

    let word : Rcl<[u8]> = Rcl::project(buf, |x| &x[4..10]);
    assert!(std::ptr::eq(&*word, &bigdata[4..10]));

由于生命周期通常是可以推断的,在大多数情况下 RclRc 的工作方式完全相同。主要的例外是在数据类型中,你可能需要显式指定生命周期。例如,如果你想要一个字段,它是一个 Rc<str> 类型,并且字符串可能存在生命周期较短的情况,你可以这样编写:

    use genrc::rc::Rcl;

    // Token in a parser where the buffer is an `Rc<str>`, and `text` can point
    // directly into the buffer. (Or `text` can point to owned data, e.g. for
    // unescaped strings, and callers generally don't have to care.)
    struct Token<'a> {
      some_data: u32,
      text: Rcl<'a, str>
    }

当类型擦除具有生命周期的自定义分配器时,也需要生命周期参数,因为 Rc<T> 也会隐藏分配器。

std::sync::Arcstd::rc::Rc

Rc::from_box 不会从原始盒子中复制对象。相反,它直接以当前的形式获取盒子的所有权,计数在单独的分配中。

如果你泄露了如此多的 Rc 对象,以至于引用计数溢出,std 指针将终止。但是 genrc 不会这样做,因为在 no_std 中没有 abort() 函数。

Rc<T>Rc<dyn Trait> 的隐式转换不受支持,因为这需要一些不稳定的特征。但是你可以使用 Rc::project 显式进行转换。[待办:在夜间要求的功能后支持此功能.]

std 指针有各种与 MaybeUninit 相关的方法,用于分配后初始化对象。该 API 在 Genrc 中未提供,因为你可以使用 Optionproject 在安全代码中完成相同的事情。

    # use genrc::rc::{Rc, RcBox, Weak};
    // construct the object uninitialized
    let mut obj : RcBox<Option<i32>> = Rc::new_unique(None);

    // ... later ...
    // initialize the object
    obj.replace(5);

    // project to the inner value that we just created
    let obj : Rc<i32> = RcBox::project(obj, |x| x.as_ref().unwrap());

    assert_eq!(*obj, 5);

与 std 不同,RcArc(以及 RcBoxArcBox)共享单个泛型实现。Rc<T> 是别名 Genrc<'static, T, Nonatomic>,而 Arc<T> 是别名 Genrc<'static, T, Atomic>。这确实使文档看起来有些难看,因为它都在 Genrc 结构中,而不是你通常关心的实际类型。

std::rc::Rc::ptr_eq(a,b) 如果 a 和 b 共享相同的分配,则返回 true,这等同于询问它们是否是相等的指针。但在 genrc 中,这是两个不同的问题:你可以从同一个分配中获得指向两个不同子对象的指针,或者来自两个不同分配的指针指向同一个对象!(例如,它们可能被投影到一个静态对象)。因此,这里我们有 Rc::ptr_eq,它相当于 std::ptr::eq(&*a, &*b),以及 Rc::root_ptr_eq,后者检查计数是否共享。

from_rawinto_raw 不可用,因为返回的指针可能与原始分配没有关系。

shared-rc 的差异

shared-rc 与此非常相似的 crate;如果我知道 shared-rc 已经存在,我就不会写这个。话虽如此,还有一些差异

  • shared-rc 在底层使用 ArcRc 的 std 版本,因此它不支持零分配使用。

  • shared-rc 包含一个 Owner 类型参数,具有显式的 erase_owner 方法来隐藏它。genrc::arc::Arc 总是类型擦除所有者。这节省了指针中一个字节的开销,当类型擦除的 shared-rc 指向一个无大小类型时。(例如,shared_rc::rc::[u8] 是 32 字节,但 genrc::rc::[u8] 是 24。)

  • genrc 在原子与共享之间是泛型的。shared-rc 使用宏来实现这一点,这使得 rustdocs 更难阅读,但“转到定义”更容易阅读。

rc-box 的差异

rc-box crate 在 std Arc/Rc 周围添加了很好的 API:创建后立即知道你有了对其的唯一指针,所以将其放入一个实现 DerefMut 的包装类型中。这个 crate 复制了那个 API。

  • 由于 rc-box 是基于 std 类型构建的,因此允许弱指针指向其 RcBox 类型是不安全的,因此它不能像上面图例中的 new_cyclic 一样替换。

  • genrc 中的实现是通用的,关于指针是否唯一(GenRc 的 UNIQ 参数)。这允许编写关于指针唯一性的通用代码,这在初始化时可能很有用(例如上面的图创建示例,其中图是一个 Vec<RcBox<Node>>,在初始化时,然后被转换为 Vec<Rc<Node>>。)

  • shared-rc:与这个 crate 类似,但它包装了 std 版本的 ArcRc 而不是重新实现它们。
  • rc-box:已知为唯一版本的 Rc 和 Arc。
  • erasable:擦除其具体类型的指针。
  • rc-borrowRcArc 的借用形式。

待办事项

在一个功能后面实现各种 Unsize 特性。(尽管它们自 1.0 以来没有变化,但它们仍然是实现智能指针所必需的,尽管它们需要在夜间运行。)

如果计数溢出,使行为匹配 std

更丰富的自定义分配器 API。目前只提供了 new_infrom_box;应该有 try_* 并支持 no_global_oom_handling

更多的文档示例。

许可证

genrc 根据 MIT 或 Apache 2.0 许可证进行许可,您选择哪一个。

无运行时依赖