- std::pin::Pin은 포인터가 가리키는 값이 그 포인터를 통해 이동되지 않는다는 타입 수준 보장을 표현하며, 자기 자신 내부를 참조하는 타입처럼 주소가 안정적이어야 하는 값 때문에 필요함
- async/await에서는 .await를 넘어 살아남는 지역 변수와 참조가 컴파일러 생성 상태 머신의 필드가 될 수 있어, 폴링 이후 future 이동을 막기 위해 Future::poll이 Pin<&mut Self>를 요구함
- Pin<P>는 고정된 값을 안전한 코드로 이동하는 일을 막지만 일반적인 변경까지 금지하지는 않으며, T: Unpin이 아니면 안전하게 Pin<&mut T>에서 &mut T를 꺼낼 수 없음
- Rust 타입 대부분은 기본적으로 Unpin이므로, 이동되면 안 되는 자기 참조 구조체는 보통 PhantomPinned 필드를 넣어 !Unpin으로 만들어야 함
- 실제로는 future를 직접 poll하거나 pinned future를 요구하는 API에 넘길 때 Box::pin 또는 std::pin::pin!을 쓰며, 직접 Future나 저수준 async 원시 타입을 구현할 때는 unsafe 불변식까지 다뤄야 함
Pin이 필요한 이유
- std::pin::Pin은 포인터 래퍼로, 포인터가 가리키는 값이 그 포인터를 통해 이동되지 않는다는 보장을 나타냄
- 핵심 문제는 자기 참조 타입에서 생김
- 예시 구조체 SelfRef는 data: i32와 ptr: *const i32를 가지며, ptr은 self.data를 가리킴
- 구조체 인스턴스를 다른 변수로 이동하거나 함수에서 반환하면 메모리 주소가 바뀔 수 있음
- 원시 포인터 ptr은 이전 메모리 위치를 계속 가리켜 댕글링 포인터가 됨
- 자기 참조가 설정된 뒤에는 해당 값이 다시 이동되지 않도록 막는 장치가 필요함
async/await와 Future에서 생기는 문제
- async/await와 Future는 Pin이 자주 등장하는 대표적인 영역임
- .await 지점을 넘어 살아남는 지역 변수는 컴파일러가 생성하는 상태 머신의 필드가 됨
- 어떤 지역 변수에 대한 참조도 같은 .await를 넘어 살아남으면, 생성된 future가 자기 참조적일 수 있음
- 폴링이 시작된 뒤 future는 자기 내부의 다른 필드를 가리키는 참조에 의존할 수 있음
- 이 상태에서 future가 이동되면 해당 참조가 무효화됨
- 이를 막기 위해 Future::poll은 &mut self 대신 Pin<&mut Self>를 받음
- 호출자는 poll을 부른 뒤 future가 이동되지 않는다는 보장을 제공해야 함
Pin이 막는 것과 허용하는 것
-
Pin<P>는 고정된 포인터를 통해 가리키는 값을 안전한 코드로 이동하지 못하게 막음
-
값의 일반적인 변경은 허용됨
- 고정된 타입의 메서드는 필드를 변경할 수 있음
- 다만 pinning에 의존하는 필드를 값 밖으로 이동시키면 안 됨
-
&mut T의 한계
- &mut T가 있으면 mem::replace, mem::swap, 대입 같은 연산으로 해당 메모리 위치의 값을 재배치할 수 있음
- Pin은 일반적인 가변 참조를 되찾는 일을 제한함
- T: Unpin이 아니면 안전한 코드로 Pin<&mut T>에서 &mut T를 꺼낼 수 없음
- 타입이 Unpin을 구현하지 않는 !Unpin이면, 안전한 코드만으로는 &mut T를 얻을 수 없음
- 이 경우 Pin::get_unchecked_mut 같은 unsafe 메서드를 써야 하며, 값이 그 참조 밖으로 이동되지 않는다는 약속을 코드가 지켜야 함
Unpin과 PhantomPinned
- Unpin을 구현하는 타입은 메모리 안전성을 위해 pinning에 의존하지 않음
- Rust의 대부분 타입은 이동되어도 문제가 없어 기본적으로 Unpin임
- 예: i32, String, Vec
- Unpin은 명시적으로 !Unpin을 만들지 않는 한 모든 타입에 자동 구현됨
- std::marker::PhantomPinned는 명시적으로 !Unpin 인 마커 구조체임
- auto trait은 자동 전파되므로, PhantomPinned 필드를 포함한 구조체도 자동으로 !Unpin이 됨
- 사용자 정의 구조체가 고정된 뒤 이동되면 안전하지 않다는 점을 선언하는 표준 방식임
- 컴파일러는 보통 unsafe 원시 포인터로 만들어지는 자기 참조를 자동 감지할 수 없음
- 따라서 개발자가 자기 참조 구조체에 대해 명시적으로 Unpin을 포기해야 함
- 보통 PhantomPinned 필드를 포함하는 방식으로 처리함
- 자기 참조 타입이 실수로 Unpin 상태로 남아 있으면, 안전한 코드가 Pin에서 가변 참조를 꺼내 값을 이동할 수 있음
- 그러면 자기 참조를 만든 unsafe 코드의 가정이 깨짐
Pin을 만드는 방법
-
Pin 자체가 값을 고정하는 것은 아님
-
Pin을 만든다는 것은 해당 pointee가 pin의 수명 동안 안정적인 메모리 위치에 남는다는 점을 증명하는 일임
-
Pin::new
- 가장 단순한 생성 방식은 Pin::new임
- 이 생성자는 T: Unpin일 때만 사용할 수 있음
- Unpin 타입은 pinning에 의존하지 않으므로, Pin으로 감싸도 항상 안전함
- 이 경우 pinning 보장은 사실상 no-op임
-
std::pin::pin!
- 힙 할당 없이 지역적으로 값을 pin해야 할 때 pin! 매크로를 사용할 수 있음
- 이 매크로는 지역 변수를 만들고, 그 변수를 가리키는 Pin<&mut T>를 반환함
- 컴파일러가 해당 지역 변수를 남은 수명 동안 이동되지 않게 보장하므로, 스택에서 !Unpin 값을 안전하게 pin할 수 있음
- 이름과 달리 pin!은 스택 메모리 자체를 pin하지 않음
- 지역 변수에 묶인 고정 참조를 만들 뿐이며, 변수가 스코프를 벗어나면 pinning 보장도 끝남
-
Box::pin
- !Unpin 타입에서 가장 흔한 생성자는 Box::pin임
- pin!은 지역 변수에 묶인 Pin<&mut T>를 만들지만, Box::pin은 Box가 소유하는 Pin<Box<T>>를 반환함
- 힙 할당 자체는 이동하지 않으므로 pointee는 Box의 수명 동안 안정적인 메모리 위치를 가짐
- Box 자체를 이동해도 소유한 값은 이동하지 않고, Box 안의 포인터만 이동됨
- 힙 할당은 같은 주소에 남음
-
Pin::new_unchecked
- 안전한 생성자가 값이 제자리에 남는다는 점을 증명할 수 없을 때는 unsafe 코드로 Pin을 직접 만들 수 있음
- Pin::new_unchecked 호출자는 반환된 Pin의 수명 동안 pointee가 어떤 포인터를 통해서도 다시 이동되지 않는다고 약속함
- 이 약속이 깨지면 pinning 보장에 의존하는 코드에서 정의되지 않은 동작이 발생할 수 있음
- 따라서 보통 이 불변식을 지킬 수 있는 저수준 추상화를 구현할 때만 사용됨
실제로 신경 써야 하는 경우
- 대부분의 Rust 개발자에게 Pin과 Unpin은 배경에서 조용히 동작함
- 직접 신경 써야 하는 경우는 주로 두 가지임
- async 코드 소비: future를 직접 poll하거나 pinned future를 요구하는 API에 전달해야 하면 Box::pin(future)로 힙에 pin하거나 std::pin::pin!(future)로 로컬 스택에 pin함
- Future 직접 구현: 사용자 정의 상태 머신이나 저수준 async 원시 타입을 작성할 때 Pin<&mut Self>를 다뤄야 하며, pinning 불변식을 지키기 위해 PhantomPinned와 unsafe 코드가 필요할 수 있음
- Pin은 주소 민감 타입 문제를 다루는 Rust의 zero-cost 해법임
- 이를 통해 Rust는 garbage collector 없이 메모리 안전성 보장을 유지하면서 async/await와 다른 자기 참조 추상화를 사용할 수 있음

4 hours ago
2








English (US) ·