不安全的 Rust 中的堆栈引用,但确保不安全不会从堆栈中泄漏?

Posted

技术标签:

【中文标题】不安全的 Rust 中的堆栈引用,但确保不安全不会从堆栈中泄漏?【英文标题】:Stack of references in unsafe Rust, but ensuring that the unsafeness does not leak out of the stack? 【发布时间】:2020-12-04 02:21:42 【问题描述】:

我正在实现一些递归代码,其中调用堆栈更深处的函数实例可能需要引用来自先前帧的数据。但是,我只能对这些数据进行非 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() 的闭包只提供了一个非 mut @ 987654326@。请注意,传入闭包的非mut str 只能在get_str() 实现中构造,因此我们不能假设所有&amp;str 具有相同的生命周期。

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

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

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

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

【问题讨论】:

通过避免引用的不同方法可能会更好地解决您的问题,但很难说,因为您没有描述您要解决的实际问题。也就是说,我认为这本身仍然是一个很好的问题,即使它不是解决您问题的最佳方法。 您需要随机访问堆栈元素,还是只需要迭代访问? @MatthieuM。我需要随机访问堆栈元素。我需要的元素的索引通常取决于从当前LinkedListNodehead计算的一些属性。 @SvenMarnach 我认为那里仍然存在一些不安全因素 - 第 31 行的变量 tmp 可能会比它最初插入的帧的寿命更长。 @Bernard 我不这么认为,因为get() 方法返回的引用具有传入的&amp;self 引用的生命周期,而不是生命周期'a 【参考方案1】:

基于rodrigo's answer,我实现了这个稍微简单的版本:

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

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

【讨论】:

这很好,但我认为这个版本实现有一个微妙的问题,可能仍然不健全:如果f(st) 出现恐慌怎么办?本地的&amp;'t 引用永远不会被弹出!外部drop 实现可能会看到堆栈并访问未弹出的、现在悬空的引用。 @rodrigo 你是对的,它仍然不健全。与您的版本相比,我对正交更改进行了:(a)我将向量的类型更改为Vec&lt;&amp;'a T&gt; 并且(b)我删除了代码以在with_elemnt() 中创建一个新的RefStack。这两个更改都可以彼此独立应用,但事实证明 (b) 不是一个好主意,而我仍然相信 (a) 是——它不仅使代码更短,而且还更好地传达了意图.我现在没时间修。 我现在解决了这个问题,使这个答案更类似于你的代码。【参考方案2】:

免责声明:这个答案最初使用了特征,这是一场噩梦; Francis Gagne 正确地指出,使用Option 作为尾部是一个更好的选择,因此答案大大简化了。

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

这种方法的主要优点是完全安全。您可以查看whole code here,或关注下面的详细说明。

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

【讨论】:

当我实际将它与我的链表一起使用时,我不会得到一个无限类型吗? (我的代码中n 的值在编译时是未知的。) @Bernard:如果您尝试命名类型,您会这样做。这就是trait 的用武之地,通过将​​&amp;dyn Tail&lt;Element = str&gt; 传递给do_recursion,它不再是无限的。 @MatthieuM。如果您只有&amp;dyn Tail,您将如何在迭代中调用push() 我怀疑您可能需要将StackElement 中的tail 的类型更改为dyn 对象。我真的看不出StackElementget() 方法可以在汇编级别编译成什么。 @SvenMarnach:你不能这样做,因为它不是Tail 的方法,但它只是一个你可以不用它的辅助方法。【参考方案3】:

我认为存储原始指针是要走的路。您需要一个 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&lt;'a, T&gt; 转换为RefStack&lt;'t, T&gt;。我很确定我可以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(&amp;mut tmp); 悬空引用在本地临时 tmp 中消失,而 self.content 为空。

【讨论】:

这和我的实现有同样的问题。您可以通过在闭包内存储对RefStack 的另一个引用来打破这一点,并使用它来偷偷在外框的生命周期中添加到内框中的值。例如,请参阅我的答案。 哦,等等,也许这在这里不起作用,因为with_element() 通过可变引用获取self,所以我们不能在任何地方存储对堆栈的任何其他引用。跨度> @SvenMarnach:是的,这是想法的一部分。这和分开的 'a't 生命周期。我觉得没有't我应该可以打破它,但我不知道如何...... @Bernard:是的,*const TT 是协变的,但不是 'a。由于RefStack 的行为好像 它包含一堆&amp;'a T 我想它也应该与生命周期协变。而且你需要一个PhantomData 来保存生命周期,所以我很自然地写PhantomData&lt;&amp;'a T&gt; Self 在可变引用后面实际上是不变的,所以我之前评论的最后一句话并不完全正确。如果您对使用transmute 的可能性的评论是关于转换self 引用本身,那么仍然存在恐慌问题,因此必须加以解决。由于不想使用transmute,您可能无意中绕过了该恐慌问题:)【参考方案4】:

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

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

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

这个想法是仍然使用堆栈来绑定引用的生命周期,但将所有生命周期存储在单个 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 参数总是比推入堆栈的元素的生命周期到目前为止更严格。

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

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

【讨论】:

如何构建和遗忘堆栈?在我看来,StackDrop impl 应该已经处理过了。 我尝试让with_element() 工作的here 版本失败。看来我无法说服编译器 f 的参数不会超过函数调用。有什么方法可以让with_element() 工作吗? @Bernard:字面意思是std::mem::forget :) @Bernard:确实,这个with_element 很烦人——而且编译器并没有真正提供帮助。你真的需要它吗?它只是在做 let mut new = stack.push(element); do_recursion(tail, &amp;mut new); @Bernard:这是一个带有with_element here 的版本;弄清楚生命周期确实有点棘手。

以上是关于不安全的 Rust 中的堆栈引用,但确保不安全不会从堆栈中泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

了解 Rust 中的线程安全 RwLock<Arc<T>> 机制

Rust编程语言入门之高级特性

为啥 Rust 认为泄漏内存是安全的?

Rust 内存安全指南

rust内存安全--借用

rust内存安全--借用