Rust中存在字段引用依赖的自引用结构体初始化方案问询
这是Rust里典型的自引用结构体场景:结构体的一个字段持有对同结构体另一个字段的引用,普通的结构体初始化语法完全没法处理这种情况。
核心原因有两点:
- 结构体按字段顺序初始化,一旦把局部变量
bytes移动到结构体的bytes字段,局部的bytes就失效了,没法再借用它传给PE::parse - 就算绕开初始化顺序,只要
Binary实例在内存中发生移动(比如作为返回值返回、存入集合),bytes的内存地址就会变化,pe里存的引用会直接变成悬空指针,触发内存未定义行为。
下面是按推荐度排序的可行实现方式:
方案1:使用goblin的自有所有权PE变体(最推荐,无额外依赖,无unsafe)
goblin默认做零拷贝解析,PE结构会直接借用输入字节切片的内存,但开启alloc feature后,PE提供了into_owned()方法,可以把所有借用的字节数据拷贝成自身持有的数据,彻底消除生命周期约束,不需要任何自引用技巧。
首先在Cargo.toml中调整goblin的依赖配置,开启alloc特性:
goblin = { version = "0.8", features = ["alloc", "pe32", "pe64"] }
之后就可以直接正常初始化结构体:
struct Binary { path: PathBuf, bytes: Vec<u8>, pe: PE<'static>, // PE完全持有自身所有数据,无外部生命周期依赖 } impl Binary { fn read(p: PathBuf) -> std::io::Result<Self> { let bytes = std::fs::read(&p)?; let pe = PE::parse(&bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? .into_owned(); Ok(Self { path: p, bytes, pe }) } }
这个方案的唯一代价是PE中原本零拷贝引用的节数据、字符串等内容会多拷贝一次,内存占用小幅上升,但换来了最简洁的代码逻辑,没有任何unsafe,也不需要额外引入依赖,90%以上的场景下这都是最优选择。
方案2:使用自引用库实现零拷贝存储(性能优先场景)
如果你需要严格保留goblin的零拷贝解析特性,不想承担额外的拷贝开销,可以用成熟的自引用库ouroboros来安全实现自引用逻辑,不需要自己手写unsafe:
- 引入ouroboros依赖
ouroboros = "0.18"
- 用库提供的宏定义自引用结构体:
use ouroboros::self_referencing; #[self_referencing] struct Binary { path: PathBuf, bytes: Vec<u8>, #[borrows(bytes)] #[covariant] pe: PE<'this>, } impl Binary { fn read(p: PathBuf) -> std::io::Result<Self> { let bytes = std::fs::read(&p)?; Ok(BinaryBuilder { path: p, bytes, pe_builder: |bytes_ref| PE::parse(bytes_ref).unwrap() }.build()) } // 需要访问PE实例时通过库生成的borrow方法获取 fn pe(&self) -> &PE<'_> { self.borrow_pe() } }
ouroboros会自动处理结构体移动时的内存稳定性,保证bytes的地址不会因为结构体移动失效,所有unsafe逻辑都被库封装验证过,不会出现悬空指针问题。
不建议采用的方案
- 不要自己手写裸指针、Pin等逻辑实现自引用,除非你对Rust的内存模型、Pin的安全约束有极深的理解,这类代码非常容易引入难以排查的内存漏洞。
- 你当前使用的每次调用都重新解析PE的方案,仅适合pe方法调用频率极低的场景,如果频繁调用会产生非常明显的性能浪费,不建议在生产环境使用。
内容的提问来源于stack exchange,提问作者Jb Evain
相关产品推荐
相关产品推荐

