Rust中将指向结构体第一个成员的指针转换为指向结构体的指针是否合法?

12
在C语言中,一个指向结构体的指针可以转换为指向其第一个成员的指针,反之亦然。也就是说,结构体的地址被定义为其第一个成员的地址。
struct Base { int x; };
struct Derived { struct Base base; int y; };

int main() {
    struct Derived d = { {5}, 10 };
    struct Base *base = &d.base; // OK
    printf("%d\n", base->x);
    struct Derived *derived = (struct Derived *)base; // OK
    printf("%d\n", derived->y);
}

这通常用于实现类似 C++ 的继承。

如果结构体使用了 repr(C)(使其字段不被重新排列),在 Rust 中是否允许相同的操作呢?

#[derive(Debug)]
#[repr(C)]
struct Base {
    x: usize,
}

#[derive(Debug)]
#[repr(C)]
struct Derived {
    base: Base,
    y: usize,
}

// safety: `base` should be a reference to `Derived::base`, otherwise this is UB
unsafe fn get_derived_from_base(base: &Base) -> &Derived {
    let ptr = base as *const Base as *const Derived;
    &*ptr
}

fn main() {
    let d = Derived {
        base: Base {
            x: 5
        },
        y: 10,
    };

    let base = &d.base;
    println!("{:?}", base);

    let derived = unsafe { get_derived_from_base(base) }; // defined behaviour?
    println!("{:?}", derived);
}

这段代码可行,但它是否一直可行,并且是否符合定义的行为?

1个回答

11

按照你写的方式,目前还不行;但是可以使其工作。

引用T只允许访问T - 没有更多(它对T具有来源)。表达式&d.base会给你一个引用,仅适用于Base。将其用于访问Derived的字段是未定义的行为。目前这是否符合我们的意愿尚不清楚,正在积极讨论(也可以参考这个),但这是当前的行为。有一个很好的工具叫做Miri,可以检查你的Rust代码中是否存在某些(并非全部!)未定义的行为(你可以在playground中运行它;Tools->Miri),确实标记了你的代码

error: Undefined Behavior: trying to reborrow <untagged> for SharedReadOnly permission at alloc1707[0x8], but that tag does not exist in the borrow stack for this location
  --> src/main.rs:17:5
   |
17 |     &*ptr
   |     ^^^^^
   |     |
   |     trying to reborrow <untagged> for SharedReadOnly permission at alloc1707[0x8], but that tag does not exist in the borrow stack for this location
   |     this error occurs as part of a reborrow at alloc1707[0x0..0x10]
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
           
   = note: inside `get_derived_from_base` at src/main.rs:17:5
note: inside `main` at src/main.rs:31:28
  --> src/main.rs:31:28
   |
31 |     let derived = unsafe { get_derived_from_base(base) }; // defined behaviour?
   |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^

您可以通过创建对整个 Derived 的引用并将其转换为 Base 的原始指针来使其工作。原始指针将保留原始引用的来源,因此这将起作用

// safety: `base` should be a reference to `Derived`, otherwise this is UB
unsafe fn get_derived_from_base<'a>(base: *const Base) -> &'a Derived {
    let ptr = base as *const Derived;
    &*ptr
}

fn main() {
    let d = Derived {
        base: Base {
            x: 5
        },
        y: 10,
    };

    let base = &d as *const Derived as *const Base;
    println!("{:?}", unsafe { &*base });

    let derived = unsafe { get_derived_from_base(base) };
    println!("{:?}", derived);
}

注意:在这个过程中,引用不应该参与其中。如果您将base重新借用为Base类型的引用,那么您将再次失去来源。这可以通过Miri Playground,但根据当前规则,仍然是未定义的行为,并且在使用更严格的标志(在本地运行Miri之前将环境变量MIRIFLAGS设置为-Zmiri-tag-raw-pointers)时会导致Miri失败。


在支持类似于C99之前有用的Common Initial Sequence保证的语言中,通常的范例是传递一个指向外部对象的指针或引用,该对象将被转换为包含公共结构成员的类型。然而,Clang和gcc将标准解释为禁止这种构造,并且维护者们表示,即使函数可能需要使用外部类型的成员来传递这样一种类型的对象,程序也应该将共享项放在内部类型中并进行转换。为什么会有这样的需求呢... - supercat
仅为了优化而存在的语言规则,使程序员难以完成他们需要做的事情,是不好的。另一方面,如果有其他方法可以同样或更好地工作,那么禁止某种特定方式做某事是可以的。能够将指针传递给各种结构体的代码,可以交替使用公共部分,这是有用的;关于是否支持这些结构的问题应取决于程序员为什么使用它们以及存在哪些替代方案。 - supercat
一个好的编程语言为什么不能提供一种结构,允许一种方式来表达“在这个块中,使用该指针标识的对象可以以编译器不理解的方式被引用,因此,在这个块中执行的任何操作都必须在使用该指针的基本对象之前进行排序,并在使用该指针的后续对象之前进行排序”?当然,这将阻止“优化”,但这正是整个意图--如果需要按照某个特定顺序执行操作,则重新排列它们将是一种破坏性的变更,而不是一种优化。 - supercat
如果执行某些有用任务所需的机器代码很简单,那么适合这种任务的语言不应该使其变得困难。因为拒绝允许语义,认为它们会干扰“优化”,这是一种比Knuth警告的任何一种更糟糕的过早优化形式。如果一种语言不能让程序员可靠地实现所需的语义,那么生成的代码执行任务时出错的速度是无关紧要的。 - supercat
一种语言为了保持常见情况的速度而牺牲了不常见情况的易用性。这在C语言中也是如此。高级语言并不是机器码,一个特定问题的机器码简单,并不意味着语言中的代码也会简单。你基本上想要一种没有未定义行为(至少是暂时的)的语言。但在这里争论这个问题是无用的;你可以请求Rust设计者(或任何其他编程语言的设计者)允许这样的事情,但我强烈怀疑他们会接受。 - Chayim Friedman
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接