不安全 Rust 中的引用堆栈,但确保不安全性不会泄漏到堆栈之外?

Stack of references in unsafe Rust, but ensuring that the unsafeness does not leak out of the stack?

我正在实现一些递归代码,其中调用堆栈中更深层的函数实例可能需要引用先前帧中的数据。但是,我只能对这些数据进行非 mut 访问,因此我会收到这些数据作为参考。因此,我需要在堆栈数据结构中保留对这些数据的引用,以便可以从更深的实例访问。

举例说明:

// I would like to implement this RefStack class properly, without per-item memory allocations
struct RefStack<T: ?Sized> {
    content: Vec<&T>,
}
impl<T: ?Sized> RefStack<T> {
    fn new() -> Self { Self{ content: Vec::new() } }
    fn get(&self, index: usize) -> &T { self.content[index] }
    fn len(&self) -> usize { self.content.len() }
    fn with_element<F: FnOnce(&mut Self)>(&mut self, el: &T, f: F) {
        self.content.push(el);
        f(self);
        self.content.pop();
    }
}

// This is just an example demonstrating how I would need to use the RefStack class
fn do_recursion(n: usize, node: &LinkedListNode, st: &mut RefStack<str>) {
    // get references to one or more items in the stack
    // the references should be allowed to live until the end of this function, but shouldn't prevent me from calling with_element() later
    let tmp: &str = st.get(rng.gen_range(0, st.len()));
    // do stuff with those references (println is just an example)
    println!("Item: {}", tmp);
    // recurse deeper if necessary
    if n > 0 {
        let (head, tail): (_, &LinkedListNode) = node.get_parts();
        manager.get_str(head, |s: &str| // the actual string is a local variable somewhere in the implementation details of get_str()
            st.with_element(s, |st| do_recursion(n - 1, tail, st))
        );
    }
    // do more stuff with those references (println is just an example)
    println!("Item: {}", tmp);
}

fn main() {
    do_recursion(100, list /* gotten from somewhere else */, &mut RefStack::new());
}

在上面的示例中,我关心的是如何实现 RefStack 而无需任何每项内存分配。 Vec 的偶尔分配是可以接受的 - 这些分配很少而且相差甚远。 LinkedListNode 只是一个例子 - 实际上它是一些复杂的图形数据结构,但同样适用 - 我只有一个非 mut 引用它,并且给 manager.get_str() 的闭包只提供了一个非 mut str。请注意,传入闭包的非 mut str 可能仅在 get_str() 实现中构造,因此我们不能假设所有 &str 具有相同的生命周期。

我相当确定如果不将 str 复制到拥有的 String 中,RefStack 无法在安全 Rust 中实现,所以我的问题是这怎么可能在不安全的 Rust 中完成。感觉我也许可以得到这样的解决方案:

如何在(不安全的)Rust 中实现这样的结构?

我觉得我可以将元素引用转换为指针并将它们存储为指针,但是在将它们转换回引用时,我仍然难以表达上面第二个要点中的要求。或者是否有更好的方法(或者这种结构是否可以在安全的 Rust 中实现,或者已经在某个库中的某个地方)?

免责声明:这个回答原来用的是traits,简直是噩梦; Francis Gagne 正确地指出,使用 Option 作为尾巴是一个更好的选择,因此答案要简单得多。

鉴于您的使用结构,RefStack 中的堆栈跟随堆栈帧的使用,您可以简单地将元素放在堆栈帧上并从中构建堆栈。

这种方法的主要优点是完全安全。您可以查看 whole code here,或按照下面的逐项描述进行操作。

关键是想法是建立一个so-calledcons-list.

#[derive(Debug)]
struct Stack<'a, T> {
    element: &'a T,
    tail: Option<&'a Stack<'a, T>>,
}

impl<'a, T> Stack<'a, T> {
    fn new(element: &'a T) -> Self { Stack { element, tail: None } }

    fn top(&self) -> &T { self.element }

    fn get(&self, index: usize) -> Option<&T> {
        if index == 0 {
            Some(self.element)
        } else {
            self.tail.and_then(|tail| tail.get(index - 1))
        }
    }

    fn tail(&self) -> Option<&'a Stack<'a, T>> { self.tail }

    fn push<'b>(&'b self, element: &'b T) -> Stack<'b, T> { Stack { element, tail: Some(self) } }
}

一个简单的用法示例是:

fn immediate() {
    let (a, b, c) = (0, 1, 2);

    let root = Stack::new(&a);
    let middle = root.push(&b);
    let top = middle.push(&c);
    
    println!("{:?}", top);
}

它只打印堆栈,产生:

Stack { element: 2, tail: Some(Stack { element: 1, tail: Some(Stack { element: 0, tail: None }) }) }

还有一个更复杂的递归版本:

fn recursive(n: usize) {
    fn inner(n: usize, stack: &Stack<'_, i32>) {
        if n == 0 {
            print!("{:?}", stack);
            return;
        }

        let element = n as i32;
        let stacked = stack.push(&element);
        inner(n - 1, &stacked);
    }

    if n == 0 {
        println!("()");
        return;
    }

    let element = n as i32;
    let root = Stack::new(&element);
    inner(n - 1, &root);
}

打印:

Stack { element: 1, tail: Some(Stack { element: 2, tail: Some(Stack { element: 3, tail: None }) }) }

一个缺点是get性能可能不太好;它具有线性复杂性。另一方面,cache-wise 坚持使用栈帧非常好。如果您主要访问前几个元素,我希望它足够好。

我认为存储原始指针是可行的方法。您需要 PhantomData 来存储生命周期并获得适当的协方差:

use std::marker::PhantomData;

struct RefStack<'a, T: ?Sized> {
    content: Vec<*const T>,
    _pd: PhantomData<&'a T>,
}

impl<'a, T: ?Sized> RefStack<'a, T> {
    fn new() -> Self {
        RefStack {
            content: Vec::new(),_pd: PhantomData
        }
    }
    fn get(&self, index: usize) -> &'a T {
        unsafe { &*self.content[index] }
    }
    fn len(&self) -> usize {
        self.content.len()
    }
    fn with_element<'t, F: FnOnce(&mut RefStack<'t, T>)>(&mut self, el: &'t T, f: F)
        where 'a: 't,
    {
        self.content.push(el);
        let mut tmp = RefStack {
            content: std::mem::take(&mut self.content),
            _pd: PhantomData,
        };
        f(&mut tmp);
        self.content = tmp.content;
        self.content.pop();
    }
}

(Playground)

唯一的 unsafe 代码是将指针转换回引用。

棘手的部分是让 with_element 正确。我认为 were 'a: 't 是隐含的,因为整个 impl 都依赖于它(但安全总比遗憾好)。

最后一个问题是如何将RefStack<'a, T>转换为RefStack<'t, T>。我很确定我可以 std::transmute 它。但这需要额外注意 unsafe,并且创建一个新的临时堆栈非常简单。

关于't生命周期

您可能认为实际上并不需要这个 't 生命周期,但不添加它可能会导致微妙的不合理,因为回调可以调用 get() 并获取生命周期 'a 的值] 实际上比插入的值长。

例如,这段代码不应编译。使用 't 它正确地失败了,但没有它它编译并导致未定义的行为:

fn breaking<'a, 's, 'x>(st: &'s mut RefStack<'a, i32>, v: &'x mut Vec<&'a i32>) {
    v.push(st.get(0));
}
fn main() {
    let mut st = RefStack::<i32>::new();
    let mut y = Vec::new();
    {
        let i = 42;
        st.with_element(&i, |stack| breaking(stack, &mut y));
    }
    println!("{:?}", y);
}

关于 panic!

当做这些不安全的事情时,特别是当你调用用户代码时,就像我们现在在 with_element 中所做的那样,我们必须考虑如果它发生恐慌会发生什么。在 OP 代码中,最后一个对象 不会 被弹出,并且当堆栈展开时,任何 drop 实现都可以看到现在悬空的引用。我的代码在出现恐慌时没问题,因为如果 f(&mut tmp); 悬空引用在本地临时 tmp 中消失,而 self.content 为空。

免责声明:不同的答案;使用不同的 trade-off.

与我的其他答案相比,此答案提供的解决方案是:

  • 不安全:它是封装的,但很微妙。
  • 使用起来更简单。
  • 更简单的代码,可能更快。

想法是 仍然 使用堆栈来绑定引用的生命周期,但将所有生命周期存储在单个 Vec 中用于 O(1) 随机访问.所以我们在栈上构建一个栈,而不是将引用本身存储在栈上。好吗?

完整代码is available here.

堆栈本身很容易定义:

struct StackRoot<T: ?Sized>(Vec<*const T>);

struct Stack<'a, T: ?Sized>{
    len: usize,
    stack: &'a mut Vec<*const T>,
}

impl<T: ?Sized> StackRoot<T> {
    fn new() -> Self { Self(vec!()) }

    fn stack(&mut self) -> Stack<'_, T> { Stack { len: 0, stack: &mut self.0 } }
}

Stack 的实现比较棘手,当涉及到 unsafe 时总是如此:

impl<'a, T: ?Sized> Stack<'a, T> {
    fn len(&self) -> usize { self.len }

    fn get(&self, index: usize) -> Option<&'a T> {
        if index < self.len {
            //  Safety:
            //  -   Index is bounds as per above branch.
            //  -   Lifetime of reference is guaranteed to be at least 'a (see push).
            Some(unsafe { &**self.stack.get_unchecked(index) })
        } else {
            None
        }
    }

    fn push<'b>(&'b mut self, element: &'b T) -> Stack<'b, T>
        where
            'a: 'b
    {
        //  Stacks could have been built and forgotten, resulting in `self.stack`
        //  containing references to further elements, so that the newly pushed
        //  element would not be at index `self.len`, as expected.
        //
        //  Note that on top of being functionally important, it's also a safety
        //  requirement: `self` should never be able to access elements that are
        //  not guaranteed to have a lifetime longer than `'a`.
        self.stack.truncate(self.len);

        self.stack.push(element as *const _);
        Stack { len: self.len + 1, stack: &mut *self.stack }
    }
}

impl<'a, T: ?Sized> Drop for Stack<'a, T> {
    fn drop(&mut self) {
        self.stack.truncate(self.len);
    }
}

注意这里的unsafe;不变的是'a参数总是比压入堆栈的元素的生命周期到目前为止.[=24=更严格]

通过拒绝访问其他成员推送的元素,我们从而保证返回引用的生命周期有效。

它确实需要 do_recursion 的通用定义,但是通用生命周期参数在代码生成时被删除,因此不涉及代码膨胀:

fn do_recursion<'a, 'b>(nodes: &[&'a str], stack: &mut Stack<'b, str>) 
    where
        'a: 'b
{
    let tmp: &str = stack.get(stack.len() - 1).expect("Not empty");
    println!("{:?}", tmp);

    if let [head, tail @ ..] = nodes {
        let mut new = stack.push(head);
        do_recursion(tail, &mut new);
    }
}

一个简单的 main 炫耀:

fn main() {
    let nodes = ["Hello", ",", "World", "!"];
    let mut root = StackRoot::new();
    let mut stack = root.stack();
    let mut stack = stack.push(nodes[0]);

    do_recursion(&nodes[1..], &mut stack);
}

导致:

"Hello"
","
"World"
"!"

基于,我实现了这个稍微简单一点的版本:

struct RefStack<'a, T: ?Sized + 'static> {
    content: Vec<&'a T>,
}

impl<'a, T: ?Sized + 'static> RefStack<'a, T> {
    fn new() -> Self {
        RefStack {
            content: Vec::new(),
        }
    }

    fn get(&self, index: usize) -> &'a T {
        self.content[index]
    }

    fn len(&self) -> usize {
        self.content.len()
    }

    fn with_element<'t, F: >(&mut self, el: &'t T, f: F)
    where
        F: FnOnce(&mut RefStack<'t, T>),
        'a: 't,
    {
        let mut st = RefStack {
            content: std::mem::take(&mut self.content),
        };
        st.content.push(el);
        f(&mut st);
        st.content.pop();
        self.content = unsafe { std::mem::transmute(st.content) };
    }
}

与 rodrigo 解决方案的唯一区别是向量表示为引用向量而不是指针,因此我们不需要 PhantomData 和不安全代码来访问元素。

当一个新元素被压入 with_element() 中的堆栈时,我们要求它的生命周期比具有 a': t' 绑定的现有元素更短。然后我们创建一个生命周期较短的新堆栈,这在安全代码中是可能的,因为我们知道向量中的引用指向的数据甚至生命周期较长 'a。然后,我们将生命周期为 't 的新元素推送到新向量,再次使用安全代码,只有在我们再次删除该元素后,我们才将向量移回其原始位置。这需要不安全的代码,因为我们这次 将向量中引用的生命周期从 't 延长 'a。我们知道这是安全的,因为向量回到了它的原始状态,但编译器不知道这一点。

我觉得这个版本比罗德里戈几乎相同的版本更能表达意图。 vector 的类型始终是“正确的”,因为它描述了元素实际上是引用,而不是原始指针,并且它总是为 vector 分配正确的生命周期。我们恰好在可能发生不安全事件的地方使用不安全代码——当延长向量中引用的生命周期时。