[0]
Rust의 Cell과 RefCell은 컴파일러를 속여, interior mutability를 제공하는 기능이다. 여기에서는 Rust 코드 내에서 어떻게 이를 제공하는지 알아보자. Rust 버전은 1.44.0이다.
[1]
우선 Cell은 src/libcore/cell.rs에 정의되어 있다.
pub struct Cell<T: ?Sized> {
value: UnsafeCell<T>,
}
...
impl<T> Cell<T> {
...
pub fn replace(&self, val: T) -> T {
// SAFETY: This can cause data races if called from a separate thread,
// but `Cell` is `!Sync` so this won't happen.
mem::replace(unsafe { &mut *self.value.get() }, val)
}
pub fn set(&self, val: T) {
let old = self.replace(val);
drop(old);
}
...
}
Cell은 간단하게 UnsafeCell을 wrapping 하고 있다. interior mutablility를 제공하는 set() 메소드가 중요한데(파라미터가 &mut self가 아니라 &self임을 주목하자), 여기서는 replace() 메소드를 호출하고, replace()에서는 UnsafeCell의 get 메소드를 호출한다.
#[lang = "unsafe_cell"]
#[stable(feature = "rust1", since = "1.0.0")]
#[repr(transparent)]
#[repr(no_niche)] // rust-lang/rust#68303.
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
impl<T: ?Sized> UnsafeCell<T> {
...
pub const fn get(&self) -> *mut T {
// We can just cast the pointer from `UnsafeCell<T>` to `T` because of
// #[repr(transparent)]. This exploits libstd's special status, there is
// no guarantee for user code that this will work in future versions of the compiler!
self as *const UnsafeCell<T> as *const T as *mut T
}
...
}
Rust의 interior mutability의 가장 핵심적인 struct인 UnsafeCell과 가장 핵심적인 메소드인 get()이다. UnsafeCell 멤버는 그저 value를 wrapping하고 있을 뿐이므로 메소드를 보자. get()은 파라미터로 imuutable인 &self를 받아서 mutable raw pointer인 *mut T를 리턴한다. 그런데 내부에서는 단순히 캐스팅만을 할 뿐이다. 3단계의 캐스팅을 거쳐, 마지막에는 *const T를 *mut T로 캐스팅하는데, 당연하지만 이는 Rust compiler에서 금지된 행동이다. 이것이 허용되면 imutable의 의미가 전혀 없어져버리니 금지되어 있다. 그럼 UnsafeCell에서는 어떻게 캐스팅을 하는걸까?
[3]
답은 바로 #[lang = "unsafe_cell"] 라는 키워드다. 이 키워드를 컴파일러에게 전달해주어, 원래는 금지된 immputable reference -> mutable reference의 캐스팅을 한다. 이 과정을 약간 더 자세히 알고 싶다면, 이 포스팅을 읽어보자.
정리를 하자면, Cell은 그 자체로 interior mutability 제공하지는 못하고, UnsafeCell에서 이를 제공한다. UnsafeCell은 특별한 테크닉을 사용하는 것이 아니라, Rust 언어에서 정의된 키워드를 통해 컴파일러의 제약을 해제하여 interior mutablilby를 제공한다.
RefCell 또한 유사하게 UnsafeCell을 사용하는데 이쪽도 살펴보자.
[4]
// Positive values represent the number of `Ref` active. Negative values
// represent the number of `RefMut` active. Multiple `RefMut`s can only be
// active at a time if they refer to distinct, nonoverlapping components of a
// `RefCell` (e.g., different ranges of a slice).
//
// `Ref` and `RefMut` are both two words in size, and so there will likely never
// be enough `Ref`s or `RefMut`s in existence to overflow half of the `usize`
// range. Thus, a `BorrowFlag` will probably never overflow or underflow.
// However, this is not a guarantee, as a pathological program could repeatedly
// create and then mem::forget `Ref`s or `RefMut`s. Thus, all code must
// explicitly check for overflow and underflow in order to avoid unsafety, or at
// least behave correctly in the event that overflow or underflow happens (e.g.,
// see BorrowRef::new).
type BorrowFlag = isize;
const UNUSED: BorrowFlag = 0;
/// A mutable memory location with dynamically checked borrow rules
///
/// See the [module-level documentation](index.html) for more.
#[stable(feature = "rust1", since = "1.0.0")]
pub struct RefCell<T: ?Sized> {
borrow: Cell<BorrowFlag>,
value: UnsafeCell<T>,
}
RefCell도 Cell과 마찬가지로 값을 UnsafeCell로 감싸고 있지만, 추가적으로 borrow라는 Cell<BorrowFlag>타입의 멤버가 하나 더 있다. 얘가 어떤 역할을 하는지는 RefCell의 public method인 try_borrow_mut()를 통해 알아보자.
impl<T: ?Sized> RefCell<T> {
...
pub fn try_borrow_mut(&self) -> Result<RefMut<'_, T>, BorrowMutError> {
match BorrowRefMut::new(&self.borrow) {
// SAFETY: `BorrowRef` guarantees unique access.
Some(b) => Ok(RefMut { value: unsafe { &mut *self.value.get() }, borrow: b }),
None => Err(BorrowMutError { _private: () }),
}
}
...
}
struct BorrowRefMut<'b> {
borrow: &'b Cell<BorrowFlag>,
}
impl<'b> BorrowRefMut<'b> {
...
fn new(borrow: &'b Cell<BorrowFlag>) -> Option<BorrowRefMut<'b>> {
// NOTE: Unlike BorrowRefMut::clone, new is called to create the initial
// mutable reference, and so there must currently be no existing
// references. Thus, while clone increments the mutable refcount, here
// we explicitly only allow going from UNUSED to UNUSED - 1.
match borrow.get() {
UNUSED => {
borrow.set(UNUSED - 1);
Some(BorrowRefMut { borrow })
}
_ => None,
}
}
...
}
try_borrow_mut()는 위에서 살펴본 UnsafeCell의 get()을 통해 *const T를 받아 온 뒤 &mut T로 변환시켜서 리턴해준다. 이 과정에서 BorrowFlag를 체크한다. BorrowFlag는 0으로 초기화되어있고, 0이면 -1로 변경한 뒤에 &mut T를 리턴, 0이 아니면 에러를 리턴하는 심플한 로직이다.
impl Drop for BorrowRefMut<'_> {
#[inline]
fn drop(&mut self) {
let borrow = self.borrow.get();
debug_assert!(is_writing(borrow));
self.borrow.set(borrow + 1);
}
}
try_borrow_mut()를 통해 리턴된 mutable이 scope 밖으로 나가면 자동으로 Drop trait의 drop()메소드를 호출한다. 여기에서 다시 BorrowFlag를 리셋한다. 즉 RefCell은 현재의 값이 mutable하게 borrow되었는지 내부적으로 체크하는 flag를 가지고 있고, 이를 통해 런타임에 mutablility aliasing을 체크한다.
참고
doc.rust-lang.org/nomicon/coercions.html
stackoverflow.com/questions/33233003/how-does-the-rust-compiler-know-cell-has-internal-mutability
ricardomartins.cc/2016/07/11/interior-mutability-behind-the-curtain
'Computer Science > Rust' 카테고리의 다른 글
Unsafe Rust (0) | 2020.09.27 |
---|---|
Rust의 trait object (0) | 2020.08.02 |
Rust의 async/await와 Future (0) | 2020.07.21 |
Rust의 Copy trait와 Clone trait (2) | 2020.06.30 |
Rust의 스마트 포인터 (0) | 2020.05.20 |