Rust: Sharing a Single Object Across Multiple Owners
Today, I found an interesting way to share a single object(C) among multiple owners(A and B)
use std::ffi::c_void;
#[derive(Debug)]
pub struct A<'a> {
mut_ref: &'a mut C,
}
#[derive(Debug)]
pub struct B<'a> {
mut_ref: &'a mut C,
}
// Common object C shared between object A and object B
#[derive(Debug)]
struct C {
val: u32,
}
fn main() {
// - Using stack objects
println!("Object c shared b/w multiple stack based objects");
let mut common = C { val: 100 };
let mut c = &mut common;
let mut a = A { mut_ref: &mut c };
let mut a_ptr = &mut a as *mut A as *mut c_void;
let mut b = B { mut_ref: &mut c };
let mut b_ptr = &mut b as *mut B as *mut c_void;
let mut a_obj = unsafe { &mut *(a_ptr as *mut A) };
let mut b_obj = unsafe { &mut *(b_ptr as *mut B) };
a_obj.mut_ref.val = 2000; // mutate c from a
println!("{:?}", a_obj.mut_ref.val); // 2000
println!("{:?}", b_obj.mut_ref.val); // 2000
b_obj.mut_ref.val = 4000; // mutate c from b
println!("{:?}", a_obj.mut_ref.val); // 4000
println!("{:?}", b_obj.mut_ref.val); // 4000
// - Using heap objects
println!("Object c shared b/w multiple heap based objects");
let mut common = C { val: 100 };
let mut c = &mut common;
let mut a = A { mut_ref: &mut c };
let mut a_ptr = Box::into_raw(Box::new(a)) as *mut c_void;
let mut b = B { mut_ref: &mut c };
let mut b_ptr = Box::into_raw(Box::new(b)) as *mut c_void;
let a_obj = unsafe { Box::<A>::from_raw(a_ptr as *mut A) };
let b_obj = unsafe { Box::<B>::from_raw(b_ptr as *mut B) };
a_obj.mut_ref.val = 2000; // mutate c from a
println!("{:?}", a_obj.mut_ref.val); // 2000
println!("{:?}", b_obj.mut_ref.val); // 2000
b_obj.mut_ref.val = 4000; // mutate c from b
println!("{:?}", a_obj.mut_ref.val); // 4000
println!("{:?}", b_obj.mut_ref.val); // 4000
}
Got curious and asked ChatGPT below question:
Question: Why Part 1 works but not Part 2? What is so special about *mut c_void?
// Part 1
let mut common = C { val: 100 };
let mut c = &mut common;
let mut a = A { mut_ref: &mut c };
let mut a_ptr = &mut a as *mut A as *mut c_void;
let mut b = B { mut_ref: &mut c };
let mut b_ptr = &mut b as *mut B as *mut c_void;
// Part 2
let mut common = C { val: 100 };
let mut c = &mut common;
let mut a = A { mut_ref: &mut c };
let mut b = B { mut_ref: &mut c };
let mut a_ptr = &mut a as *mut A as *mut c_void;
let mut b_ptr = &mut b as *mut B as *mut c_void;
Answer(From ChatGPT):
The distinction lies in how the Rust borrow checker analyzes the lifetimes and mutability of references during compilation. Let’s break this down:
Why Part 1 Works?
-
Order of Operations:
ais created with a mutable reference toc.a_ptris cast to a*mut c_void. At this point, the borrow checker no longer tracks the borrow because it has been converted to a raw pointer.- After this,
bis created with a mutable reference toc. - Finally,
b_ptris cast to a*mut c_void.
-
Temporary Escape from Borrow Checking:
- The cast to
*mut c_voidremoves the borrow checker’s visibility into how the mutable reference (mut_ref) inAis being used. Rust assumes you are responsible for managing aliasing and mutability safety when you use raw pointers.
- The cast to
-
Sequential Mutability:
- Since
ais converted toa_ptr(a raw pointer) beforebis created, the borrow checker treats the mutable borrow inaas no longer active.
- Since
Why Part 2 Fails?
-
Simultaneous Mutable Borrows:
- In this case, both
AandBare holding mutable references tocat the same time. This violates Rust’s borrowing rules, which prohibit multiple mutable borrows of the same data simultaneously.
- In this case, both
-
Borrow Checker Enforcement:
- When you write
let mut b = B { mut_ref: &mut c };, the borrow checker detects that there is already an outstanding mutable borrow ofcheld byA. Even though you intend to castaandbto raw pointers later, the borrow checker enforces its rules at the point of assignment, not at the point of casting.
- When you write
-
Raw Pointer Cast Comes Too Late:
- Unlike in the first case, the raw pointer cast (
a_ptrandb_ptr) happens after the violation has already occurred. At the point wherebis created, the mutable borrow inAis still active, leading to a compile-time error.
- Unlike in the first case, the raw pointer cast (
Key Takeaway
The difference comes down to when the mutable borrow is considered
invalidated:
- In the working code, converting A to a raw pointer before creating B
invalidates the borrow in A in the borrow checker’s view.
- In the non-working code, both A and B are created before any cast to
raw pointers, so the borrow checker detects overlapping mutable borrows.
Part 1 ensures that the borrow associated with A is no longer tracked before
creating B.