Base Code
Now that we've decided the layout for our implementation of Arc
, let's create
some basic code.
Constructing the Arc
We'll first need a way to construct an Arc<T>
.
This is pretty simple, as we just need to box the ArcInner<T>
and get a
NonNull<T>
pointer to it.
impl<T> Arc<T> {
pub fn new(data: T) -> Arc<T> {
// We start the reference count at 1, as that first reference is the
// current pointer.
let boxed = Box::new(ArcInner {
rc: AtomicUsize::new(1),
data,
});
Arc {
// It is okay to call `.unwrap()` here as we get a pointer from
// `Box::into_raw` which is guaranteed to not be null.
ptr: NonNull::new(Box::into_raw(boxed)).unwrap(),
phantom: PhantomData,
}
}
}
Send and Sync
Since we're building a concurrency primitive, we'll need to be able to send it
across threads. Thus, we can implement the Send
and Sync
marker traits. For
more information on these, see the section on Send
and
Sync
.
This is okay because:
- You can only get a mutable reference to the value inside an
Arc
if and only if it is the onlyArc
referencing that data (which only happens inDrop
) - We use atomics for the shared mutable reference counting
unsafe impl<T: Sync + Send> Send for Arc<T> {}
unsafe impl<T: Sync + Send> Sync for Arc<T> {}
We need to have the bound T: Sync + Send
because if we did not provide those
bounds, it would be possible to share values that are thread-unsafe across a
thread boundary via an Arc
, which could possibly cause data races or
unsoundness.
For example, if those bounds were not present, Arc<Rc<u32>>
would be Sync
or
Send
, meaning that you could clone the Rc
out of the Arc
to send it across
a thread (without creating an entirely new Rc
), which would create data races
as Rc
is not thread-safe.
Getting the ArcInner
To dereference the NonNull<T>
pointer into a &T
, we can call
NonNull::as_ref
. This is unsafe, unlike the typical as_ref
function, so we
must call it like this:
unsafe { self.ptr.as_ref() }
We'll be using this snippet a few times in this code (usually with an associated
let
binding).
This unsafety is okay because while this Arc
is alive, we're guaranteed that
the inner pointer is valid.
Deref
Alright. Now we can make Arc
s (and soon will be able to clone and destroy them correctly), but how do we get
to the data inside?
What we need now is an implementation of Deref
.
We'll need to import the trait:
use std::ops::Deref;
And here's the implementation:
impl<T> Deref for Arc<T> {
type Target = T;
fn deref(&self) -> &T {
let inner = unsafe { self.ptr.as_ref() };
&inner.data
}
}
Pretty simple, eh? This simply dereferences the NonNull
pointer to the
ArcInner<T>
, then gets a reference to the data inside.
Code
Here's all the code from this section:
use std::ops::Deref;
impl<T> Arc<T> {
pub fn new(data: T) -> Arc<T> {
// We start the reference count at 1, as that first reference is the
// current pointer.
let boxed = Box::new(ArcInner {
rc: AtomicUsize::new(1),
data,
});
Arc {
// It is okay to call `.unwrap()` here as we get a pointer from
// `Box::into_raw` which is guaranteed to not be null.
ptr: NonNull::new(Box::into_raw(boxed)).unwrap(),
phantom: PhantomData,
}
}
}
unsafe impl<T: Sync + Send> Send for Arc<T> {}
unsafe impl<T: Sync + Send> Sync for Arc<T> {}
impl<T> Deref for Arc<T> {
type Target = T;
fn deref(&self) -> &T {
let inner = unsafe { self.ptr.as_ref() };
&inner.data
}
}