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:
a
is created with a mutable reference toc
.a_ptr
is 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,
b
is created with a mutable reference toc
. - Finally,
b_ptr
is cast to a*mut c_void
.
-
Temporary Escape from Borrow Checking:
- The cast to
*mut c_void
removes the borrow checker’s visibility into how the mutable reference (mut_ref
) inA
is being used. Rust assumes you are responsible for managing aliasing and mutability safety when you use raw pointers.
- The cast to
-
Sequential Mutability:
- Since
a
is converted toa_ptr
(a raw pointer) beforeb
is created, the borrow checker treats the mutable borrow ina
as no longer active.
- Since
Why Part 2 Fails?
-
Simultaneous Mutable Borrows:
- In this case, both
A
andB
are holding mutable references toc
at 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 ofc
held byA
. Even though you intend to casta
andb
to 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_ptr
andb_ptr
) happens after the violation has already occurred. At the point whereb
is created, the mutable borrow inA
is 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
.