Rust中如何为子模块设置父结构体裸指针?代码异常求解
我是一名从C转来的Rust新手,英文水平有限请见谅。Player结构体包含多个子模块,这些子模块通常需要互相引用。在C中我们习惯保存指向Player的指针,以便轻松访问其他模块或方法,但我不清楚如何在Rust的安全检查机制下组织这类代码。
我尝试使用裸指针,但仍然失败了。以下是我的代码:
struct Task { id: u32, status: u32, } struct TaskMgr { tasks: Vec<Task>, pl: *mut Player } impl TaskMgr { fn get_owner(&mut self) -> &mut Player { unsafe { &mut *self.pl } } fn submit_task(&mut self, id: u32) { unsafe { (*self.pl).add_item(1000, 1); } } fn on_add_item(&mut self, id: u32, cnt: u32) { println!("task on add item {} {} items len:{}", id, cnt, self.tasks.len()); self.tasks[0].status =2; } } struct Item { id: u32, cnt: u32, } struct ItemMgr { items: Vec<Item>, pl: *mut Player } struct Player { task_mgr: TaskMgr, item_mgr: ItemMgr, } impl Default for Player { fn default() -> Self { let mut r = Self { task_mgr: TaskMgr { tasks: vec![Task { id: 1, status: 0 }], pl: std::ptr::null_mut() }, item_mgr: ItemMgr { items: vec![Item { id: 1000 , cnt: 1}],pl: std::ptr::null_mut() }, }; unsafe { r.task_mgr.pl = std::ptr::addr_of_mut!(r); r.item_mgr.pl = &mut r; } r } } impl Player { fn submit_task(&mut self, id: u32) { self.task_mgr.submit_task(id); } fn on_add_item(&mut self, id: u32, cnt: u32) { self.task_mgr.on_add_item(id, cnt); } fn add_item(&mut self, id: u32, cnt: u32) { let i = self.item_mgr.items.iter_mut().find(|x| x.id == id); if i.is_some() { i.unwrap().cnt += 1; } self.on_add_item(id, cnt); } } fn main() { let mut pl = Player::default(); pl.submit_task(1); }
程序输出为:
task on add item 1000 2 items len:486381840784
其中items的长度明显错误,这是为什么?该如何修复?
无论是使用std::ptr::addr_of_mut!(r)还是&mut r都无效,但将指针赋值代码移到default()函数外似乎能正常运行:
let mut pl = Player::default(); unsafe { pl.task_mgr.pl = &mut pl as *mut Player; pl.item_mgr.pl = &mut pl as *mut Player; }
但这样会让代码使用起来很麻烦,请问如何在创建Player时就完成裸指针的赋值?
错误原因分析
你在Default实现中给子模块赋值的裸指针指向的是函数内的栈临时变量r。当default()执行完毕返回r时,这个临时变量的内容会被移动到调用者的内存位置(比如main里的pl),原栈地址会被回收或覆盖,导致保存的指针变成悬垂指针。后续通过这个指针访问TaskMgr的tasks时,读取的是无效内存区域,因此得到错误的长度值。
解决方案
方案1:安全的引用计数+弱指针(推荐)
如果不需要多线程,使用Rc<RefCell<Player>>配合Weak<RefCell<Player>>可以完全在Rust安全规则内实现子模块对Player的引用,避免裸指针的不安全操作:
use std::cell::RefCell; use std::rc::{Rc, Weak}; struct Task { id: u32, status: u32, } struct TaskMgr { tasks: Vec<Task>, pl: Weak<RefCell<Player>>, } impl TaskMgr { fn submit_task(&mut self, id: u32) { // 尝试升级Weak指针为强引用 if let Some(pl) = self.pl.upgrade() { let mut pl = pl.borrow_mut(); pl.add_item(1000, 1); } } fn on_add_item(&mut self, id: u32, cnt: u32) { println!("task on add item {} {} items len:{}", id, cnt, self.tasks.len()); self.tasks[0].status = 2; } } struct Item { id: u32, cnt: u32, } struct ItemMgr { items: Vec<Item>, pl: Weak<RefCell<Player>>, } struct Player { task_mgr: TaskMgr, item_mgr: ItemMgr, } impl Player { fn new() -> Rc<RefCell<Self>> { // 创建Player的引用计数实例 let pl = Rc::new(RefCell::new(Self { task_mgr: TaskMgr { tasks: vec![Task { id: 1, status: 0 }], pl: Weak::new() }, item_mgr: ItemMgr { items: vec![Item { id: 1000, cnt: 1 }], pl: Weak::new() }, })); // 给子模块设置Weak指针 let mut pl_mut = pl.borrow_mut(); pl_mut.task_mgr.pl = Rc::downgrade(&pl); pl_mut.item_mgr.pl = Rc::downgrade(&pl); pl } fn submit_task(&mut self, id: u32) { self.task_mgr.submit_task(id); } fn on_add_item(&mut self, id: u32, cnt: u32) { self.task_mgr.on_add_item(id, cnt); } fn add_item(&mut self, id: u32, cnt: u32) { if let Some(item) = self.item_mgr.items.iter_mut().find(|x| x.id == id) { item.cnt += 1; } self.on_add_item(id, cnt); } } fn main() { let pl = Player::new(); pl.borrow_mut().submit_task(1); }
这种方式通过Weak指针避免了循环引用,完全符合Rust的内存安全规则,无需手动管理裸指针。
方案2:封装裸指针的安全构造
如果你坚持使用裸指针,可以通过封装构造函数来确保指针指向最终的有效内存:
struct Task { id: u32, status: u32, } struct TaskMgr { tasks: Vec<Task>, pl: *mut Player, } impl TaskMgr { fn submit_task(&mut self, id: u32) { unsafe { (*self.pl).add_item(1000, 1); } } fn on_add_item(&mut self, id: u32, cnt: u32) { println!("task on add item {} {} items len:{}", id, cnt, self.tasks.len()); self.tasks[0].status = 2; } } struct Item { id: u32, cnt: u32, } struct ItemMgr { items: Vec<Item>, pl: *mut Player, } struct Player { task_mgr: TaskMgr, item_mgr: ItemMgr, } impl Player { fn new() -> Self { // 先创建初始实例,指针设为null let mut pl = Self { task_mgr: TaskMgr { tasks: vec![Task { id: 1, status: 0 }], pl: std::ptr::null_mut() }, item_mgr: ItemMgr { items: vec![Item { id: 1000, cnt: 1 }], pl: std::ptr::null_mut() }, }; // 此时pl已经是即将返回给调用者的实例,内存位置固定,赋值指针 unsafe { pl.task_mgr.pl = &mut pl; pl.item_mgr.pl = &mut pl; } pl } fn submit_task(&mut self, id: u32) { self.task_mgr.submit_task(id); } fn on_add_item(&mut self, id: u32, cnt: u32) { self.task_mgr.on_add_item(id, cnt); } fn add_item(&mut self, id: u32, cnt: u32) { if let Some(item) = self.item_mgr.items.iter_mut().find(|x| x.id == id) { item.cnt += 1; } self.on_add_item(id, cnt); } } fn main() { let mut pl = Player::new(); pl.submit_task(1); }
这里的核心是:new()函数中创建的pl是最终会被调用者持有实例,此时它的内存位置已经确定,赋值的指针不会变成悬垂指针。而之前的Default实现中,临时变量r在返回时会被移动,原地址失效,导致指针无效。
内容的提问来源于stack exchange,提问作者yan huang

