【问题标题】:How can I use PhantomData in a struct, with raw pointers, such that the struct does not outlive the lifetime of the referenced other struct?如何使用原始指针在结构中使用 PhantomData,以使结构不会超过所引用的其他结构的生命周期?
【发布时间】:2021-06-26 15:07:11
【问题描述】:

我的结构有不安全的代码和指向另一种结构的原始可变指针。不安全结构只能在另一个结构的生命周期内使用,但您不能为指针指定生命周期。我发现std::marker::PhantomData 可以用于这个未使用的生命周期问题,但我在让它工作时遇到问题。我不确定这是无效用例还是我做错了什么。

Simplified Example:

use std::marker::PhantomData;

pub struct Test {
    value: u32,
}

impl Test {
    pub fn value(&self) {
        println!("{}", self.value)
    }

    pub fn set_value(&mut self, value: u32) {
        self.value = value;
    }
}

// I want compiler to complain about the lifetime of test
// so that UnsafeStruct is not used after test is dropped
pub struct UnsafeStruct<'a> {
    test: *mut Test,
    phantom: PhantomData<&'a mut Test>,
}

impl<'a> UnsafeStruct<'a> {
    pub fn new(test: &'a mut Test) -> UnsafeStruct<'a> {
        UnsafeStruct {
            test: test,
            phantom: PhantomData,
        }
    }

    pub fn test_value(&self) {
        unsafe { println!("{}", (*self.test).value) }
    }

    pub fn set_test_value(&mut self, value: u32) {
        unsafe {
            (*self.test).set_value(value);
        }
    }
}

fn main() {
    // No borrow checker errors
    // but the compiler does not complain about lifetime of test
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct {
            test: &mut test,
            phantom: PhantomData,
        };

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value();

    // Lifetime errors caught
    // but there will be borrow checker errors if you fix
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct::new(&mut test);

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value();

    // Borrow checker errors when you fix lifetime error
    {
        let mut test = Test { value: 0 };
        let mut unsafe_struct: UnsafeStruct;
        unsafe_struct = UnsafeStruct::new(&mut test);

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
}

如果我直接创建UnsafeStruct,编译器不会捕获生命周期错误,我还是想使用构造函数。如果我使用构造函数,那么我有借用检查器错误。是否可以修复此代码,以使编译器在尝试在相应 Test 的生命周期之外使用 UnsafeStruct 时会出错,但不会出现示例中显示的借用检查错误?

【问题讨论】:

  • 为什么不直接使用可变引用?
  • 如果您的意思是 UnsafeStruct 中的可变引用,由于我需要不安全的行为而无法正常工作。
  • 你能解释一下“我需要不安全的行为”是什么意思吗?
  • 我需要原始指针功能。我需要直接在借用检查规则之外操作结构。
  • 您可以在结构中存储可变引用,并在需要时将其转换为不安全代码中的原始指针。

标签: rust


【解决方案1】:

我正在回答我自己的问题。我试图解决的问题是使用std::marker::PhantomData 来实现向带有原始指针的结构添加生命周期,以防止在出现错误后使用。 PhantomData 无法实现这一点。有一个处理未处理生命周期的用例,但这与我尝试完成的不同,是我困惑/问题的根源。

我已经意识到并且已经处理了这样一个事实,即在使用不安全代码时必须处理释放后的使用和其他错误。我只是想我可能能够在编译时而不是运行时出现免费错误后处理这种类型的使用。

【讨论】:

  • 在使用不安全代码时,您仍然可以避免使用后释放错误。这取决于您想要达到的目标。比如stdVec是用不安全的代码写的,但是Vec上的free是禁止使用的。
【解决方案2】:

TL;DR您所做的事情违反了可变引用的排他性要求,但您可以使用共享引用和内部可变性来制作有效的 API。

&amp;mut T 引用代表独占访问T。当您使用&amp;mut 借用对象时,在&amp;mut 借用的生命周期内,不得通过任何其他引用访问(可变或不可变)该对象。在这个例子中:

let mut test = Test { value: 0 };
let mut unsafe_struct: UnsafeStruct;
unsafe_struct = UnsafeStruct::new(&mut test);

unsafe_struct.set_test_value(1);
test.value();

test.set_value(2);
unsafe_struct.test_value();

unsafe_struct 保持 &amp;mut 借用的 test 活动。它内部包含一个原始指针并不重要;它可以不包含任何内容。 UnsafeStruct&lt;'a&gt; 中的 'a 延长了借用的生命周期,使得直接访问 test 成为未定义行为,直到最后一次使用 unsafe_struct 之后。

该示例表明您实际上希望共享 访问资源(即在testunsafe_struct 之间共享)。 Rust 有一个共享的引用类型;它是&amp;T。如果您希望原始 T 在借用活动期间仍可访问,则该借用必须共享 (&amp;),而不是独占 (&amp;mut)。

如果你只有一个共享引用,你如何改变一些东西?使用内部可变性

use std::cell::Cell;

pub struct Test {
    value: Cell<u32>,
}

impl Test {
    pub fn value(&self) {
        println!("{}", self.value.get())
    }

    pub fn set_value(&self, value: u32) {
        self.value.set(value);
    }
}

pub struct SafeStruct<'a> {
    test: &'a Test,
}

impl<'a> SafeStruct<'a> {
    pub fn new(test: &'a Test) -> SafeStruct<'a> {
        SafeStruct { test }
    }

    pub fn test_value(&self) {
        println!("{}", self.test.value.get())
    }

    pub fn set_test_value(&self, value: u32) {
        self.test.set_value(value);
    }
}

没有留下unsafe 代码——Cell 是一个安全的抽象。您也可以使用AtomicU32 代替Cell&lt;u32&gt;,以确保线程安全,或者如果Test 的实际内容更复杂,则使用RefCellRwLockMutex。这些都是提供共享(“内部”)可变性的抽象,但它们的用法不同。阅读以下文档和链接了解更多详情。

作为最后的手段,如果您需要在没有开销的情况下对对象进行共享可变访问,并自行承担保证其正确性的全部责任,您可以使用UnsafeCell。这确实需要使用unsafe 代码,但您可以编写任何您想要的API。请注意,我刚才提到的所有安全抽象都是在内部使用UnsafeCell 构建的。没有它,你就无法共享可变性。

链接

【讨论】:

  • 我创建了自己的答案,准确解释了发生了什么,因为您的答案没有直接回答我关于使用std::marker::PhantomData 的问题。 PhantomData 周围的文档和信息很简单,我从一些措辞中得到了错误的印象。我认为借用检查行为不同,因为 PhantomData 引用实际上并不存在于运行时代码中,只是为了编译器澄清。
  • 您的关于生命周期、细胞、内部可变性等的信息是正确的。它与我试图实现的目标不太相关。我提供了一个简化的示例来说明我遇到的PhantomData 的问题。该示例不代表我的实际代码。我不希望/不能使用单元格或其他内部可变性将其转换为安全代码。
【解决方案3】:

我看到这个问题有一个可接受的答案,但我想为这个问题添加更多解释。

注意:在使用不安全的代码时,您仍然可以保护自己免受释放后使用错误的影响。这取决于您想要达到的目标。比如stdVec是用不安全的代码写的,但是Vec上的free是禁止使用的。

以下是对该问题的详细解释。

例如 1

    // No borrow checker errors
    // but the compiler does not complain about lifetime of test
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct {
            test: &mut test,
            phantom: PhantomData,
        };

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3); // line uaf
    unsafe_struct.test_value();      //line uaf

我假设您在问为什么 rust 编译器接受标有 line uaf 的行。答案是您直接操作指针。 Rust 生命周期只能在引用的情况下工作。

第二个例子

       Lifetime errors caught
    but there will be borrow checker errors if you fix
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct::new(&mut test);

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value();

借用检查器错误很容易理解,就像在 Rust 中一样,你不能同时拥有多个可变引用或可变引用和不可变引用。 而对于生命周期错误,这是因为当您使用::new 创建UnsafeStruct 时,您正在使用该函数 pub fn new(test: &amp;'a mut Test) -&gt; UnsafeStruct&lt;'a&gt; 。此函数断言输入引用 test 将与结构的生命周期 'a 一样有效。 但是在上面的代码中(示例 2),new 函数的输入生命周期比结构的生命周期短。让我将它们注释如下

    let mut unsafe_struct: UnsafeStruct; 
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct::new(&'test mut test); // the reference of test has the lifetime 'test which is clear that it is shorter than the lifetime 'struct.

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value(); // the lifetime of unsafe_struct ends here.

有了上面的解释,例子3应该就清楚了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多