在不安全的 Rust 中引用堆栈,但要确保不安全不会泄漏出堆栈



我正在实现一些递归代码,其中调用堆栈中更深的函数实例可能需要引用先前帧中的数据。 但是,我只能对这些数据进行非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());
}

在上面的示例中,我关心如何在没有任何每项目内存分配的情况下实现RefStackVec偶尔的拨款是可以接受的——这些拨款很少,而且相距甚远。LinkedListNode只是一个示例 - 在实践中它是一些复杂的图形数据结构,但同样的事情适用 - 我只有一个非mut引用,而给manager.get_str()的闭包只提供了一个非mutstr。 请注意,传递到闭包中的非mutstr只能在get_str()实现中构造,因此我们不能假设所有&str都具有相同的生存期。

我相当确定,如果不将str复制到拥有的String中,就无法在安全的 Rust 中实现RefStack,所以我的问题是如何在不安全的 Rust 中做到这一点。 感觉我可能能够得到一个解决方案,例如:

  • 不安全仅限于实施RefStack
  • st.get()返回的引用应该至少与do_recursion函数的当前实例一样长(特别是,它应该能够在对st.with_element()的调用之后生存,这在逻辑上是安全的,因为st.get()返回的&T无论如何都不是指RefStack拥有的任何内存)

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

感觉我可以将元素引用强制转换为指针并将它们存储为指针,但是在将它们转换回引用时,我仍然会遇到表达上述第二个项目符号中要求的困难。 或者有没有更好的方法(或者这样的结构可以在安全的 Rust 中实现,或者已经在某个地方的某个库中实现)?

我认为存储原始指针是要走的路。您需要一个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();
}
}

(游乐场)

唯一的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

免责声明:这个答案原本用的是特质,简直是一场噩梦;弗朗西斯·加涅(Francis Gagne)正确地指出,使用Option作为尾巴是一个更好的选择,因此答案要简化得多。

给定您的使用结构,堆栈RefStack遵循堆栈帧的使用,您可以简单地将元素放在堆栈帧上并从中构建堆栈。

这种方法的主要优点是它是完全安全的。您可以在此处查看整个代码,或按照下面的逐次说明进行操作。

关键是想法是建立一个所谓的缺点列表。

#[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性能可能不是那么好;它具有线性复杂性。另一方面,缓存方面坚持堆栈帧非常好。如果您主要访问前几个元素,我希望它会足够好。

基于rodrigo的回答,我实现了这个稍微简单的版本:

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。我们知道这是安全的,因为向量会恢复到其原始状态,但编译器不知道这一点。

我觉得这个版本比罗德里戈几乎相同的版本更能代表意图。向量的类型始终是"正确的",因为它表明元素实际上是引用,而不是原始指针,并且它始终为向量分配正确的生存期。我们准确地在发生潜在不安全的地方使用不安全代码 - 在延长向量中引用的生存期时。

免责声明:不同的答案;有不同的权衡。

与我的另一个答案相比,这个答案提出了一个解决方案:

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

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

完整的代码可在此处获得。

堆栈本身很容易定义:

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参数总是到目前为止推送到堆栈中的元素的生存期更严格。

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

它确实需要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"
"!"

最新更新