Rust의 기존 기능과 더 일관되게 핀 참조의 프로젝션을 허용하고, 예외적인 필드는 UnpinCell이라는 새 셀 타입으로 처리하자는 제안과 예제 설명.
이전에 썼던 pinned places에 대한 설계를, Rust의 기존 기능 집합과 더 일관되게 만들 수 있는 변형이 떠올랐습니다.
이전 설계에서 가장 엉뚱했던 부분은 고정(projection) 가능한 “pinned 필드”라는 개념이었습니다. 이는 Rust에서 일반적인 필드 프로젝션 방식과 꽤 다릅니다. 보통 구조체에 대한 가변 참조가 있으면, 그 필드에 대한 가변 참조를 얻을 수 있습니다. (최근 Niko Matsakis가 이를 바꾸는 아이디어를 탐구한 것으로 압니다만, 이 글에서는 그 제안을 깊게 다루지 않습니다.) 저는 필드 마커 같은 것을 도입하지 않고도 비슷한 성질을 갖게 하는 설계를 생각해 보았습니다.
먼저, 핀 참조는 다른 참조처럼 프로젝션을 지원해야 합니다. 지금까지 그렇지 못한 유일한 이유는 제가 예전에 이 글에서 논의한 Drop 주변의 음성(unsound)성 때문입니다. 이를 우회하는 방법은, 다음 기준을 만족하는 타입을 경유해 프로젝션할 때에 한해 핀 참조의 프로젝션을 허용하는 것입니다:
Unpin을 구현한다면, 수동으로 impl을 작성하는 것이 아니라 오토 트레이트 메커니즘을 통해 구현해야 한다.Drop을 구현한다면, Unpin도 구현하든지, 아니면 파괴자(destructor)가 fn drop(&pin mut self) 시그니처를 사용해야 한다.타입이 이 요구사항을 만족하는 한, 안전한 코드로는 핀 보장을 깨뜨릴 방법이 없습니다. (이 글의 이전 버전에서는, Unpin이 음성을 피하려면 오토 트레이트 메커니즘으로 구현되어야 한다는 점을 깜빡해 기준을 잘못 서술했습니다.)
하지만 객체 전체에 적용되는 핀 계약의 예외인, ‘unpinned 필드’를 지원할 방법은 여전히 필요합니다. 이를 위해 언어에 새로운 “셀(cell)” 타입인 UnpinCell을 도입해, 그 안에 들어 있는 어떤 객체든 “unpin”되도록 합니다. UnpinCell의 API는 대략 다음과 같을 수 있습니다:
pub struct UnpinCell<T>(T);
impl<T> UnpinCell<T> {
pub fn new(value: T) -> UnpinCell<T> {
UnpinCell(value)
}
pub fn into_inner(self) -> T {
self.0
}
}
// T: !Unpin 이더라도
impl<T> Unpin for UnpinCell<T> { }
impl<T> Deref for UnpinCell<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for UnpinCell<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
이 놀라울 만큼 단순한 API는, 그 값이 Unpin을 구현하지 않았더라도, 핀 포인터를 통해서조차 UnpinCell 내부 값에 가변 접근을 허용합니다. 이는 UnpinCell이 핀 프로젝션을 통과할 수 없는 장벽을 만들기 때문에 음성적이지 않습니다. 즉, 셀 안의 객체는 결코 핀된 상태로 관측되지 않습니다.
이전 글의 MaybeDone 예제를 다시 보면 이제 다음과 같을 것입니다:
enum MaybeDone<F: Future> {
Polling(F),
Done(UnpinCell<Option<F::Output>>),
}
impl<F: Future> MaybeDone<F> {
fn maybe_poll(&pin mut self, cx: &mut Context<'_>) {
if let MaybeDone::Polling(fut) = self {
if let Poll::Ready(res) = fut.poll(cx) {
*self = MaybeDone::Done(UnpinCell::new(Some(res)));
}
}
}
fn is_done(&self) -> bool {
matches!(self, &MaybeDone::Done(_))
}
fn take_output(&pin mut self) -> Option<F::Output> {
// res: &pin mut UnpinCell<Option<F::Output>>
if let MaybeDone::Done(res) = self {
// 두 번의 deref mut 강제로 Option::take로 해석됩니다
res.take()
} else {
None
}
}
}
(문법을 pinned 대신 pin을 쓰도록 업데이트했습니다. 이는 프로젝트의 핀 인체공학 실험에서 사용하는 문법입니다.)
이 조합은 pinned places를 언어에 도입하는 변경 폭을 훨씬 더 작게 만듭니다. 핀 참조는 일반 참조처럼 동작하고, 예외적인 필드가 필요할 때는 “내부 가변성(interior mutability)”과 마찬가지로 “내부 비고정성(interior unpinnability)”을 위한 셀 같은 API를 사용하면 됩니다. 제 생각에 이는 필드 한정자(modifier)를 도입하는 것보다 우수합니다. 새로운 범주의 언어 기능을 추가하지 않기 때문입니다.