Namsoo CHO
Intro to Rust
Published in
15 min readApr 30, 2020

--

Pin을 사용해 보자.

러스트에서 pin이란 어떤 객체의 메모리 위치가 고정되어 재배치 되지 않도록 해주는 특성를 말한다. 이 기능이 필요한 가장 흔한 경우는 어떤 struct의 멤버 변수가 자신을 참조하는 경우이다. 이 경우 그 struct의 객체가 재배치 되면, 멤버 변수의 값은 무의미한 값을 가지게 될 것이고, 이는 예측 불가능한 결과를 가져올 것이다.

객체가 재배치 된 경우 메모리 상의 내용이 어떻게 표시되는지 예를 아래 그림에 나타내었다.

replace
replace

여기서 처음엔 객체 내의 멤버 변수가 객체의 시작위치 0x7F23을 정확히 가리키고 있다. 만약 이 객체가 임의의 위치, 0x7C44로 재배치 되었다고 한다면, 객체 내의 멤버 변수의 값은 여전히 0x7F23을 가리키게 된다. 이렇게 되면 이 객체를 사용한 연산은 결과를 예측할 수 없게 되며, 행운이 안 따라 준다면 프로세스가 panic이 날 것이다.

이와 같은 불행을 방지하고 싶다면 이 객채를 pin하여 객체의 재배치를 금지하면 된다.

러스트는 std::pin 모듈을 제공하여 이 기능을 지원한다. 어떤 Pin<P>는 포인터 타입 P의 객체를 메모리의 고정적인 위치를 차지하게 하며 drop될 때까지 그 위치를 유지하게 해준다. 러스트에서 기본적으로 모든 타입은 이동이 가능하다. 즉, 어떤 타입의 객체를 by-value방식으로 다른 변수에 move할 수 있다. 객체의 소유권이 한 변수에서 다른 변수로 이동하는 경우, 그 객체는 재배치 될 수 있다. Pin<P> 는 포인터를 감싼 타입이며, 당신은 Pin<Box<T>> 를 마치 Box<T> 처럼 사용할 수 있으며 비슷하게 Pin<&mut T>&mut T 처럼 사용할 수 있다. 하지만 Pin된 데이터 자체를 꺼내오는 경우는 허용되지 않는다. 이는 mem::swap 같은 함수가 Pin된 데이터에 접근하여 객체를 재배치 하려는 시도를 막아준다.

use std::pin::Pin;

struct SomeStruct {
a: i32,
b: i32
}

fn main() {
let mut s1 = SomeStruct { a: 1, b: 2 };
let mut s2 = SomeStruct { a: 1, b: 3 };
let p1 = Pin::new(&mut s1);
let p2 = Pin::new(&mut s2);
swap_pinned_data(p1, p2);
}

fn swap_pinned_data<T>(x: Pin<&mut T>, y: Pin<&mut T>) {
std::mem::swap(&mut *x, &mut *y);
}

위 코드는 Pin된 객체를 재배치 하려는 시도를 한다. 이런 경우 러스트 컴파일러는 다음의 에러 메시지를 출력하며 이러한 시도를 허용하지 않는다.

error[E0596]: cannot borrow data in a dereference of `std::pin::Pin<&mut T>` as mutable
--> src/main.rs:17:20
|
17 | std::mem::swap(&mut *x, &mut *y);
| ^^^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin<&mut T>`

위 코드에서 알 수 있듯이 러스트 컴파일러는 Pin된 타입 T에 대해서 &mut *(&T) 접근을 허용하지 않는다. 즉 T에 대하여 역참조된 값을 변경할 수 있는 빌림을 허용하지 않는 것이다.

러스트는 Unpin이라는 기본 trait을 제공한다. 이 trait은 러스트의 기본 타입들, bool, i32, f64 등에 자동으로 구현된다. 이러한 기본 타입이 설사 Pin되었다 하더라도 Unpin trait이 적용되어 메모리 재배치가 허용된다. 이러한 기본 타입의 객체들은 자신의 메모리 위치를 참조하지 않기 때문에 설사 이동 되더라도 시스템의 무결성을 해치지 않기 때문이다.

다음은 러스트레퍼런스 에 나와 있는 Pin을 사용한 예제이다.

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::ptr::NonNull;

// This is a self-referential struct because the slice field points to the data field.
// We cannot inform the compiler about that with a normal reference,
// as this pattern cannot be described with the usual borrowing rules.
// Instead we use a raw pointer, though one which is known not to be null,
// as we know it's pointing at the string.
struct Unmovable {
data: String,
slice: NonNull<String>,
_pin: PhantomPinned,
}

impl Unmovable {
// To ensure the data doesn't move when the function returns,
// we place it in the heap where it will stay for the lifetime of the object,
// and the only way to access it would be through a pointer to it.
fn new(data: String) -> Pin<Box<Self>> {
let res = Unmovable {
data,
// we only create the pointer once the data is in place
// otherwise it will have already moved before we even started
slice: NonNull::dangling(),
_pin: PhantomPinned,
};
let mut boxed = Box::pin(res);

let slice = NonNull::from(&boxed.data);
// we know this is safe because modifying a field doesn't move the whole struct
unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).slice = slice;
}
boxed
}
}

let unmoved = Unmovable::new("hello".to_string());
// The pointer should point to the correct location,
// so long as the struct hasn't moved.
// Meanwhile, we are free to move the pointer around.
let mut still_unmoved = unmoved;
assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data));

// Since our type doesn't implement Unpin, this will fail to compile:
// let mut new_unmoved = Unmovable::new("world".to_string());
// std::mem::swap(&mut *still_unmoved, &mut *new_unmoved);

Intrusive doubly-linked list

intrusive doubly-linked list 를 다루면서 Pin과 관련된 남은 관심사를 살펴보자.

intrusive linked list란 무엇인지 부터 보자.

일반적인 linked list의 모습은 다음과 같다.

pin_2
pin_2

이 방식은 값비싼 힙 메모리 할당을 두번이나 요구한다. 또한 노드의 생성, 추가, 삭제가 더 복잡해진다.

보다 효율적인 intrusive linked list는 data를 포인터로 갖고 실제 data는 별도로 저장하지 않고 리스트 엘리먼트 내에 내재된 것을 말한다.

pin_3
pin_3

그림에서 보듯이 data를 위한 메모리 공간이 별도로 할당되지 않고 노드의 데이터와 링크 포인터가 한 공간에 같이 존재한다. intrusive doubly-linked list에서 next와 prev포인터는 pin된 노드를 가리켜야 한다. 만약 노드의 위치가 이동한다면, 그 노드의 앞, 뒤 노드의 링크 포인터는 무의미한 위치를 가리키게 될 것이다.

pin_4
pin_4

위 그림에서 중간의 노드의 위치가 재 배치된 경우 포인터 2개가 무의미한 값을 갖게 되는 상황을 볼 수 있다. 노드의 재배치를 pin을 사용해서 막는다 해도 문제는 여전히 존재한다. 러스트에서 객체의 해제는 Drop 에 의존하는데, 만약 리스트의 어떤 노드가 해제된 경우 단순히 그 노드를 해제하는 것으로 작동이 끝난다면, 링크드 리스트는 연결이 끊어질 것이다. 따라서 pinning은 반드시 drop보장성과 함께 논의 되어야 한다.

Drop 보장성

Pin된 데이터 영역은 drop되기 전까지 재사용되거나, 이동하지 않도록 러스트 시스템 레벨에서 보호된다. 메모리의 무효화는 메모리 해제 뿐만 아니라 Some(v)를 None으로 대체하거나, 벡터로부터 한 엘리먼트를 빼내는 동작으로도 발생한다. 데이터는 ptr::write를 사용함으로써 데이터를 해제하지 않고도 재사용될 수 있다. Drop 보장성이란 앞에서 말한 링크드 리스트에서 노드의 해제가 리스트를 여전히 잘 동작하도록 만들도록 보장하는 것을 의미한다. 또한 이러한 보장성이 메모리 누수를 막는 것을 의미하는 것은 아니라는 것을 유념하라. 메모리 누수는 또 다른 주의사항이며 drop과 관계 없다. 위의 링크드 리스트에서 노드를 안전하게 drop하려면 결국 사용자가 손수 Drop을 구현해야 한다.

Pin된 자료구조를 사용할 때에는 Drop에 상당히 주의해야 한다. drop함수는 &mut self를 받으며 pin된 데이터의 경우도 마찬가지이다. 다시 말해 pin이 된 데이터인 경우 데이터를 이동하지 않는 책임을 사용자에게 넘기는 unsafe한 코드가 호출되어 &mut self가 추출된다. Drop의 구현 예는 다음과 같다.

impl Drop for Type {
fn drop(&mut self) {
// `new_unchecked` is okay because we know this value is never used
// again after being dropped.
inner_drop(unsafe { Pin::new_unchecked(self)});
fn inner_drop(this: Pin<&mut Type>) {
// Actual drop code goes here.
}
}
}

여기서 new_unchecked함수는 unsafe 로서, 사용자가 그 데이터의 재배치나 무효화를 하지 않을 책임을 진다. 이 함수는 어떤 객체에 대하여 Pin된 객체의 포인터를 반환한다. 이러한 단계를 거치는 이유는 우리가 Drop trait의 drop함수의 시그니쳐를 따라야 하기 때문이다. 이 함수가 안전한 이유는 pin된 포인터를 생성하고나서 바로 그 객체를 해제하기 때문에 재배치하거나 무효화 시키지 않는다는 것이 보장되기 때문이다. 만약 #[repr(packed)]이 사용되었다면 컴파일러는 자동으로 배치를 조정하는 코드를 삽입한다. 따라서 #[repr(packed)]는 pin과 함께 사용될 수 없다. Pin돤 구조체를 사용할 때 즉 Pin<&mut Struct>를 가지고 있을 때 어떤 방법으로 그 구조체의 필드에 접근해야 하는지 살펴보자. 가장 일반적 방법으로는 projection이라고 불리우는 방식으로서, helper함수를 작성하는데, 그 함수는 Pin<&mut Struct>를 받아서 특정 필드의 레퍼런스를 반환한다. 여기서 그 레퍼런스의 타입은 Pin<&mut Field>(Pin<&Field> 인 경우도 포함) 또는 &mut Field(&Field인 경우도 포함)가 될 수 있다. 어느 타입을 반환할 것인지는 사용자가 결정하기에 달렸는데, 여기에는 몇 가지 제약이 있고, 가장 중요한 제약은 일관성이다. 모든 필드는 pin되거나 pin이 제거된 레퍼런스로 projection될 수 있는데, 어떤 필드는 pin이고 다른 필드는 pin이 제거괸 형태로 코드를 작성하는 것은 매우 좋지 않다. 자료구조의 설계자로서 사용자는 pin을 전파하도록 할 것인지 결정할 수 있고 이렇게 전파되는 경우를 "structural"하다고 말한다. 왜냐하면 이 자료구조의 projection은 원 자료구조의 구조를 그대로 따르기 때문이다.

Pinning이 “structural”하지 않은 경우

Pin된 구조체의 필드가 pin되지 않은 경우는 사실 가장 쉬운 선택이기도 한데, 이 경우 또 다른 Pin<&mut Field>를 생성하지 않는한 문제가 될 일은 없다. 즉 재배치를 허용하면서, 재배치를 금지하는 행동을 하지 않으면 된다는 것이다. 따라서 사용자가 pin되지 않은 projection을 할 경우 주의해야할 점은 딱 한가지로써, Pin<&mut Fiend>를 만들지 않는 것이다. 이러한 projection의 예를 아래에 보인다.

impl Struct {
fn pin_get_field(self: Pin<&mut Self>) -> &mut Field {
// This is okay because `field` is never considered pinned.
unsafe { &mut self.get_unchecked_mut().field }
}
}

사용자는 해당 Field가 Unpin이 아니더라도 impl Unpin for Struct를 작성하는 것이 허용된다. 그 이유는 Pin<&mut Fiend>가 절대로 만들어 지지 않을 것이기 때문에 Unpin이 아닌 Field를 포함하는 Struct의 Unpin을 구현해도 안전하기 때문이다.

Pinning이 “structural”인 경우

이 경우는 Struct가 재배치 불가이면서 그 구조체 안의 필드도 재배치 금지인 경우이다. 즉 이 projection은 Pin<&mut Field>를 만든다. 이러한 코드의 예가 아래에 있다.

impl Struct {
fn pin_get_field(self: Pin<&mut Self>) -> Pin<&mut Field> {
// This is okay because `field` is pinned when `self` is.
unsafe { self.map_unchecked_mut(|s| &mut s.field) }
}
}

하지만, 이 경우는 추가적인 몇가지 요구사항이 발생한다.

  1. 모든 structural 필드가 Unpin이어야만 구조체가 자동으로 Unpin이 되고, 이 경우만 허용된다. 즉 자동으로 Unpin이 되지 않는 구조체를 impl<T> Unpin for Struct<T>같은 구현을 통해서 수동으로 Unpin으로 만들어서는 안된다. 여기서 Unpin은 안전하다는 것을 염두에 두어야 한다. projection은 unsafe 코드를 이용하지만 Unpin은 안전하므로 pinning하면서 unsafe를 사용하면서 unpin의 안전성 까지 염려할 필요는 없다.
  2. 구조체의 소멸자는 structural 필드를 옮기는 동작을 해서는 안된다. drop은 &mut self를 인자로 넘겨 받으므로 언제든 이 인자의 재배치 연산이 발생할 위험은 열려 있다. 그런데 구조체와 필드는 pin되어 있는 상태이므로 drop함수에서 이러한 재배치 연산이 발생하면 시스템 불일치가 발생하는 것이다. #[repr(packed)] 또한 데이터의 자동 재배치를 일으키므로 pin과 함께 사용되어서는 안된다.
  3. 구조체가 pin되엇으면 그 구조체는 Drop보장성이 있어야 한다. 다시 말해서, pin된 구조체의 내용은 drop되기 전까지 해제되거나 덮어 씌여지거나 하면 안된다. 이 의미는 약간 트릭키한데 VecDeque<T>의 경우에서 보면 모든 엘리먼트를 해제하는 과정에서 하나라도 실패하면 나머지 엘리먼트를 해제하는데 실패하기 때문이다. 이렇게 되면 Drop보장성이 훼손되는데, VecDeque에 대하여 실패한 drop 호출은 하나 이상의 엘리멘트에 대해서 drop을 호출하지 않고 엘리먼트를 해제할 수 있기 때문이다.
  4. 사용자는 pin된 구조체의 내용을 밖으로 꺼내오는 어떠한 기능도 제공해서는 안된다. 이는 구조체 내용을 이동 시키는 결과를 가져올 수 있기 때문이다. 간단한 예를 든다면 어떤 구조체가 Option 값을 갖는 필드를 가지고 있고 fn(Pin<&mut Struct<T>>) -> Option<T>와 같은 함수를 정의한다면 이 함수는 값을 꺼내 올 수 있다. 보다 북잡한 예를 든다면 아래 코드와 같다.
fn exploit_ref_cell<T>(rc: Pin<&mut RefCell<T>>) {
{ let p = rc.as_mut().get_pin_mut(); } // Here we get pinned access to the `T`.
let rc_shr: &RefCell<T> = rc.into_ref().get_ref();
let b = rc_shr.borrow_mut();
let content = &mut *b; // And here we have `&mut T` to the same data.
}

위 함수는 pin 된 데이터를 move하는 코드이고, 이 코드를 실행 하는 것은 재앙일 것이다.

--

--