러스토노미콘

경고: 이 책은 미완성입니다. 모든 것을 문서화하고, 예전에는 맞았지만 지금은 아닌 내용들을 고치는 데는 시간이 걸립니다. 이슈 트래커에 없는 기능이나, 예전에는 맞았지만 지금은 아닌 내용들을 보실 수 있고, 아직 보고되지 않은 실수나 생각들이 있다면 거기에 얼마든지 새로운 이슈를 열어 주세요.

불안전 러스트의 흑마법들

이 지식은 "있는 그대로" 제공되며, 표현할 수 없는 공포스러운 것들을 해방시켜 당신의 정신을 산산조각내고, 당신의 마음을 알 수 없는 무한한 우주에 떠다니게 하는 것, 혹은 그 이상의 범위를 포함한 사항에 있어서, 명시적 혹은 묵시적인 어떠한 보증도 하지 않는다.

러스토노미콘은 불안전한 러스트 프로그램을 작성할 때 알아야 하는 모든 무시무시한 하나하나를 다 파헤칩니다.

만일 당신이 러스트 프로그램을 작성하는 데 있어서 길고 행복한 나날을 바란다면, 당장 뒤돌아서서 이 책을 봤다는 것도 잊어버리세요. 이것이 필수적인 것은 아닙니다. 그러나 당신이 불안전한 코드를 쓰려고 하거나 - 아니면 그냥 언어의 속을 파 보고 싶다면 - 이 책은 유용한 정보를 많이 담고 있습니다.

러스트 프로그래밍 언어 와 다르게, 상당한 양의 사전 지식을 갖추고 있다고 가정하겠습니다. 특별히, 당신은 기본적인 시스템 프로그래밍과 러스트에 익숙해야 합니다. 이 주제들이 익숙하지 않다면, 기본 책 을 먼저 읽으셔야 할 겁니다. 그렇긴 하지만, 그 책을 읽었다고 가정하진 않을 것이고, 필요하다고 여기는 부분에서는 때때로 기본을 다시 다질 것입니다. 원하시면 바로 이 책으로 건너뛰어도 됩니다 - 모든 것을 처음부터 설명하지는 않을 거라는 것만 알아 두세요.

이 책은 주로 높은 수준에서 러스트 언어 참조서(영문) 와 함께 가는 용도로 존재합니다. 참조서가 언어의 모든 부분의 문법과 의미를 자세하게 알기 위해 존재한다면, 러스토노미콘은 이런 부분들을 어떻게 짜맞추어 쓰느냐, 그리고 그러는 동안 부딪힐 난관들을 조명하기 위해 존재합니다.

참조서는 레퍼런스, 소멸자, 그리고 되감기에 대한 문법과 의미를 말해 주겠지만, 그들을 결합하는 것이 어떻게 예외 내구성 관련 문제를 가져다 줄 수 있는지, 혹은 그 문제들을 어떻게 해결해야 하는지를 말해 주지는 않을 겁니다.

알아두실 필요가 있는 것은 러스토노미콘과 참조서를 잘 동기화하진 않아서, 중복된 내용을 발견하실 수도 있다는 점입니다. 보통 두 문서가 내용이 일치하지 않는다면, 참조서가 맞다고 보는 것이 좋습니다 (참조서가 표준은 아닙니다. 그냥 더 잘 관리될 뿐입니다).

이 책의 범위 안에 있는 주제들은 다음과 같습니다:

  • (불)안전의 의미
  • 언어와 표준 라이브러리에서 재공되는 불안전한 기본 연산들
  • 그 불안전한 기본 연산들로 안전한 추상화를 만드는 기법들
  • 부분타입과 변성
  • 예외 내구성 (패닉/되감기 내구성)
  • 초기화되지 않은 메모리를 가지고 작업하기
  • type punning
  • 병렬성
  • 다른 언어와 상호작용하기 (FFI)
  • 최적화 기법
  • 구조들이 어떻게 컴파일러/OS/하드웨어 기본 연산들로 변환되는지
  • 메모리 모델 사람들을 화나지 않게 하는 법
  • 메모리 모델 사람들을 화나게 하는 법
  • 기타 등등

러스토노미콘은 표준 라이브러리의 모든 API 하나하나의 의미와 보장되는 사항을 일일이 설명하는 곳이 아니고, 러스트의 모든 기능을 하나하나 설명하는 곳도 아닙니다.

다른 말이 없으면, 이 책의 러스트 코드는 러스트 2021 에디션을 사용합니다.

안전함과 불안전함을 마주하라

safe and unsafe

낮은 레벨의 구현 세부사항에 대해 걱정하지 않아도 되면 참 좋을 것입니다. 빈 튜플이 얼마만큼의 공간을 차지하는지 대체 누가 신경쓸까요? 슬프게도 이런 것들은 어떤 때에는 중요하고, 우리는 이런 것들을 걱정해야 합니다. 개발자들이 구현 세부사항에 대해서 걱정하기 시작하는 가장 흔한 이유는 성능이지만 그보다 더 중요한 것은, 하드웨어, 운영체제, 혹은 다른 언어들과 직접적으로 상호작용할 때 이런 세부적인 것들이 올바른 코드를 작성하는 것에 대한 문제가 될 수 있습니다.

안전한 프로그래밍 언어에서 구현 세부사항이 중요해지기 시작할 때, 프로그래머들은 보통 3가지 선택지가 있습니다:

  • 컴파일러나 런타임이 최적화를 수행하도록 코드에 꼼수 쓰기
  • 원하는 구현을 얻기 위해 훨씬 부자연스럽거나 거추장스러운 디자인을 채용하기
  • 구현을 그런 세부사항을 다룰 수 있는 언어로 다시 작성하기

마지막 선택지에서, 프로그래머들이 주로 쓰는 언어는 C 입니다. 이 언어는 C 인터페이스만 정의하는 시스템들과 상호작용하기 위해 보통 필요합니다.

불행하게도 C는 (때때로 좋은 이유로) 사용하기에 엄청나게 불안전하고, 다른 언어들과 협업하려고 할 때 이 불안전함은 증폭됩니다. C와 다른 언어가 어떤 일이 일어날지 서로 동의하고, 서로의 발을 밟지 않게 하기 위해 주의가 필요합니다.

그래서 이것이 러스트와 무슨 상관일까요?

그게, C와 다르게, 러스트는 안전한 프로그래밍 언어입니다.

하지만, C와 마찬가지로, 러스트는 불안전한 언어입니다.

더 정확하게 말하자면, 러스트는 안전한 언어와 불안전한 언어 두 가지를 모두 포함하고 있습니다.

러스트는 안전한 러스트불안전한 러스트 라는 두 개의 프로그래밍 언어의 조합이라고 볼 수 있습니다. 편리하게도, 이 이름들은 말 그대로입니다: 안전한 러스트는 안전하고, 불안전한 러스트는, 음, 그렇지 않죠. 사실 불안전한 러스트는 매우 불안전한 것들을 할 수 있게 해 줍니다. 러스트 개발자들이 하지 말라고 간곡히 부탁하는 것들이지만, 우리는 그냥 무시하고 할 겁니다.

안전한 러스트는 진정한 러스트 프로그래밍 언어입니다. 만약 안전한 러스트만 작성한다면, 타입 안정성이나 메모리 안정성은 절대 걱정할 필요가 없을 겁니다. 달랑거리는 포인터나, 해제 후 사용, 혹은 다른 종류의 미정의 동작은 겪지 않을 테니까요.

표준 라이브러리도 또한 당신에게 바로 쓸 수 있는 도구들을 풍부하게 주어서 당신이 순수하고 자연스러운, 안전한 러스트로 고성능의 응용 프로그램을 만들 수 있게 해 줍니다.

하지만 혹시 당신이 다른 언어에게 말을 걸고 싶을 수도 있습니다. 표준 라이브러리가 노출하지 않은 저수준의 추상화를 작성하고 있을 수도 있죠. 당신이 (순수하게 러스트로 만들어진) 표준 라이브러리를 작성하고 있을 수도 있습니다. 타입 시스템이 이해하지 못하는 것을 해야 할 수도 있고 그냥 좀 비트들에 장난질을 하고 싶을 수도 있습니다. 당신은 불안전한 러스트가 필요할지도 모릅니다.

불안전한 러스트는 안전한 러스트와 아주 흡사합니다. 안전한 러스트의 모든 규칙들과 의미들을 담고 있죠. 다만 추가적으로 절대로 안전하지 않은 것들을 할 수 있게 해 줍니다 (이에 대해서는 다음 섹션에서 다루겠습니다).

이런 구분을 통해 우리는 C와 같은 불안전한 언어의 이점 - 구현 세부사항에 대한 저수준 제어 - 을 얻으면서 완전히 다른 안전한 언어에 통합하려고 할 때 생기는 문제들 대부분을 피할 수 있게 됩니다.

몇몇 문제들이 아직 있긴 합니다 - 대표적으로 타입 시스템이 가정하는 속성들에 대해서 인식하고, 불안전한 러스트와 상호작용하는 어떤 코드에도 그 속성들을 검증해야 합니다. 그게 바로 이 책의 목적입니다: 이런 가정들에 대해서, 또 이것들을 어떻게 관리해야 하는지 가르쳐주는 것이죠.

안전함과 불안전함은 어떻게 상호작용하는가

안전한 러스트와 불안전한 러스트는 어떤 관계일까요? 둘은 어떻게 상호작용할까요?

안전한 러스트와 불안전한 러스트 간의 구분은 unsafe 라는 키워드로 제어되는데, 이것은 서로에게 인터페이스 역할을 합니다. 이것이 바로 안전한 러스트는 안전한 언어라고 할 수 있는 이유입니다: 모든 불안전한 부분은 unsafe 라는 경계 뒤로 밀리거든요. 원한다면 #![forbid(unsafe_code)] 를 코드베이스에 집어넣음으로써 오직 안전한 러스트만 쓴다는 것을 컴파일할 때 보장할 수도 있죠.

unsafe 키워드는 두 가지 용도가 있습니다: 컴파일러가 확인할 수 없는 계약의 존재를 정의할 때 사용하고, 또한 이 계약들이 성립한다는 것을 프로그래머가 확인했다고 선언할 때 사용합니다.

함수들트레잇 정의들 에서 확인되지 않은 계약들의 존재를 알리기 위해 unsafe 키워드를 쓸 수 있습니다. 함수에서 unsafe 는 함수의 사용자들이 함수의 문서를 확인해서, 함수가 요구하는 계약을 지키는 방식으로 사용해야 한다는 것을 의미합니다. 트레잇 정의에서 unsafe 는 트레잇의 구현자들이 트레잇 문서를 확인해서 그들의 구현이 트레잇이 요구하는 계약을 지키는 것을 확실히 해야 한다는 것을 뜻합니다.

코드 블럭에도 unsafe 를 사용해서 그 안에서 이루어진 모든 불안전한 작업들이 그 작업들의 계약들을 지켰다는 것을 확인했다고 선언할 수 있습니다. 예를 들어, slice::get_unchecked 에 넘겨진 인덱스는 범위 안에 있어야 합니다.

트레잇 구현에 unsafe 를 사용해서 그 구현이 트레잇의 계약을 지킨다고 선언할 수 있습니다. 예를 들어, Send 를 구현하는 타입은 정말로 다른 스레드로 안전하게 이동할 수 있어야 합니다.

표준 라이브러리는 다음을 포함한 다수의 불안전한 함수들을 가지고 있습니다:

  • slice::get_unchecked 는 범위를 확인하지 않고 인덱싱을 하기 때문에 메모리 안정성이 자유롭게 침해되도록 허용합니다.
  • mem::transmute 는 어떤 값을 주어진 타입으로 재해석하여 임의의 방식으로 타입 안정성을 건너뜁니다 (자세한 사항은 변환 을 참고하세요).
  • 사이즈가 정해진 타입의 모든 생(raw)포인터는 offset 메서드가 있는데, 이 메서드는 전달된 편차(offset)가 "범위 안에 있지" 않을 경우 미정의 동작을 일으킵니다.
  • 모든 외부 함수 인터페이스 (FFI) 함수들은 호출하기에 불안전 합니다. 이는 다른 언어들이 러스트 컴파일러가 확인할 수 없는 임의의 연산들을 할 수 있기 때문입니다.

러스트 1.48.0 버전에서 표준 라이브러리는 다음의 불안전한 트레잇들을 정의하고 있습니다 (다른 것들도 있지만 아직 안정화되지 않았고, 어떤 것들은 나중에도 안정화되지 않을 것입니다):

  • Send 는 이를 구현하는 타입들이 다른 스레드로 이동해도 안전함을 약속하는 표시 트레잇(API가 없는 트레잇)입니다.
  • Sync 는 또다른 표시 트레잇으로, 이를 구현하는 타입들을 불변 레퍼런스를 이용해 스레드들이 서로 공유할 수 있음을 약속합니다.
  • GlobalAlloc 은 프로그램 전체의 메모리 할당자를 커스터마이징할 수 있게 해 줍니다.
  • SliceIndex 는 슬라이스 타입들의 인덱싱을 위한 동작을 정의합니다. 여기에는 경계를 확인하지 않고 인덱싱하는 작업이 포함됩니다.

러스트 표준 라이브러리도 내부적으로 불안전한 러스트를 꽤 많이 씁니다. 이 구현사항들은 수동으로 엄격하게 확인되어서, 이 위에 안전한 러스트로 지은 인터페이스들은 안전하다고 생각해도 됩니다.

이런 구분의 필요성은 건전성 이라고 불리는, 안전한 러스트의 근본적인 특성으로 귀결됩니다:

무슨 일을 하던, 안전한 러스트는 미정의 동작을 유발할 수 없습니다.

안전/불안전으로 구분하는 디자인은 안전한 러스트와 불안전한 러스트 사이에 비대칭적 신뢰 관계가 있다는 것을 의미합니다. 안전한 러스트는 본질적으로 모든 불안전한 러스트 코드가 올바르게 작성되었다고 믿어야 합니다. 반면 불안전한 러스트는 부주의하게 작성한 안전한 러스트 코드를 믿을 수 없습니다.

예를 들어, 러스트는 "그냥" 비교할 수 있는 타입과 "완전한" 순서를 가지고 있는 (즉 비교가 합리적으로 이루어지는) 타입을 구분하기 위해 PartialOrdOrd 트레잇을 가지고 있습니다.

BTreeMap 은 불완전한 순서를 가지는 타입들에 쓰는 것은 말이 안 되기 때문에 키 역할을 하는 타입이 Ord 를 구현하도록 요구합니다. 하지만 BTreeMap 은 구현 내부에 불안전한 러스트 코드가 있습니다. 안전한 러스트 코드이긴 하겠지만, 부주의한 Ord 구현이 미정의 동작을 일으키는 것은 받아들일 수 없기 때문에, BTreeMap 에 있는 불안전한 코드는 완전하게 순서를 이루고 있지 않은 Ord 구현을 견딜 수 있도록 작성되어야 합니다 - 비록 그렇기 때문에 Ord 를 요구한다고 해도요.

불안전한 러스트 코드는 안전한 러스트 코드가 잘 작성되었을 것이라고 마냥 믿을 수가 없습니다. 그래서 말하자면, BTreeMap 은 당신이 완전한 순서를 이루지 않는 값들을 집어넣으면 완전히 예측 불가능하게 행동할 겁니다. 다만 미정의 동작은 절대로 일으키지 않을 겁니다.

이렇게 질문할 수도 있습니다, 만약 BTreeMapOrd 가 안전해서 믿을 수 없다면, 다른 안전한 코드는 어떻게 믿죠? 예를 들어 BTreeMap 은 정수들과 슬라이스 타입들이 올바르게 구현되었을 거라고 가정합니다. 그것들도 안전하잖아요, 그죠?

그 차이는 범위의 차이입니다. BTreeMap 이 정수들과 슬라이스들에 의존할 때, 그건 매우 특정한 구현에 의존하는 것입니다. 이것은 이득을 생각할 때 넘겨 버릴 수 있는, 일정한 부담입니다. 이 경우에서는 비용이 없다고 할 수 있습니다; 만약 정수들과 슬라이스들이 오류가 있다면, 모두가 오류가 있는 거니까요. 게다가 그것들은 BTreeMap 을 관리하는 사람들의 손에 관리되기 때문에, 그 구현들을 지켜보기 쉽죠.

반면에 BTreeMap 의 키 타입은 제네릭입니다. 그 Ord 구현을 믿는 것은 과거, 현재, 미래의 모든 Ord 구현을 믿는 것과 같습니다. 여기서의 비용은 큽니다: 어디서 누군가는 실수를 해서 본인의 Ord 구현을 망치거나, 심지어는 "그냥 되는 것처럼 보여서" 완전한 순서를 가지는 것처럼 거짓말을 할 수도 있습니다. 그런 일이 벌어질 때 BTreeMap 은 대비해야 합니다.

당신에게 전달된 클로저가 올바르게 작동할 거라고 믿는 것에도 동일한 논리가 적용됩니다.

광범위한 제네릭을 신뢰하는 이런 문제는 unsafe 트레잇을 이용하여 해결할 수 있습니다. 이론적으로 BTreeMap 는 키 타입이 Ord 가 아니라 UnsafeOrd 라고 불리는 새로운 트레잇을 구현하도록 요구할 수도 있습니다. 이 트레잇은 이렇게 생겼습니다:

#![allow(unused)]
fn main() {
use std::cmp::Ordering;

unsafe trait UnsafeOrd {
    fn cmp(&self, other: &Self) -> Ordering;
}
}

그러면 타입이 UnsafeOrd 를 구현할 때 unsafe 키워드를 쓰겠지요. 그 말은 그들의 트레잇이 기대하는 계약을 지켰다는 것을 그들이 확인했다는 뜻일 겁니다. 이런 상황에서 BTreeMap 내부의 불안전 러스트는 키 타입의 UnsafeOrd 구현이 맞다고 신뢰하는 것이 정당화됩니다. 만약 그 구현이 틀렸다면 그것은 불안전한 트레잇 구현의 문제이고, 이것은 러스트의 안전성 보장에 부합합니다.

트레잇을 unsafe 로 표시할지는 API 디자인 선택입니다. 안전한 트레잇이 구현하기에는 더 쉽지만, 그 코드에 의존하는 모든 불안전한 코드는 잘못된 동작을 방어해야 합니다. 트레잇을 unsafe 로 표시하면 그 책임이 구현하는 사람에게로 넘어갑니다. 러스트는 전통적으로 트레잇들을 unsafe 로 표시하는 것을 피해 왔는데, 만약 그러면 불안전한 러스트를 널리 퍼지게 하고, 그것은 별로 바람직하지 않기 때문입니다.

SendSync 는 불안전으로 표시되어 있는데, 그것은 스레드 안정성은 불안전한 코드가 방어할 수 없는 근본적인 특성이라 버그가 있는 Ord 구현을 방어할 때처럼 막을 수 없기 때문입니다. 마찬가지로, GlobalAllocator 는 프로그램의 모든 메모리를 관리하고, BoxVec 같은 다른 것들이 그 위에 지어져 있습니다. 만약 이게 이상한 짓을 한다면 (이미 사용중인 메모리를 할당할 때 반환한다던가), 그것을 인지하거나 대비할 수 있는 가능성은 없습니다.

당신이 만든 트레잇들을 unsafe 로 표시할지 여부는 같은 종류의 고민을 해 봐야 합니다. 만약 unsafe 코드가 이 트레잇의 잘못된 구현을 방어할 수 없다고 합리적으로 생각될 때, 이 트레잇을 unsafe 로 표시하는 것은 합리적인 선택입니다.

한편 SendSyncunsafe 트레잇이지만, 동시에 어떤 타입들에게는 자동으로 구현되는데, 이런 구현이 아마도 안전하다고 여겨지는 타입들입니다. SendSend 를 구현하는 타입들로만 이루어진 타입에 자동으로 구현됩니다. SyncSync 를 구현하는 타입들로만 이루어진 타입에 자동으로 구현됩니다. 이런 자동 구현은 이 두 트레잇들을 unsafe 로 만들어서 불안전성이 널리 퍼지는 것을 최소화합니다. 그리고 메모리 할당자를 직접 만드는 사람은 많지 않을 겁니다 (그것을 직접적으로 쓰는 사람도요).

이것이 바로 안전한 러스트와 불안전한 러스트 사이의 균형입니다. 이런 구분은 최대한 자연스럽게 안전한 러스트를 쓸 수 있도록 하되, 불안전한 러스트를 쓸 때는 추가적인 노력과 주의를 요하게 설계되었습니다. 이 책의 나머지 부분은 주로 어떤 종류의 주의가 필요한지와 불안전한 러스트가 지켜야 할 계약들이 어떤 것들인지를 논합니다.

불안전한 러스트는 무엇을 할 수 있는가

불안전한 러스트에서 다른 점은 이런 것들이 가능하다는 것뿐입니다:

  • 생 포인터 역참조하기
  • unsafe 함수 호출하기 (C 함수나, 컴파일러 내부, 그리고 할당자를 직접 호출하는 것 포함)
  • unsafe 트레잇 구현하기
  • 가변 정적 변수를 접근하거나 수정하기
  • union 의 필드를 접근하기

이게 전부입니다. 이런 연산들이 불안전의 영역으로 추방된 이유는, 이것들 중 하나라도 잘못 사용할 경우 그토록 두렵던 미정의 동작을 일으키기 때문입니다. 미정의 동작을 일으키면 컴파일러가 당신의 프로그램에 임의의 나쁜 짓들을 할 수 있는 모든 권리를 얻게 됩니다. 당연하게도 미정의 동작은 일으켜서는 안됩니다.

C와 다르게, 미정의 동작은 러스트에서는 꽤 제한되어 있습니다. 러스트의 코어 언어가 막으려고 하는 것들은 이런 것들입니다:

  • 달랑거리거나 정렬되어 있지 않은 포인터를 역참조하는 것 (* 연산자를 사용해서) (밑 참조)
  • 레퍼런스 규칙 을 어기는 것
  • 잘못된 호출 ABI를 이용해 함수를 호출하거나 잘못된 되감기 ABI를 가지고 있는 함수에서 되감는 것
  • 데이터 경합 을 일으키는 것
  • 지금 실행하는 스레드가 지원하지 않는 타겟 기능들 로 컴파일된 코드를 실행하는 것
  • 잘못된 값을 생산하는 것 (혼자서나 enum/struct/배열/튜플과 같은 복합 타입의 필드로써나):
    • 0도 1도 아닌 bool
    • 유효하지 않은 식별자1를 사용하는 enum
    • fn 포인터
    • [0x0, 0xD7FF] 와 [0xE000, 0x10FFFF] 범위를 벗어나는 char
    • ! 타입의 값 (이 타입의 모든 값은 유효하지 않습니다)
    • 초기화되지 않은 메모리 로부터 읽어들인 정수 (i*/u*), 부동소수점 값 (f*), 혹은 생 포인터, 혹은 str 안의 초기화되지 않은 메모리.
    • 달랑거리거나, 정렬되지 않았거나, 유효하지 않은 값을 가리키는 레퍼런스/Box
    • 잘못된 메타데이터를 가지고 있는 넓은 레퍼런스, Box, 혹은 생 포인터:
      • dyn Trait 메타데이터는 그것이 Trait의 vtable을 가리키는 포인터가 아닐 경우 유효하지 않습니다
      • 슬라이스 메타데이터는 길이가 올바른 usize 가 아니면 유효하지 않습니다 (즉, 초기화되지 않은 메모리에서 읽어들이면 안됩니다)
    • 널인 NonNull 같은, 커스텀으로 잘못된 값이 들어있는 타입 (커스텀으로 잘못된 값을 요청하는 것은 불안정한 기능이지만 NonNull 같은, 몇 가지 안정 버전의 표준 라이브러리 타입들은 이것을 사용합니다.)

"미정의 동작"에 관해 더 자세한 설명이 필요하다면 참조서 를 참고하셔도 됩니다.

값을 "생산하는" 일은 값이 할당되거나, 함수/기본 연산에 전달되거나, 함수/기본 연산에서 반환될 때 일어납니다.

레퍼런스/포인터가 "달랑거린다"는 것은 그것이 널이거나 그것이 가리키는 바이트가 모두 같은 할당처에 있는 것이 아니라는 뜻입니다 (그 바이트들은 모두 어떤 할당처에는 있어야 합니다). 그것이 가리키는 바이트들의 너비는 포인터 값과 참조되는 타입의 크기에 따라 결정됩니다. 따라서 만약 너비가 비어 있다면, "달랑거리는" 것은 "널"인 것과 같습니다. 슬라이스와 문자열은 그들의 전체 범위를 가리킨다는 것을 유의한다면, 길이 메타데이터가 너무 크지 않도록 하는 것이 중요해집니다 (특히, 할당량과 그에 따른 슬라이스와 문자열은 isize::MAX 바이트보다 클 수 없습니다). 만약 어떤 이유로 이것이 거추장스럽다면, 생 포인터를 쓰는 것을 고려해 보세요.

그게 전부입니다. 그것이 러스트에 있는 미정의 동작의 모든 원인입니다. 물론 불안전한 함수들과 트레잇들은 프로그램이 지켜야 하는 임의의 다른 제약들을 걸 수 있고, 그것을 어기면 미정의 동작이 일어나겠죠. 예를 들어, 할당자 API는 할당되지 않은 메모리를 해제하는 것은 미정의 동작이라고 정의합니다.

그러나 이런 제약들을 어기면 결국 위의 문제들 중 하나로 이어지게 될 것입니다. 어떤 추가적인 제약들은 컴파일러 내부가 코드를 최적화하는 과정에서 하는 특별한 가정들에서부터 비롯될 수도 있습니다. 예를 들어, VecBox 는 그들의 포인터가 항상 널이 아니도록 하는 내부 코드를 사용합니다.

러스트는 이 외의 다른 애매한 작업들에는 꽤나 관대합니다. 러스트는 이런 작업들을 "안전하다"고 판단합니다:

  • 데드락 (교착 상태)
  • 경합 조건 이 있는 것
  • 메모리 누수
  • (+ 등의 기본 연산자를 이용한) 정수 오버플로우
  • 프로그램 비정상적 종료
  • 프로덕션 데이터베이스 삭제하기

더 자세한 정보는 참조서 를 참고하세요.

하지만 어떤 프로그램이 이런 것을 한다면 아마도 잘못된 것일 겁니다. 러스트는 이런 것들이 드물게 일어나게 하기 위해 많은 도구들을 제공하지만, 이런 종류의 문제를 아예 막기에는 비현실적이라고 판단됩니다.

1

"식별자(discriminant)"는 열거형의 각각의 형(variant)에 대응하는 정수입니다. 보통 0부터 시작합니다.

불안전함과 함께 일하는 것

러스트는 일반적으로 우리가 불안전한 러스트와 이야기할 때 정해진 범위 안에서, 이진수의 방식으로 이야기하는 도구만 줍니다. 불행하게도 현실은 그것보다 훨씬 복잡하죠. 예를 들어, 다음의 간단한 함수를 생각해 볼까요:

#![allow(unused)]
fn main() {
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
    if idx < arr.len() {
        unsafe {
            Some(*arr.get_unchecked(idx))
        }
    } else {
        None
    }
}
}

이 함수는 안전하고 올바릅니다. 우리는 인덱스가 범위 안에 있는지 확인하고, 그렇다면 더 이상 확인하지 않고 배열을 바로 인덱싱합니다. 이렇게 잘 구현된 불안전한 함수를 건전하다 고 하는데, 이것은 안전한 코드가 이 코드를 악용해서 미정의 동작을 유발할 수 없다는 뜻입니다 (이건 바로 안전한 러스트의 유일한 근본적 특성이었죠).

하지만 이런 흔한 함수 안에서도, 불안전한 코드 블럭의 범위는 모호합니다. 여기서 <<= 로 바꾸는 경우를 생각해 보세요:

#![allow(unused)]
fn main() {
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
    if idx <= arr.len() {
        unsafe {
            Some(*arr.get_unchecked(idx))
        }
    } else {
        None
    }
}
}

이 프로그램은 이제 불건전하고, 안전한 러스트는 미정의 동작을 유발할 수 있지만, 우리는 안전한 코드만 수정했을 뿐입니다. 이것이 바로 안전함의 근본적인 문제입니다: 지역에 한정되어 있지 않죠. 이 불안전한 연산의 건전성은 어쩔 수 없이 다른 "안전한" 연산들이 만든 상태에 의존하게 됩니다.

불안전함을 도입하는 것이 임의의 다른 종류의 유해함을 고려하지 않아도 되는 점에서, 안전성은 경계가 있습니다. 예를 들어, 슬라이스에 확인되지 않은 인덱싱을 하는 것은 갑자기 슬라이스가 널이 되거나 초기화되지 않은 메모리를 포함하게 되는 경우를 걱정해야 하는 것은 아닙니다. 근본적으로 변하는 것은 없습니다. 하지만 프로그램이 본질적으로 상태를 가지게 되는 것과 당신의 불안전한 작업들이 임의의 다른 상태에 의존할 수 있게 된다는 점에서, 안전성은 분리되어 있지는 않습니다.

이런 지역에 한정되지 않는 특성은 우리가 실제로 지속되는 상태를 포함하게 되면 더욱 심해집니다. 다음의 간단한 Vec 구현을 생각해 보세요:

use std::ptr;

// 주의: 이 정의는 순진합니다. Vec을 구현하는 챕터를 참고하세요.
pub struct Vec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

// 이 구현은 무량 타입을 제대로 다루지 못하니 주의하세요.
// Vec을 구현하는 챕터를 보세요.
impl<T> Vec<T> {
    pub fn push(&mut self, elem: T) {
        if self.len == self.cap {
            // 이 예제에는 중요하지 않은 부분입니다
            self.reallocate();
        }
        unsafe {
            ptr::write(self.ptr.add(self.len), elem);
            self.len += 1;
        }
    }
    fn reallocate(&mut self) { }
}

fn main() {}

이 코드는 합리적으로 검사하고 편하게 확인할 수 있을 만큼 간단합니다. 이제 이런 메서드를 추가하는 것을 생각해 보세요:

fn make_room(&mut self) {
    // 용량을 늘립니다
    self.cap += 1;
}

이 코드는 100% 안전한 러스트이지만, 동시에 완전히 불건전합니다. 용량을 변경하는 것은 Vec의 불문율(capVec의 할당된 용량을 반영한다는 것)을 파괴합니다. 이것은 Vec의 나머지 코드가 방어할 수 있는 것이 아닙니다. Veccap 필드를 검증할 방법이 없기 때문에 믿는 수밖에 없습니다.

unsafe 코드는 구조체 필드의 불문율에 의존하기 때문에, 한 함수 전체를 오염시키는 것보다 더한 짓을 합니다: 한 모듈 전체를 오염시키죠. 일반적으로 불안전한 코드의 범위를 제한하는, 오류 없는 유일한 방법은 모듈 경계에서 private를 이용하는 것입니다.

그렇지만 이 코드는 완벽하게 동작합니다. make_room의 존재는, 우리가 그것을 pub 으로 표시하지 않았기 때문에, Vec의 건전성에 문제가 되지 않습니다. 이 함수를 정의한 모듈만 이것을 호출할 수 있기 때문입니다. 또, make_roomVecprivate 필드를 직접적으로 접근하므로, Vec과 같은 모듈에서 작성될 수밖에 없습니다.

그러므로 우리는 복잡한 불문율에 의지하는, 완전히 안전한 추상화를 작성할 수 있게 됩니다. 이것은 안전한 러스트와 불안전한 러스트 사이의 관계에 있어서 필수적입니다.

우리는 이미 불안전한 러스트가 일부의 안전한 코드를 믿어야 하지만, 일반적인 안전한 코드는 믿으면 안된다는 것을 보았습니다. 공개 상태(pub 을 이용한)도 비슷한 이유로 불안전한 코드에 있어서 중요합니다: 우리가 믿고 있는 상태를 망가트리지 않을 거라고 믿으며 온 우주에 있는 모든 안전한 코드를 의지할 필요가 없으니까요.

안전함은 살았습니다!

러스트에서의 데이터 표현

저수준 프로그래밍은 데이터 레이아웃에 많은 신경을 씁니다. 그것은 중요하거든요. 이것은 또한 언어의 전반적인 부분에 영향을 주기 때문에, 우리는 러스트에서 데이터가 어떻게 표현되는지를 파헤쳐 보면서 시작하겠습니다.

이 챕터는 이상적으로는 참조서의 타입 레이아웃 섹션과 동의하는 내용이고, 중복으로 이 책에 표시되었습니다. 이 책이 처음 쓰여질 때 참조서는 완전히 황폐한 상태였고, 러스토노미콘은 참조서에 대한 부분적인 대안으로 제공하려고 시도했습니다. 이제 참조서는 그렇지 않으므로, 이 챕터 전체는 이상적으로는 삭제되어도 될 겁니다.

우리는 이 챕터를 조금 더 놔둘 거지만, 이상적으로는 새로운 사실이나 개선점을 기여하고 싶다면 참조서에 대신 기여해 주세요.

(번역자: 현재 러스트 참조서는 한국어로 번역되지 않은 상태이므로, 한국어 노미콘에 우선 기여해주시면 감사하겠습니다!)

repr(Rust)

첫번째로 그리고 가장 중요하게도, 모든 타입은 바이트로 표시되는 정렬선이 있습니다. 타입의 정렬선은 값을 어떤 주소에 저장하는 게 유효한지를 특정해 줍니다. n의 정렬선을 가지고 있는 값은 n의 배수인 주소에만 저장할 수 있습니다. 따라서 정렬선이 2이면 짝수인 주소에 저장되어야 한다는 뜻이고, 1이라면 어디든지 저장될 수 있다는 뜻입니다. 정렬선은 최소 1이고, 항상 2의 거듭제곱입니다.

기본 타입들은 그들의 크기에 맞춰 정렬됩니다. 플랫폼에 따라 다르긴 하지만요. 예를 들어, x86에서는 u64f64는 보통 4바이트(32비트)마다 정렬됩니다.

타입의 크기는 항상 정렬선의 배수여야 합니다 (0은 어떤 정렬선이든 인정되는 크기입니다). 이것은 그 타입의 배열이 언제나 그 크기의 배수로 인덱싱되는 것을 보장합니다. 동량 타입의 경우에는 타입의 크기와 정렬선이 컴파일할 때 모를 수도 있다는 것을 주의하세요.

러스트는 복잡한 데이터를 다음의 방법으로 가지런하게 놓을 수 있게 해 줍니다:

  • 구조체 (곱 타입이라고 부름)
  • 튜플 (이름 없는 곱 타입)
  • 배열 (동형의 곱 타입)
  • 열거형 (합 타입 또는 태그가 있는 공용체라고 부름)
  • 공용체 (태그 없는 공용체)

열거형은 그 형(形)이 모두 연관된 데이터가 없으면 필드가 없다고 합니다.

기본적으로, 복합적인 자료구조는 그 필드들의 정렬선들 중 최댓값을 정렬선으로 갖습니다. 러스트는 따라서 필요한 곳에 여백을 넣음으로써 모든 필드가 잘 정렬되고, 타입의 총 크기가 그 정렬선의 배수가 되도록 합니다. 예를 들어 다음의 구조체는:

#![allow(unused)]
fn main() {
struct A {
    a: u8,
    b: u32,
    c: u16,
}
}

이런 기본 타입들을 그들의 해당되는 크기로 정렬하는 타겟 플랫폼에서 32비트로 정렬될 것입니다. 따라서 구조체 전체는 32비트의 배수를 크기로 가지게 될 겁니다. 이 구조체는 이렇게 될 수도 있습니다:

#![allow(unused)]
fn main() {
struct A {
    a: u8,
    _pad1: [u8; 3], // `b`를 정렬하기 위해서입니다
    b: u32,
    c: u16,
    _pad2: [u8; 2], // 전체 크기가 4바이트의 배수가 되게 하기 위해서입니다
}
}

아니면 이렇게도요:

#![allow(unused)]
fn main() {
struct A {
    b: u32,
    c: u16,
    a: u8,
    _pad: u8,
}
}

이런 타입들에는 간접적인 조치는 없습니다; 모든 데이터는 C에서 그럴 것 같이, 구조체 안에 저장됩니다. 그러나 배열은 예외인데 (순서대로, 그리고 밀집되어 할당되어 있으니까요), 기본적으로 데이터의 정렬선은 특정되지 않습니다. 다음의 두 구조체 정의를 볼 때:

#![allow(unused)]
fn main() {
struct A {
    a: i32,
    b: u64,
}

struct B {
    a: i32,
    b: u64,
}
}

러스트는 A 타입의 두 값은 그 데이터가 정확히 똑같은 식으로 정렬될 것은 보장합니다. 그러나 러스트는, 현재로써는, A 타입의 값이 B 타입의 값과 같은 필드 순서나 여백을 가질지는 보장하지 않습니다.

AB가 이렇게 적혔으니 학술적인 느낌일 것 같지만, 러스트의 몇 가지 다른 기능들이 데이터 정렬을 여러 가지 복잡한 방법으로 가지고 놀기 좋게 해 줍니다.

예를 들어, 이 구조체를 생각해 보세요:

#![allow(unused)]
fn main() {
struct Foo<T, U> {
    count: u16,
    data1: T,
    data2: U,
}
}

이제 이 구조체의 한 버전인 Foo<u32, u16>Foo<u16, u32>를 생각해 보세요. 만약 러스트가 특정된 순서로 필드를 배치한다면, 그 정렬선 문제를 해결하기 위해 구조체 안에 여백의 값을 집어넣기를 우리는 기대할 것입니다. 따라서 만약 러스트가 필드를 재정렬하지 않았다면, 이런 식으로 출력이 나오겠지요:

struct Foo<u16, u32> {
    count: u16,
    data1: u16,
    data2: u32,
}

struct Foo<u32, u16> {
    count: u16,
    _pad1: u16,
    data1: u32,
    data2: u16,
    _pad2: u16,
}

후자의 경우는 꽤나 공간을 낭비합니다. 공간 사용을 최적화하려면 일반화의 다른 버전마다 다른 필드 순서가 필요하겠군요.

열거형은 이런 고민을 더욱 복잡하게 만듭니다. 순진하게 보자면, 이런 열거형은:

#![allow(unused)]
fn main() {
enum Foo {
    A(u32),
    B(u64),
    C(u8),
}
}

이렇게 배치될 수 있겠습니다:

#![allow(unused)]
fn main() {
struct FooRepr {
    data: u64, // 이것은 `tag`에 따라 u64, u32, 또는 u8 입니다.
    tag: u8,   // 0 = A, 1 = B, 2 = C
}
}

그리고 이것은 정말로 실제와 비슷하게 배치되는 것입니다 (tag의 위치와 크기에 따라).

하지만 어떤 몇 가지 경우에서는 이런 표현은 비효율적입니다. 전통적인 경우는 러스트의 "널 포인터 최적화"입니다: 하나의 ()을 가진 형(예를 들어 None)과 (중첩될 수도 있는) 널이 될 수 없는 포인터를 가진 형(예를 들어 Some(&T))이 있는 열거형은 태그가 필요하지 않습니다. 널 포인터는 안전하게 () 형(None)으로 해석할 수 있거든요. 최종적인 결과는, 예를 들어, 이렇게 됩니다: size_of::<Option<&T>>() == size_of::<&T>()

러스트에는 널이 될 수 없는 타입이나, 이를 포함하는 타입들이 많이 있는데, Box<T>, Vec<T>, String, &T, 그리고 &mut T 같은 것들입니다. 비슷하게, 중첩된 열거형들이 태그를 하나의 식별자로 뭉치는 경우도 생각할 수 있는데, 그것은 그들이 정의에 의해서 유효한 값의 범위가 정해져 있기 때문입니다. 원칙상 열거형은 꽤나 정교한 알고리즘을 써서 중첩된 타입에 있는 비트들을 금지된 값들과 함께 저장할 수 있습니다. 따라서 오늘날 우리는 열거형의 배치 상태를 밝혀지지 않은 상태로 놔두는 것이 특별히 좋습니다.

이량(異量) 타입

거의 항상, 우리는 타입이 정적으로 알려져 있고 양수의 크기를 가지고 있다고 생각합니다. 러스트에서 이것은 항상 그렇지는 않습니다.

동량(動量) 타입 (DST)

러스트는 동량(動量) 타입(DST)을 지원합니다: 정적으로 알려진 크기나 정렬선이 없는 타입을 말이죠. 표면적으로는 이것은 좀 말이 되지 않습니다: 러스트는 무언가와 올바르게 작업하기 위해서는 그것의 크기와 정렬선을 알아야 하거든요! 이런 면에서 DST는 보통의 타입이 아닙니다. 정적으로 알려진 크기가 없기 때문에, 이런 타입들은 포인터 뒤에서만 존재할 수 있습니다. 따라서 DST를 가리키는 포인터는 포인터와 DST를 "완성하는" 정보로 이루어진 넓은 포인터가 됩니다 (밑에서 더 설명합니다).

언어에서 보이는 주요한 DST는 두 가지가 있습니다:

  • 트레잇 객체: dyn MyTrait
  • 슬라이스: [T], str, 등등

트레잇 객체는 그것이 특정하는 트레잇을 구현하는 어떤 타입을 표현합니다. 정확한 원래 타입은 런타임 리플렉션을 위해 지워지고, 타입을 쓰기 위해 필요한 모든 정보를 담고 있는 vtable로 대체됩니다. 트레잇 객체를 완성하는 정보는 이 vtable의 포인터입니다. 포인터가 가리키는 대상의 런타임 크기는 vtable에서 동적으로 요청될 수 있습니다.

슬라이스는 어떤 연속적인 저장소에 대한 뷰일 뿐입니다 -- 보통 이 저장소는 배열이거나 Vec입니다. 슬라이스 포인터를 완성시키는 정보는 가리키고 있는 원소들의 갯수입니다. 가리키는 대상의 런타임 크기는 그냥 한 원소의 정적으로 알려진 크기와 원소들의 갯수를 곱한 것입니다.

구조체는 사실 마지막 필드로써 하나의 동량 타입을 직접 저장할 수 있지만, 그러면 그들 자신도 동량 타입이 됩니다:

#![allow(unused)]
fn main() {
// 직접적으로 스택에 저장할 수 없음
struct MySuperSlice {
    info: u32,
    data: [u8],
}
}

이런 타입은 생성할 방법이 없으면 별로 쓸모가 없지만 말이죠. 현재 유일하게 제대로 지원되는, 커스텀 동량 타입을 만들 방법은 타입을 제네릭으로 만들고 크기 강제 망각을 실행하는 것입니다:

struct MySuperSliceable<T: ?Sized> {
    info: u32,
    data: T,
}

fn main() {
    let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
        info: 17,
        data: [0; 8],
    };

    let dynamic: &MySuperSliceable<[u8]> = &sized;

    // 출력: "17 [0, 0, 0, 0, 0, 0, 0, 0]"
    println!("{} {:?}", dynamic.info, &dynamic.data);
}

(네, 커스텀 동량 타입은 지금으로써는 매우 설익은 기능입니다.)

무량(無量) 타입 (ZST)

러스트는 타입이 공간을 차지하지 않는다고 말하는 것도 허용합니다:

#![allow(unused)]
fn main() {
struct Nothing; // 필드 없음 = 크기 없음

// 모든 필드가 크기 없음 = 크기 없음
struct LotsOfNothing {
    foo: Nothing,
    qux: (),      // 빈 튜플은 크기가 없습니다
    baz: [u8; 0], // 빈 배열은 크기가 없습니다
}
}

무량(無量) 타입(ZST)은, 당연하게도 그 자체로는, 별로 쓸모가 없습니다. 하지만 러스트의 많은 기이한 레이아웃 선택들이 그렇듯이, 그들의 잠재력은 일반적인 환경에서 빛나게 됩니다: 러스트는 무량 타입의 값을 생성하거나 저장하는 모든 작업이 아무 작업도 하지 않는 것과 같을 수 있다는 사실을 매우 이해하거든요. 일단 값을 저장한다는 것부터가 말이 안됩니다 -- 차지하는 공간도 없는걸요. 또 그 타입의 값은 오직 하나이므로, 어떤 값이 읽히든 그냥 무에서 값을 만들어내면 됩니다 -- 이것 또한 차지하는 공간이 없기 때문에, 아무것도 하지 않는 것과 같습니다.

이것의 가장 극단적인 예시 중 하나가 MapSet입니다. Map<Key, Value>가 주어졌을 때, Set<Key>Map<Key, UselessJunk>를 적당히 감싸는 자료구조로 만드는 것은 흔하게 볼 수 있습니다. 많은 언어들에서 이것은 UselessJunk 타입을 위한 공간을 할당하고, UselessJunk를 가지고 아무것도 하지 않기 위해서 그 값을 저장하고 읽는 작업을 강제할 겁니다. 이 작업이 불필요하다는 것을 증명하려면 컴파일러는 복잡한 분석을 해야 할 겁니다.

그러나 러스트에서는 우리는 그냥 Set<Key> = Map<Key, ()>라고 말할 수 있습니다. 이제 러스트는 컴파일할 때 모든 메모리 읽기와 저장은 의미가 없고, 저장 공간을 할당할 필요도 없다는 것을 알게 됩니다. 결과적으로 나오는 코드는 그냥 HashSet의 커스텀 구현일 뿐이고, HashMap이 값을 처리할 때의 연산은 존재하지 않게 됩니다.

안전한 코드는 무량 타입에 대해서 걱정하지 않아도 되지만, 불안전한 코드는 크기가 없는 타입의 중요성을 신경써야 합니다. 특히 포인터 오프셋은 아무 작업도 하지 않는 것과 같고, 할당자는 보통 0이 아닌 크기를 요구합니다.

무량 타입을 가리키는 레퍼런스(빈 슬라이스 포함)는 다른 레퍼런스와 마찬가지로, 널이 아니고 잘 정렬되어 있어야 합니다. 무량 타입을 가리키지만 널이나 정렬되지 않은 포인터를 역참조하는 것 역시, 다른 타입들과 마찬가지로 미정의 동작입니다.

빈 타입

러스트는 또한 그 타입의 값을 만들 수조차 없는 타입을 정의하는 것도 지원합니다. 이런 타입들은 타입 측면에서만 말할 수 있고, 값 측면에서는 절대 말할 수 없습니다. 빈 타입은 형이 없는 열거형을 정의함으로써 만들 수 있습니다:

#![allow(unused)]
fn main() {
enum Void {} // 형 없음 = 비어 있음
}

빈 타입은 무량 타입보다도 더 작습니다. 빈 타입의 예시로 들 만한 것은 타입 측면에서의 접근불가성입니다. 예를 들어, 어떤 API가 일반적으로 Result를 반환해야 하지만, 어떤 경우에서는 실패할 수 없다고 합시다. 우리는 이것을 Result<T, Void>를 반환함으로써 타입 레벨에서 소통할 수 있습니다. API의 사용자들은 이런 Result의 값이 Err가 되기에 정적으로 불가능하다는 것을 알고 자신 있게 unwrap할 수 있을 겁니다, 왜냐하면 Err 값이 있으려면 Void 타입의 값이 생산되어야 하거든요.

원칙적으로는 러스트가 이런 사실에 기반하여 몇 가지 흥미로운 분석과 최적화를 수행할 수 있을 겁니다. 예를 들어 Result<T, Void>Err 형이 실제로 존재하지 않기에, 그냥 T로 표현할 수 있겠죠 (엄격하게 말하면, 이것은 보장되지 않은 최적화일 뿐이고, TResult<T, Void> 중 하나를 다른 하나로 변질시키는 것은 아직 미정의 동작입니다).

다음의 코드도 컴파일 됩니다:

#![allow(unused)]
fn main() {
enum Void {}

let res: Result<u32, Void> = Ok(0);

// Err 형이 존재하지 않으므로, Ok 형은 사실 패턴 매칭이 실패할 수 없습니다.
let Ok(num) = res;
}

빈 타입에 대한 마지막 하나의 조그만 사실은, 빈 타입을 가리키는 생 포인터는 놀랍게도 유효하게 생성할 수 있지만, 그것을 역참조하는 것은 말이 안되기 때문에 미정의 동작이라는 것입니다.

우리는 C의 void* 타입을 *const Void로 설계하는 것을 추천하지 않습니다. 많은 사람들이 이렇게 했지만 얼마 지나지 않아 문제에 부딪혔는데, 러스트는 불안전한 코드로 빈 타입의 값을 만드려고 하는 것을 막는 안전 장치가 없고, 만약 빈 타입의 값을 만들면, 그것은 미정의 동작이기 때문입니다. 이것은 특별히 문제가 되었는데, 개발자들이 생 포인터를 레퍼런스로 바꾸는 습관이 있었고 &Void 값을 만드는 것 역시 미정의 동작이기 때문입니다.

*const () (혹은 비슷한 타입)은 void*에 대응해서 무리 없이 잘 동작하고, 안전성의 문제 없이 레퍼런스로 만들 수 있습니다. 값을 읽고 쓰려고 하는 시도를 막는 것은 여전히 하지 않지만, 최소한 미정의 동작보다는 아무 작업도 하지 않는 것으로 컴파일됩니다.

외래 타입

외래 타입으로 불리는, 알 수 없는 크기의 타입을 추가하여 러스트 개발자들이 C의 void*나 다른 "선언되었지만 정의되지 않은" 타입들을 좀더 정확하게 설계하자는, 승인된 RFC가 있습니다. 하지만 러스트 2018 기준으로, 이 기능은 size_of_val::<MyExternType>()이 어떻게 작동해야 하는지에 걸려서 대기 상태에 갇혀 있습니다.

다른 데이터 표현들

러스트는 기본으로부터 다른 데이터 설계 전략을 구성하게 해 줍니다.

불안전 코드 가이드라인도 있습니다 (비표준이니 주의하세요).

repr(C)

이것은 가장 중요한 repr입니다. 이것은 매우 간단한 의도를 가지고 있습니다: C가 하는대로 하라는 것이죠. 필드들의 정렬 순서, 크기, 정렬선은 C나 C++에서 되는 것 같이 될 겁니다. 어떤 타입이든 FFI 경계를 넘겨 보내려면 repr(C)로 표현되어야 하는데, 이는 C가 프로그래밍 세계의 공용어이기 때문입니다. 이것은 값을 다른 타입으로 재해석하는 것과 같은, 데이터 레이아웃을 가지고 정교한 장난을 수월하게 칠 수 있기 위해서 필수적입니다.

우리는 rust-bindgencbindgen를 둘 다, 혹은 둘 중 하나를 써서 당신 대신 FFI 경계를 관리하기를 매우 권장합니다. 러스트 팀은 이 프로젝트들과 긴밀하게 작업하여 이들이 튼튼하게 작동하고, 타입 레이아웃과 repr들에 대한 현재와 미래의 보장에 잘 맞도록 신경쓰고 있습니다.

repr(C)와 러스트의 (C보다) 이상한 데이터 설계 기능의 상호작용은 주의해야 합니다. "FFI를 위한" 것과 "데이터 표현을 바꾸기" 위한 두 가지 목적이 동시에 있기 때문에, repr(C)는 FFI 경계로 보내면 말이 안되거나 문제가 생길 수 있는 타입들에 적용할 수 있습니다.

  • 무량 타입(ZST)은 그대로 크기가 0으로 되는데, 이것은 C에서 표준 동작이 아니고, C++에서 빈 타입의 동작과 분명하게 반대되는데, C++에서는 빈 타입이라도 한 바이트의 공간을 차지해야 한다고 말하기 때문입니다.

  • 동량 타입(DST)의 포인터(넓은 포인터)와 튜플은 C에서 없는 개념이므로, FFI로 보내면 절대 안전하지 않습니다.

  • 필드가 있는 열거형 또한 C와 C++에서 없는 개념이지만, 타입 사이의 유효한 변환이 정의되어 있습니다.

  • 만약 TFFI로 보내도 안전하고 널이 아닌 포인터 타입이라면, Option<T>T와 같은 데이터 표현과 ABI를 갖추고, 따라서 FFI로 보내도 안전하다는 것이 보장됩니다. 이 글을 쓰는 시점에서, 이것은 &, &mut, 그리고 함수 포인터들에 해당하는데, 이것들은 전부 널이 될 수 없기 때문입니다.

  • 튜플 구조체는 repr(C)에서는 일반 구조체와 같은데, 일반 구조체와 다른 점은 필드의 이름이 없다는 것뿐이기 때문입니다.

  • 필드가 없는 열거형에 있어서는 repr(C)repr(u*) (다음 섹션을 보세요) 중 하나와 같습니다. 여기서 선택되는 바이트 크기는 타겟 플랫폼의 C 애플리케이션 이진 인터페이스(ABI)에서의 기본 열거형 크기입니다. 주의할 점은 C에서의 데이터 표현은 구현에 따라 다르게 정의되어 있으므로, 이것은 "최선의 추측"이라는 점입니다. 특별히, 관련된 C 코드가 특정한 플래그로 컴파일되면 이 설명이 맞지 않을 수도 있습니다.

  • repr(C)repr(u*)로 표현되는 필드 없는 열거형은, C나 C++에서는 허용되는 동작이지만, 그래도 대응하는 형이 없는 정수 값으로 설정하면 안됩니다. 열거형의 형이 대응하지 않는 열거형의 값을 (불안전하게) 만들어내는 것은 미정의 동작입니다. (이렇게 함으로써 패턴 완전 매칭이 잘 작성되고 컴파일되게 됩니다.)

repr(transparent)

#[repr(transparent)]은 크기가 0이 아닌 하나의 필드를 가지고 있는 (무량 필드는 더 있어도 됩니다) 구조체나 형이 하나인 열거형에만 쓰일 수 있습니다. 이것의 효과는 구조체/열거형의 전체 레이아웃과 ABI가 그 하나의 필드와 완전히 동일하도록 보장된다는 것입니다.

주의: repr(transparent)를 공용체에 적용하는 transparent_unions nightly 기능이 있지만, 디자인 문제 때문에 표준화되지 않았습니다. 더 자세한 내용은 이슈를 참고하세요.

이것의 목표는 구조체/열거형과 그의 유일한 필드 간에 변환을 가능하게 하는 것입니다. 이것의 한 예는 UnsafeCell인데, 이것은 자신이 감싸고 있는 타입으로 변환될 수 있습니다 (UnsafeCell은 또한 불안정한 no_niche를 쓰기 때문에, 이것의 ABI는 다른 타입 속에 들어갔을 때에 동일하다고 보장되지는 않습니다).

또, 그 유일한 필드가 FFI로 보내도 괜찮은 타입이라면, 그 유일한 필드가 있는 구조체/열거형을 FFI로 보내는 작업도 잘 작동하는 것이 보장됩니다. 예를 들어, 이것은 struct Foo(f32)enum Foo { Bar(f32) }f32와 항상 동일한 ABI를 가지게 하도록 하기 위해서 꼭 필요합니다.

이 표현 방식은 타입의 유일한 필드가 pub이거나, 그 레이아웃이 단순하게 묘사되었을 경우에만 공개 ABI의 한 부분으로 인정됩니다. 그렇지 않다면 이 레이아웃은 다른 크레이트들이 의존해서는 안됩니다.

더 자세한 내용은 RFC 1758RFC 2645에 있습니다.

repr(u*), repr(i*)

이것들은 필드 없는 열거형을 만들기 위한 크기와 부호를 지정합니다. 식별자가 이 정수를 벗어나서 오버플로우되면 컴파일할 때 에러가 발생할 겁니다. 하지만 이것을 러스트에서 허용할 수도 있습니다: 오버플로우된 순간 0이 되게 명시적으로 말하는 것이죠. 그러나 러스트는 열거형의 두 개의 형이 같은 식별자를 가지는 것을 허용하지는 않을 겁니다.

"필드 없는 열거형"이라는 용어는 단지 열거형이 그 형에서 데이터를 가지지 않는다는 것을 말합니다. repr(u*)repr(C)가 없는 필드 없는 열거형은 여전히 러스트 타입이고, 안정적인 ABI 표현이 존재하지 않습니다. repr을 더하는 것은 ABI를 위해 이 열거형이 지정된 타입의 정수로 다뤄지게 합니다.

만약 열거형이 필드가 있다면, 타입의 정해진 레이아웃이 있다는 점에서 효과는 repr(C)의 효과와 비슷하게 됩니다. 이것은 열거형을 C 코드에 넘기고, 그 타입의 실제 표현을 접근하고 직접 그 태그와 필드를 조작할 수 있게 해 줍니다. 자세한 내용은 RFC를 참고하세요.

repr은 구조체에는 아무 효과도 없습니다.

필드가 있는 열거형에 명시적인 repr(u*), repr(i*), 혹은 repr(C)를 추가하는 것은 널 포인터 최적화를 억제하는데, 이것은 다음과 같습니다:

#![allow(unused)]
fn main() {
use std::mem::size_of;
enum MyOption<T> {
    Some(T),
    None,
}

#[repr(u8)]
enum MyReprOption<T> {
    Some(T),
    None,
}

assert_eq!(8, size_of::<MyOption<&u16>>());
assert_eq!(16, size_of::<MyReprOption<&u16>>());
}

이 최적화는 필드가 없는 열거형에 명시적으로 repr(u*), repr(i*), 혹은 repr(C)가 적용된 경우에는 여전히 작동합니다.

repr(packed)

repr(packed)는 타입에 어떤 여백도 제거하고, 한 바이트에 정렬하도록 러스트에 강제합니다. 이것은 메모리 사용량은 줄여주지만, 아마 다른 부작용들을 초래할 것입니다.

더 자세히 말하자면, 대부분의 아키텍쳐는 값들이 제대로 정렬되는 것을 매우 선호합니다. 이것은 제대로 정렬되지 않은 읽기는 뒤로 미뤄지거나 (x86), 심지어는 강제종료됩니다 (일부의 ARM 칩들). 이렇게 "압축"된 필드를 직접 읽거나 저장하는 간단한 경우들에서는, 컴파일러가 쉬프트나 마스크 같은 것들로 정렬 문제를 간단히 수정할 수 있을지도 모릅니다. 그러나 그 압축된 필드에 레퍼런스를 단다면, 컴파일러가 정렬되지 않은 읽기를 피할 수 있는 코드를 낼 수 있을 것 같지는 않습니다.

이것이 미정의 동작을 일으킬 수 있기 때문에, 린트가 구현되었고 지금은 아주 심각한 오류가 될 것입니다.

repr(packed)는 가볍게 사용되어서는 안됩니다. 극심하게 이것이 필요하지 않다면 이것은 쓰여져서는 안됩니다.

이 표현은 repr(C)repr(Rust)와 함께 쓸 수 있습니다.

repr(align(n))

repr(align(n))은 (n은 2의 거듭제곱입니다) 타입이 최소 n의 정렬선을 가지도록 요구합니다.

이것은 여러 가지 장난을 가능하게 해 주는데, 예를 들어 배열의 이웃하는 원소들이 서로의 캐시 선을 절대 공유하지 않도록 하는 것이 있습니다 (이것으로 어떤 종류의 동시성 코드는 빠르게 할 수 있습니다).

이 표현은 repr(C)repr(Rust)와 함께 쓸 수 있지만, repr(packed)와는 함께 쓸 수 없습니다.

소유권과 수명

소유권은 러스트의 혁신적인 기능입니다. 이것은 러스트가 쓰레기 수집을 피하면서, 완전히 메모리 안전과 효율성을 챙길 수 있게 해 줍니다. 소유권 시스템을 깊게 살펴보기 전에, 이런 설계의 동기를 생각해 보겠습니다.

우리는 당신이 쓰레기 수집(GC)이 항상 최선의 해결책은 아니고, 어떤 상황에서는 메모리를 수동으로 관리하는 것이 낫다는 것을 수긍한다고 가정하겠습니다. 만약 이것을 수긍하지 않는다면, 다른 언어를 찾아보시는 건 어떨까요?

GC에 대한 당신의 생각과 상관없이, 코드를 안전하게 만드는 것은 꽤나 확실하게 엄청난 축복입니다. 절대로 값들이 너무 빨리 없어지는 것을 걱정하지 않아도 되니까요 (그래도 그 값을 가리키고 싶었는지 여부는 조금 다른 문제이지만 말이죠...). 이것은 C와 C++ 프로그램들이 해결해야 하는, 프로그램 어디에서나 볼 수 있는 문제입니다. 비 GC 언어를 사용해본 사람들이라면 한번쯤은 저질러 보았을 이런 간단한 실수를 생각해 보세요:

#![allow(unused)]
fn main() {
fn as_str(data: &u32) -> &str {
    // String 만들기
    let s = format!("{}", data);

    // OH NO! 이 함수 안에서만 존재하는
    // 값의 레퍼런스를 반환했군요!
    // 달랑거리는 포인터 발생! 해제 후 사용! 이런!
    // (러스트에서는 컴파일되지 않습니다)
    &s
}
}

바로 이런 문제를 해결하기 위해서 러스트의 소유권 시스템이 만들어졌습니다. 러스트는 &s가 살아있는 범위를 알고 있고, 이것으로 이 레퍼런스의 탈출을 막을 수 있습니다. 그러나 이 문제는 C 컴파일러라도 그럴듯하게 잡을 수 있는 간단한 경우입니다. 코드가 커지고 포인터들이 다양한 함수들에 인자로 넘겨지면 상황은 더 복잡해집니다. 결국 C 컴파일러는 과부하를 일으키게 되고, 당신의 코드가 불건전하다는 것을 증명하기에 충분한 포인터 분석을 하지 못하게 됩니다. 이렇게 되면 당신의 프로그램이 그냥 맞다고 생각하고 받아들여야만 하겠죠.

러스트에서는 이런 일이 절대 일어나지 않을 것입니다. 컴파일러에게 모든 것이 건전하다는 것을 증명하는 것은 프로그래머의 몫입니다.

물론, 러스트의 소유권 시스템은 그냥 레퍼런스들이 그 본체의 범위를 벗어나지 않는 것을 검증하는 것보다 훨씬 복잡합니다. 그것은 포인터들이 언제나 유효하도록 보장하는 것은 이것보다 훨씬 복잡하기 때문입니다. 예를 들어 이 코드에서,

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
// 내부 값의 레퍼런스를 받아옵니다
let x = &data[0];

// OH NO! `push`는 `data`의 포인터가 재할당되도록 유도합니다.
// 달랑거리는 포인터! 해제 후 사용! 이런!
// (러스트에서는 컴파일되지 않습니다)
data.push(4);

println!("{}", x);
}

이 버그를 잡기 위해서는 단순한 범위 분석으로는 부족한데, 그것은 data가 우리가 필요한 만큼 오래 살기 때문입니다. 그러나 우리가 레퍼런스를 가지고 있는 동안 바뀌었지요. 그래서 러스트는 레퍼런스가 본체와 다른 레퍼런스들을 수정하지 못하게 막는 것입니다.

레퍼런스

레퍼런스에는 두 가지 종류가 있습니다:

  • 불변 레퍼런스: &
  • 가변 레퍼런스: &mut

이것들은 다음의 규칙들을 지킵니다:

  • 레퍼런스는 그 본체보다 오래 살 수 없다
  • 가변 레퍼런스는 복제할 수 없다

이게 다입니다. 이것이 레퍼런스가 따르는 모델 전부입니다.

물론, 우리는 복제한다는 것이 어떤 의미인지 정의해야겠죠.

error[E0425]: cannot find value `aliased` in this scope
 --> <rust.rs>:2:20
  |
2 |     println!("{}", aliased);
  |                    ^^^^^^^ not found in this scope

error: aborting due to previous error

아쉽게도, 러스트는 아직 복제 모델을 정의하지 않았습니다. 🙀

러스트 개발자들이 언어의 의미를 확실히 정하는 동안, 다음 섹션에서 복제가 무엇인지, 왜 중요한지 논해 보겠습니다.

복제

먼저, 몇 가지 짚고 넘어가겠습니다:

  • 논의를 위해 가능한 한 가장 넓은 복제의 정의를 사용할 것입니다. 러스트의 정의는 아마 변형이나 살아있음 여부에 대해서는 조금 더 제한적일 것입니다.

  • 우리는 한 스레드에서, 인터럽트 없는 실행을 가정하겠습니다. 또한 메모리-매핑된 하드웨어 같은 것들은 무시하겠습니다. 러스트는 당신이 말하지 않는 한 이런 것들이 일어나지 않는다고 가정합니다. 더 자세한 내용은 동시성 챕터를 참고하세요.

이런 것들을 말했으니, 이제 우리의 아직 작업중인 정의를 말해보겠습니다: 변수들과 포인터들은 메모리의 겹쳐지는 지역을 가리킬 때 복제되었다고 합니다.

복제가 중요한 이유

그래서 왜 우리가 복제를 신경써야 할까요?

이런 간단한 함수를 생각해 보세요:

#![allow(unused)]
fn main() {
fn compute(input: &u32, output: &mut u32) {
    if *input > 10 {
        *output = 1;
    }
    if *input > 5 {
        *output *= 2;
    }
    // `input > 10`일 경우 `output`은 `2`가 될 것이라는 것을 기억하세요
}
}

이 함수를 다음의 함수로 최적화할 수 있다면 좋겠네요:

#![allow(unused)]
fn main() {
fn compute(input: &u32, output: &mut u32) {
    let cached_input = *input; // `*input`을 레지스터에 저장합니다.
    if cached_input > 10 {
        // `input`이 10보다 크면, 이전 코드는 `output`을 1로 지정했다가 2를 곱하니, 
        // `output`은 최종적으로 2가 됩니다 (`>10`이면 `>5`일 테니까요).
        // 여기서는 두번 할당하는 것을 피하고 한번에 2로 설정합니다.
        *output = 2;
    } else if cached_input > 5 {
        *output *= 2;
    }
}
}

러스트에서는 이런 최적화가 건전할 것입니다. 거의 모든 다른 언어에서는 그렇지 않을 것입니다 (전역 분석을 제외하면). 이것은 이 최적화가 복제가 일어나지 않는다는 것에 의존하기 때문인데, 많은 언어들이 이것에 있어서 자유롭게 풀어두죠. 특별히 우리는 inputoutput이 겹치는 함수 매개변수들, 예를 들면 compute(&x, &mut x) 같은 것들을 걱정해야 합니다.

이런 입력으로는 이런 실행이 가능합니다:

                    //  input ==  output == 0xabad1dea
                    // *input == *output == 20
if *input > 10 {    // 참  (*input == 20)
    *output = 1;    // *input 에도 씀, *output 과 같기 때문
}
if *input > 5 {     // 거짓 (*input == 1)
    *output *= 2;
}
                    // *input == *output == 1

우리의 최적화된 함수는 이런 입력에 *output == 2라는 결과를 도출할 것이고, 따라서 우리의 최적화가 올바른지의 문제는 이런 입력이 불가능하다는 것에 기반합니다.

우리는 러스트에서는 &mut를 복제하는 것이 허락되지 않기 때문에, 이런 입력이 불가능하다는 것을 압니다. 따라서 우리는 안전하게 그런 가능성을 거부하고 최적화를 실행할 수 있게 됩니다. 다른 대부분의 언어들에서는 이런 입력 또한 완전히 가능할 것이고, 고려되어야 할 것입니다.

이것이 바로 복제 분석이 중요한 이유입니다: 유용한 최적화를 컴파일러가 실행하도록 해 주거든요! 몇 가지 예를 들자면:

  • 값의 메모리를 접근하는 포인터가 없다는 것을 증명함으로써 값들을 레지스터에 그대로 두는 것
  • 어떤 메모리는 마지막으로 읽은 후에 쓴 적이 없다는 것을 증명함으로써 읽기 작업들을 제거하는 것
  • 어떤 메모리는 다음 쓰기 작업 전에 읽은 적이 없다는 것을 증명함으로써 쓰기 작업들을 제거하는 것
  • 읽기 작업들이나 쓰기 작업들이 서로에 의존하지 않는다는 것을 증명함으로써 작업들을 옭기거나 순서를 바꾸는 것

이런 최적화는 또한 루프 벡터화, 상수 전파, 죽은 코드 제거 등의 더 큰 최적화의 건전함을 증명하게 되는 경향이 있습니다.

이전의 예제에서, 우리는 &mut u32가 복제될 수 없다는 사실을 이용해서 *output에 쓰는 작업이 *input에 영향을 줄 수 없다는 것을 증명했습니다. 이러면 우리는 레지스터에 *input을 캐싱해서, 읽기 작업을 제거할 수 있습니다.

이 읽기 작업을 캐싱함으로써, 우리는 > 10 분기에서 있는 쓰기 작업이 > 5 분기를 택하는지 여부를 영향주지 못한다는 것을 알게 되고, *input > 10일 때에 읽고, 수정하고, 다시 쓰는 작업(*output을 2배로 하는 작업)을 제거할 수 있게 됩니다.

복제 분석에 대해 꼭 기억해야 할 것은 쓰기 작업이 최적화를 방해하는 주된 걸림돌이라는 것입니다. 이 말은, 읽기 작업을 프로그램의 다른 곳으로 옮기는 것을 방해하는 유일한 것은 우리가 그것과 같은 메모리 위치에 쓰는 작업과 함께 순서를 바꾸는 것입니다.

예를 들어, 우리의 함수를 이렇게 수정한 버전이라면 우리는 복제에 대해서 걱정할 필요가 없는데, 왜냐하면 *output에 쓰는 유일한 작업을 함수의 가장 끝으로 옮겼기 때문입니다. 이는 우리가 이 쓰기 작업 이전에 있는, *input을 읽는 작업들을 자유롭게 재배치할 수 있다는 것을 의미합니다:

#![allow(unused)]
fn main() {
fn compute(input: &u32, output: &mut u32) {
    let mut temp = *output;
    if *input > 10 {
        temp = 1;
    }
    if *input > 5 {
        temp *= 2;
    }
    *output = temp;
}
}

우리는 아직도 inputtemp의 복제가 아니라는 것을 짐작하기 위해 복제 분석에 의존하지만, 증명은 훨씬 간단해집니다: 지역 변수의 값은 그것이 정의되기 전에 존재하던 것으로 복제할 수 없기 때문입니다. 이것은 모든 언어가 자유롭게 하는 짐작이고, 그래서 이 버전의 함수는 어느 언어에서든 우리가 원하는 대로 최적화시킬 수 있게 됩니다.

이것이 바로 러스트가 쓰는 "복제"의 정의가 살아있음과 변경 같은 개념이 동반되는 이유입니다: 실제로 메모리에 쓰는 작업이 없으면, 복제가 일어나도 상관없기 때문입니다.

물론, 러스트를 위한 총체적인 복제 모델은 함수 호출(보이지 않는 것들을 변경할 수도 있음)이나, 생 포인터 (그들 자체로는 복제의 요구사항이 없음), 그리고 UnsafeCell (&로 참조한 레퍼런스의 주체의 값이 변경되도록 허용함) 같은 것들도 고려해야만 합니다.

수명

러스트는 이런 규칙들을 수명을 통해서 강제합니다. 수명은 레퍼런스가 유효해야 하는, 이름이 지어진 코드의 지역입니다. 이 지역들은 꽤나 복잡할 수 있는데, 프로그램의 실행 분기에 대응하기 때문입니다. 또한 이런 실행 분기에는 구멍까지도 있을 수 있는데, 레퍼런스가 다시 쓰이기 전에 재초기화된다면, 이 레퍼런스를 무효화할 수 있기 때문입니다. 레퍼런스를 포함하는 (또는 포함하는 척하는) 타입도 수명을 붙여서 러스트가 그것을 무효화하는 것을 막게 할 수 있습니다.

우리의 예제 대부분, 수명은 코드 구역과 대응할 것입니다. 이것은 우리의 예제가 간단하기 때문입니다. 그렇게 대응되지 않는 복잡한 경우는 밑에 서술하겠습니다.

함수 본문 안에서 러스트는 보통 관련된 수명을 명시적으로 쓰게 하지 않습니다. 이것은 지역적인 문맥에서 수명에 대해 말하는 것은 대부분 별로 필요없기 때문입니다: 러스트는 모든 정보를 가지고 있고, 모든 것들이 가능한 한 최적으로 동작하도록 할 수 있습니다. 컴파일러가 아니라면 일일히 작성해야 하는 많은 무명 구역과 경계들을 컴파일러가 대신해 주어서 당신의 코드가 "그냥 작동하게" 해 줍니다.

그러나 일단 함수 경계를 건너고 나면 수명에 대해서 이야기해야만 합니다. 수명은 보통 작은 따옴표로 표시됩니다: 'a, 'static 처럼요. 수명이라는 주제에 발가락을 담그기 위해서, 우리는 코드 구역을 수명으로 수식할 수 있는 척을 하며, 이 챕터의 시작부터 있는 예제들의 문법적 설탕을 해독해 보겠습니다.

원래 우리의 예제들은 코드 구역과 수명에 대해서 공격적인 문법적 설탕-- 고당 옥수수콘 시럽 같은 --을 이용했는데, 모든 것들을 명시적으로 적는 것은 굉장히 요란하기 때문입니다. 모든 러스트 코드는 이런 식의 공격적인 추론과 "뻔한" 것들을 생략하는 것에 의존합니다.

문법적 설탕 중 하나의 특별한 조각은 모든 let 문장이 암시적으로 코드 구역을 시작한다는 점입니다. 대부분의 경우에는 이것이 문제가 되지는 않습니다. 그러나 서로 참조하는 변수들에게는 문제가 됩니다. 간단한 예제로, 이런 간단한 러스트 코드 조각을 해독해 봅시다:

#![allow(unused)]
fn main() {
let x = 0;
let y = &x;
let z = &y;
}

대여 검사기는 항상 수명의 길이를 최소화하려고 하기 때문에, 아마 이런 식으로 해독할 것입니다:

// 주의: `'a: {` 나 `&'b x` 는 유효한 문법이 아닙니다!
'a: {
    let x: i32 = 0;
    'b: {
        let y: &'b i32 = &'b x;
        'c: {
            let z: &'c &'b i32 = &'c y; // "i32의 레퍼런스의 레퍼런스" (수명이 표시되어 있음)
        }
    }
}

와. 이건... 별로네요. 러스트가 이런 것들을 간단하게 만들어 준다는 것을 잠시 감사하는 시간을 가집시다.

...

자, 계속하자면, 외부 범위에 레퍼런스를 넘기면 러스트는 더 긴 수명을 추론하게 됩니다:

#![allow(unused)]
fn main() {
let x = 0;
let z;
let y = &x;
z = y;
}
'a: {
    let x: i32 = 0;
    'b: {
        let z: &'b i32;
        'c: {
            // x의 레퍼런스가 'b 구역으로 넘겨지기 때문에 
            // 'b 가 됩니다.
            let y: &'b i32 = &'b x;
            z = y;
        }
    }
}

예제: 본체보다 오래 사는 레퍼런스들

좋습니다, 예전의 예제 중 몇 가지를 살펴봅시다:

#![allow(unused)]
fn main() {
fn as_str(data: &u32) -> &str {
    let s = format!("{}", data);
    &s
}
}

이렇게 해독됩니다:

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s;
    }
}

as_str의 시그니처는 어떤 수명을 가지고 있는, u32의 레퍼런스를 받고 딱 그만큼 사는, str의 레퍼런스를 만들 수 있다고 약속하고 있습니다. 이미 여기서 우리는 왜 이 시그니처가 문제가 있을 수도 있는지 알 수 있습니다. 이것은 우리가 str을 u32의 레퍼런스가 있던 코드 구역에서, 혹은 더 이전의 구역에서 찾을 것을 내포하고 있습니다. 이것은 좀 무리한 요구입니다.

그 다음 우리는 String s를 계산하고, 그것을 가리키는 레퍼런스를 반환합니다. 우리의 함수가 체결한 계약은 레퍼런스가 'a보다 더 살아야 한다고 되어 있으므로, 이것이 바로 우리가 이 레퍼런스에 할당해야 하는 수명입니다. 불행하게도 s'b 구역에서 정의되었으므로, 이 코드가 건전할 수 있는 유일한 방법은 'b'a를 포함하는 것입니다 -- 'a는 함수 호출 자체를 포함해야 하므로, 이는 명백하게 성립하지 않습니다. 그러므로 우리는 그 수명이 본체를 능가하는 레퍼런스를 만들었는데, 이것은 말 그대로 레퍼런스가 할 수 없다고 우리가 말했던 가장 첫번째 것입니다. 컴파일러는 당연히 우리 눈앞에서 터질 겁니다.

좀더 확실하게 하기 위해 이 예제를 확장해 볼 수 있습니다:

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s
    }
}

fn main() {
    'c: {
        let x: u32 = 0;
        'd: {
            // x가 유효한 범위 전체 동안 빌림이 유지될 필요가 
            // 없기 때문에 새로운 구역을 집어넣었습니다. as_str의 반환값은 
            // 이 함수 호출 이전에 존재하는 str을 찾아야만 합니다.
            // 당연히 일어나지 않을 일이죠.
            println!("{}", as_str::<'d>(&'d x));
        }
    }
}

젠장!

이 함수를 올바르게 작성하는 방법은 물론 다음과 같습니다:

#![allow(unused)]
fn main() {
fn to_string(data: &u32) -> String {
    format!("{}", data)
}
}

우리는 함수 안에서 소유한 값을 생성해서 반환해야 합니다! 우리가 이렇게 &'a str을 반환하려면 이것이 &'a u32의 필드 안에 있어야만 하는데, 당연히 이 경우에는 말이 안됩니다.

(사실 우리는 문자열 상수값을 반환할 수도 있었습니다. 이 상수값은 스택의 맨 밑바닥에 있다고 생각할 수 있습니다. 이 구현이 우리가 원하는 것을 조금 제한하기는 하지만요.)

예제: 가변 레퍼런스의 복제

다른 예제를 볼까요:

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
}
'a: {
    let mut data: Vec<i32> = vec![1, 2, 3];
    'b: {
        // 'b 는 우리가 이 레퍼런스가 필요한 동안 유지됩니다
        // (`println!`까지 가야 하죠)
        let x: &'b i32 = Index::index::<'b>(&'b data, 0);
        'c: {
            // &mut가 더 오래 살아남을 필요가 없기 때문에
            // 임시 구역을 추가합니다
            Vec::push(&'c mut data, 4);
        }
        println!("{}", x);
    }
}

여기서 문제는 조금 더 감추어져 있고 흥미롭습니다. 우리는 다음의 이유로 러스트가 이 프로그램을 거부하기를 원합니다: 우리는 data의 하위 변수를 참조하는, 살아있는 불변 레퍼런스 x를 가지고 있는데, 이 동안 datapush 함수를 호출하여 가변 레퍼런스를 취하려고 합니다. 이러면 복제된 가변 레퍼런스를 생성할 테고, 이것은 레퍼런스의 두번째 규칙을 위반할 것입니다.

하지만 이것은 러스트가 이 프로그램이 나쁘다고 알아내는 방법이 전혀 아닙니다. 러스트는 xdata의 일부의 레퍼런스라는 것을 이해하지 못합니다. 러스트는 Vec을 아예 이해하지 못합니다. 러스트가 보는 것은 x가 출력되기 위해서는 'b만큼 살아야 한다는 것입니다. Index::index의 시그니처가 그 다음에 요구하는 것은 우리가 data에서 만든 레퍼런스가 'b만큼 살아야 한다는 것입니다. 우리가 push를 호출하려고 할 때, 러스트는 우리가 &'c mut data를 만드려고 한다는 것을 보게 됩니다. 러스트는 'c'b 안에 있다는 것을 알게 되고, &'b data가 아직 살아 있어야 하기 때문에 우리의 프로그램을 거부합니다!

여기서 우리는 우리가 보존하는 데에 실제로 관심이 있는 레퍼런스 의미론보다 수명 시스템이 훨씬 헐거운 것을 볼 수 있습니다. 대부분의 경우에는 이것은 아무 문제가 없습니다, 왜냐하면 이것은 우리가 우리의 프로그램을 컴파일러에게 하루종일 설명하는 것을 방지해 주기 때문입니다. 그러나 이것은 러스트의 진정한 의미론에 잘 부합하는 몇 가지의 프로그램들이, 수명이 너무 멍청하기 때문에, 거부되는 것을 의미하기는 합니다.

수명이 차지하는 영역

레퍼런스(종종 빌림이라고 불립니다)는 창조된 곳부터 마지막으로 쓰이는 곳까지 살아있습니다. 빌려진 값은 살아있는 빌림들보다만 오래 살면 됩니다. 이것은 간단하게 보이지만, 몇 가지 미묘한 것들이 있습니다.

다음의 코드 조각은 컴파일되는데, x를 출력하고 난 후에는 더이상 필요하지 않아지므로, 이것이 달랑거리거나 복제되어도 상관없기 때문입니다 (변수 x실제로는 이 구역 끝까지 존재한다고 해도 말이죠).

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x);
// 이건 괜찮습니다, x는 더 이상 필요하지 않습니다
data.push(4);
}

하지만, 만약 값이 소멸자가 있다면, 그 소멸자는 그 구역 끝에서 실행됩니다. 그리고 소멸자를 실행하는 것은 그 값을 사용하는 것으로 간주되죠 - 당연하게도 마지막으로요. 따라서, 이것은 컴파일되지 않을 것입니다.

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct X<'a>(&'a i32);

impl Drop for X<'_> {
    fn drop(&mut self) {}
}

let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
data.push(4);
//여기서 소멸자가 실행되고 따라서 이 코드 조각은 컴파일에 실패합니다
}

컴파일러에게 x가 더 이상 유효하지 않다고 설득하는 방법 중 하나는 data.push(4) 전에 drop(x)를 사용하는 것입니다.

더 나아가서, 빌림의 가능한 마지막 사용처들이 여러 개 있을 수 있는데, 예를 들면 조건문의 각 가지입니다.

#![allow(unused)]
fn main() {
fn some_condition() -> bool { true }
let mut data = vec![1, 2, 3];
let x = &data[0];

if some_condition() {
    println!("{}", x); // 이것이 이 가지에서 `x`의 마지막 사용입니다
    data.push(4);      // 따라서 여기서 push할 수 있죠
} else {
    // 여기에는 `x`의 사용이 없으므로, 사실상 마지막 사용은
    // 이 예제 맨 위에서 x의 정의 시점입니다.
    data.push(5);
}
}

그리고 수명은 잠시 일시정지될 수 있습니다. 아니면 당신은 이것을 두 개의 별개의 빌림들이 같은 지역변수에 묶여 있다고 볼 수도 있습니다. 이것은 반복문 주변에서 종종 일어납니다 (반복문의 끝에서 변수에 새로운 값을 쓰고, 다음 차례 반복의 첫째 줄에서 그것을 마지막으로 사용하는 것이죠).

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
// 이 mut은 레퍼런스가 가리키는 곳을 바꿀 수 있게 해 줍니다
let mut x = &data[0];

println!("{}", x); // 이 빌림의 마지막 사용
data.push(4);
x = &data[3]; // 여기서 새로운 빌림이 시작합니다
println!("{}", x);
}

역사적으로 러스트는 빌림을 그 구역의 끝까지 살려 놓았으므로, 이런 예제들은 예전의 컴파일러에서는 컴파일에 실패할 수도 있습니다. 또한, 러스트가 빌림의 살아있는 범위를 제대로 줄이지 못하고, 컴파일되어야 할 것 같은 때에도 컴파일에 실패하는 희귀한 경우들이 있습니다. 이런 것들은 시간이 지나면서 해결될 것입니다.

수명의 한계

다음 코드가 주어졌을 때:

#[derive(Debug)]
struct Foo;

impl Foo {
    fn mutate_and_share(&mut self) -> &Self { &*self }
    fn share(&self) {}
}

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.share();
    println!("{:?}", loan);
}

이것이 컴파일이 되기를 기대할 수도 있습니다. 우리는 mutate_and_share를 호출하는데, 이것은 foo를 잠시 가변으로 빌리지만, 불변 레퍼런스만 반환하기 때문입니다. 따라서 foo는 가변으로 빌린 상태가 아니므로 우리는 foo.share()가 성공할 것이라고 기대할 것입니다.

하지만 우리가 이것을 컴파일하려고 하면:

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
  --> src/main.rs:12:5
   |
11 |     let loan = foo.mutate_and_share();
   |                --- mutable borrow occurs here
12 |     foo.share();
   |     ^^^ immutable borrow occurs here
13 |     println!("{:?}", loan);

무슨 일이 일어난 걸까요? 음, 이것은 전 섹션의 2번째 예제와 정확히 동일한 논법입니다. 우리가 이 프로그램을 해독하면 다음을 얻습니다:

struct Foo;

impl Foo {
    fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
    fn share<'a>(&'a self) {}
}

fn main() {
    'b: {
        let mut foo: Foo = Foo;
        'c: {
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
            println!("{:?}", loan);
        }
    }
}

수명 체계는 &mut foo의 수명을 'c로 늘려야 할 수밖에 없는데, 이는 loan의 수명과 mutate_and_share의 시그니처 때문입니다. 그 다음 우리가 share를 호출하려 할 때, 수명 체계는 우리가 &'c mut foo를 복제하려는 것을 알아차리고 우리 눈 앞에서 터집니다!

이 프로그램은 우리가 실제로 신경쓰는 레퍼런스 의미론에 따르면 명확히 옳지만, 수명 체계는 그것을 전달하기에 너무 헐겁습니다.

제대로 줄어들지 않은 빌림

다음의 코드는 컴파일되지 않는데, 이는 러스트가 map이라는 변수가 두 번 빌려졌다고 보고, 첫번째 빌림이 두번째 빌림이 시작하기 전에 필요가 없어진다는 것을 추론할 수 없기 때문입니다. 이것은 첫번째 빌림이 구역 전체 동안 사용된다고 러스트가 보수적으로 판단했기 때문입니다. 이 문제는 나중에는 고쳐질 것입니다.

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
where
    K: Clone + Eq + Hash,
    V: Default,
{
    match map.get_mut(&key) {
        Some(value) => value,
        None => {
            map.insert(key.clone(), V::default());
            map.get_mut(&key).unwrap()
        }
    }
}
}

주어진 수명의 제한 때문에 &mut map의 수명은 다른 가변 빌림과 겹치게 되고, 컴파일 에러로 이어집니다:

error[E0499]: cannot borrow `*map` as mutable more than once at a time
  --> src/main.rs:12:13
   |
4  |   fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
   |                  -- lifetime `'m` defined here
...
9  |       match map.get_mut(&key) {
   |       -     --- first mutable borrow occurs here
   |  _____|
   | |
10 | |         Some(value) => value,
11 | |         None => {
12 | |             map.insert(key.clone(), V::default());
   | |             ^^^ second mutable borrow occurs here
13 | |             map.get_mut(&key).unwrap()
14 | |         }
15 | |     }
   | |_____- returning this value requires that `*map` is borrowed for `'m`

수명 생략

흔한 코드 패턴을 더 편하게 만들기 위해서, 러스트는 함수 시그니처에서 수명이 생략되는 것을 허용합니다.

수명의 위치는 타입 안에서 수명을 쓸 수 있는 곳 어디나입니다:

&'a T
&'a mut T
T<'a>

수명의 위치는 "입력" 또는 "출력"으로 나타날 수 있습니다:

  • fn 정의, fn 타입, 그리고 Fn, FnMut, FnOnce 트레잇들에서, 입력은 매개변수들의 타입을 가리키고, 출력은 결과 타입을 가리킵니다. 따라서 fn foo(s: &str) -> (&str, &str)는 입력 위치에서 하나의 수명을, 출력 위치에서 두 개의 수명을 생략한 것이죠. fn 메서드 정의에서 입력 위치는 메서드의 impl 부분에 있는 수명들(혹은, 기본 메서드에 한해서, 트레잇 헤더에 있는 수명들도요)을 포함하지 않는다는 것을 유의하세요.

  • impl 부분에서는 모든 타입이 입력입니다. 따라서 impl Trait<&T> for Struct<&T>은 입력 위치에서 두 개의 수명을 생략했고, impl Struct<&T>은 하나의 수명을 생략한 것입니다.

생략 규칙은 다음과 같습니다:

  • 입력 위치에서 생략된 수명들은 각각 별개의 수명 매개변수가 됩니다.

  • 입력 위치에서 수명이 하나밖에 없다면 (생략되든 되지 않든), 그 수명이 출력 수명들 모두에 적용됩니다.

  • 만약 입력 위치에서 수명이 여러 개가 있는데, 그 중의 하나가 &self이거나 &mut self라면, self의 수명이 출력 위치에서 수명이 생략된 모든 수명들에게 할당됩니다.

  • 이 모든 경우가 해당하지 않는다면, 출력 수명을 생략하는 것은 오류입니다.

예제입니다:

fn print(s: &str);                                      // 생략됨
fn print<'a>(s: &'a str);                               // 확장됨

fn debug(lvl: usize, s: &str);                          // 생략됨
fn debug<'a>(lvl: usize, s: &'a str);                   // 확장됨

fn substr(s: &str, until: usize) -> &str;               // 생략됨
fn substr<'a>(s: &'a str, until: usize) -> &'a str;     // 확장됨

fn get_str() -> &str;                                   // 오류

fn frob(s: &str, t: &str) -> &str;                      // 오류

fn get_mut(&mut self) -> &mut T;                        // 생략됨
fn get_mut<'a>(&'a mut self) -> &'a mut T;              // 확장됨

fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command                  // 생략됨
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // 확장됨

fn new(buf: &mut [u8]) -> BufWriter;                    // 생략됨
fn new(buf: &mut [u8]) -> BufWriter<'_>;                // 생략됨 (`rust_2018_idioms`을 사용해서)
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a>          // 확장됨

무제한 수명

불안전한 코드는 종종 허공에서 레퍼런스나 수명을 만들어내곤 합니다. 이런 수명들은 무제한의 상태로 세계에 들어오게 됩니다. 이것이 일어나는 가장 흔한 경로는 생 포인터를 역참조한 다음 그것의 레퍼런스를 취하는 것인데, 이것은 무제한의 수명을 가진 레퍼런스를 생산하게 됩니다. 이런 수명은 상황이 요구하는 만큼 범위가 커집니다. 이것은 사실 그냥 'static이 되는 것보다 더 강력한데, 예를 들어 &'static &'a T&'a &'a T와 타입이 맞지 않을 것이지만, 무제한의 수명은 필요하다면 완벽하게 &'a &'a T에 맞아들어갈 것이기 때문입니다. 그러나 대부분의 의도와 목적에서는, 이런 무제한의 수명은 'static으로 간주해도 됩니다.

'static인 레퍼런스는 거의 없으니, 이것은 아마 잘못된 것일 것입니다. transmutetransmute_copy는 이런 일이 일어날 수 있는 또다른 두 가지의 원인들입니다. 우리는 최선을 다해서, 가능한 한 빨리 무제한의 수명을 제한시켜야 합니다, 특히 함수 경계를 지날 때 말이죠.

함수가 주어졌을 때 입력들에서 파생되지 않는 출력 수명은 무제한이 됩니다. 예를 들면:

fn get_str<'a>(s: *const String) -> &'a str {
    unsafe { &*s }
}

fn main() {
    let soon_dropped = String::from("hello");
    let dangling = get_str(&soon_dropped);
    drop(soon_dropped);
    println!("잘못된 str: {}", dangling); // 잘못된 str: gӚ_`
}

무제한의 수명을 피할 수 있는 가장 쉬운 방법은 함수 경계에서 수명을 생략하는 것입니다. 출력 수명이 생략되면 반드시 입력 수명에 제한되게 되니까요. 당연히 잘못된 수명에 제한될 수도 있겠지만, 이런 것들은 보통 메모리 안전이 흔하게 침범당하게 두기보다는, 컴파일 오류를 야기할 뿐입니다.

함수 안에서 수명을 제한하는 것은 더 오류가 일어나기 쉽습니다. 수명을 제한하는 가장 안전하고 가장 쉬운 방법은 제한된 수명이 있는 함수에서 반환하는 것입니다. 그러나 이것을 받아들일 수 없다면, 레퍼런스를 특정한 수명이 있는 위치에 놓을 수 있습니다. 불행히도 함수 안에서 연관된 모든 수명들을 다 나열할 수는 없습니다.

상계 트레잇 제한 (Higher-Rank Trait Bounds, HRTBs)

러스트의 Fn 트레잇은 조금 마법 같습니다. 예를 들어, 다음의 코드를 쓸 수 있겠습니다:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    where F: Fn(&(u8, u16)) -> &u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }

fn main() {
    let clo = Closure { data: (0, 1), func: do_it };
    println!("{}", clo.call());
}

만약 우리가 순진하게 수명 섹션에서 했던 대로 이 코드를 해독하려 하면, 좀 문제가 발생합니다:

// 주의: `&'b data.0`와 `'x: {`은 올바른 문법이 아닙니다!
struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    // where F: Fn(&'??? (u8, u16)) -> &'??? u8,
{
    fn call<'a>(&'a self) -> &'a u8 {
        (self.func)(&self.data)
    }
}

fn do_it<'b>(data: &'b (u8, u16)) -> &'b u8 { &'b data.0 }

fn main() {
    'x: {
        let clo = Closure { data: (0, 1), func: do_it };
        println!("{}", clo.call());
    }
}

세상에, 어떻게 우리가 F의 트레잇 제한에서 수명을 이야기할 수 있을까요? 이곳에 어떤 수명을 제공해야 하지만, 우리가 신경쓰는 수명은 우리가 call의 본문에 들어가기 전에는 이름을 붙일 수가 없습니다! 또한, 이 수명은 어떤 정해진 수명이 아닙니다; call은 그 시점에 &self가 가지고 있게 되는 아무 수명과 작업이 가능합니다.

이 작업은 상계 트레잇 제한(Higher-Rank Trait Bounds, HRTBs)의 마법이 필요합니다. 우리가 일전의 코드를 다시 해독하면 이와 같습니다:

where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,

또는 이렇게요:

where F: for<'a> Fn(&'a (u8, u16)) -> &'a u8,

(여기서 Fn(a, b, c) -> d 자체는 불안정한 실제 Fn 트레잇을 위한 문법 설탕입니다)

for<'a>는 "모든 'a의 선택들에 대해서"로 읽을 수 있고, 기본적으로 F가 만족시켜야 하는 트레잇 제한들의 무한한 목록을 만들어 냅니다. 살벌하군요. Fn 트레잇들 밖에서 상계 트레잇 제한들(HRTBs)을 만날 수 있는 곳은 별로 없고, 그런 경우들에도 우리는 대부분의 경우에 멋진 마법 문법 설탕이 있습니다.

요약하자면, 우리는 원래 코드를 좀더 명확하게, 이렇게 쓸 수 있습니다:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }

fn main() {
    let clo = Closure { data: (0, 1), func: do_it };
    println!("{}", clo.call());
}

부분타입 다형성과 변성(變性, Variance)

러스트는 빌림과 소유권 사이의 관계를 추적하기 위해 수명을 사용합니다. 하지만 수명의 순진한 구현은 너무 제한적이거나, 아니면 미정의 동작을 허용하게 됩니다.

수명을 유연하게 사용하면서도 수명의 오용을 방지하기 위해서, 러스트는 부분타입 다형성변성(變性, Variance) 을 사용합니다.

예제와 함께 시작해 보죠.

// 주의: debug는 수명이 *같은* 두 개의 매개변수를 기대합니다.
fn debug<'a>(a: &'a str, b: &'a str) {
    println!("a = {a:?} b = {b:?}");
}

fn main() {
    let hello: &'static str = "hello";
    {
        let world = String::from("world");
        let world = &world; // 'world 는 'static 보다 짧은 수명입니다
        debug(hello, world);
    }
}

보수적인 수명의 구현에서는 helloworld는 다른 수명을 가지고 있으므로, 우리는 다음과 같은 오류를 볼지도 모릅니다:

error[E0308]: mismatched types
 --> src/main.rs:10:16
   |
10 |         debug(hello, world);
   |                      ^
   |                      |
   |                      expected `&'static str`, found struct `&'world str`

이것은 뭔가 부적절할 것입니다. 이 경우에 우리가 원하는 것은 최소한 'world만큼만 사는 타입은 모두 받는 것입니다. 우리의 수명들에 부분타입 다형성을 이용해 봅시다.

부분타입 다형성

부분타입 다형성은 한 타입이 다른 타입 대신에 쓰일 수 있다는 개념입니다.

Sub이라는 타입이 Super라는 타입의 부분타입이라고 해 봅시다 (우리는 이 단원에서 이것을 Sub <: Super라고 표현하는 표기법을 사용하겠습니다).

이것이 우리에게 나타내는 것은 Super가 정의하는 요구사항들의 집합을 Sub이 완벽하게 충족한다는 것입니다. 그 다음 Sub은 더 많은 요구사항을 가질 수 있겠죠.

이제, 부분타입 다형성을 수명에 쓰기 위해, 우리는 수명의 요구사항을 정의해야 합니다:

'a는 코드 구역을 정의한다.

이제 수명을 위한 요구사항을 만들었으니, 우리는 수명들이 서로 어떻게 관련이 있는지를 정의할 수 있습니다:

'long이 정의하는 코드 구역이 'short가 정의하는 구역을 완전히 포함할 때, 그리고 오직 그 경우에만 'long <: 'short이다.

'long'short가 정의한 구역보다 더 넓은 코드 구역을 정의할 수 있지만, 그래도 우리의 정의에 어긋나지 않습니다.

우리가 이 단원의 나머지를 통해서 보겠지만, 부분타입 다형성은 이것보다는 훨씬 복잡하고 세밀하지만, 이 간단한 규칙은 직관상 99%로 아주 좋습니다. 그리고 만약 불안전한 코드를 작성하지 않는다면, 컴파일러가 당신을 위해 온갖 특수한 경우를 다 처리해 줄 겁니다. 하지만 이것은 러스토노미콘이죠. 우리는 불안전한 코드를 작성할 것이니, 우리는 이것이 실제로 어떻게 동작하는지, 그리고 우리가 이것을 어떻게 가지고 놀 수 있을지를 이해해야 합니다.

위의 예제로 돌아오면, 우리는 'static <: 'world라고 말할 수 있습니다. 지금으로써는, 수명의 부분타입 관계가 레퍼런스에도 그대로 전달된다는 것을 일단은 받아들입시다 (더 자세한 건 변성에서 다룹니다). 예를 들어, &'static str&'world str의 부분타입이므로, 우리는 &'static str&'world str로 "격하시킬" 수 있습니다. 이렇게 하면, 위의 예제는 컴파일될 겁니다:

fn debug<'a>(a: &'a str, b: &'a str) {
    println!("a = {a:?} b = {b:?}");
}

fn main() {
    let hello: &'static str = "hello";
    {
        let world = String::from("world");
        let world = &world; // 'world 는 'static 보다 짧은 수명입니다.
        debug(hello, world); // hello 는 조용히 `&'static str`을 `&'world str`로 격하시킵니다.
    }
}

변성(變性, Variance)

위에서 우리는 'static <: 'b&'static T <: &'b T를 함의한다는 것을 대충 넘어갔었습니다. 이것은 변성이라고 알려진 속성을 사용한 것인데요. 이 예제처럼 간단하지만은 않습니다. 이것을 이해하기 위해, 이 예제를 조금 확장해 보죠:

fn assign<T>(input: &mut T, val: T) {
    *input = val;
}

fn main() {
    let mut hello: &'static str = "hello";
    {
        let world = String::from("world");
        assign(&mut hello, &world);
    }
    println!("{hello}"); // 해제 후 사용 😿
}

assign에서 우리는 hello 레퍼런스를 world를 향해 가리키도록 합니다. 하지만 그 다음 world는, 나중에 helloprintln!에서 사용되기 전에, 구역 밖으로 벗어나고 맙니다.

이것은 전형적인 "해제 후 사용" 버그입니다!

우리의 본능은 먼저 assign의 구현을 나무랄 수도 있겠지만, 여기에는 잘못된 것이 없습니다. 우리가 T 타입의 값을 T 타입에 할당하는 것이 그렇게 무리는 아닐 겁니다.

문제는 우리가 &mut &'static str&mut &'b str이 서로 호환되는지를 짐작할 수 없다는 점입니다. 이것이 의미하는 것은 &mut &'static str&mut &'b str의 부분타입이 될 수 없다는 말입니다, 비록 'static'b의 부분타입이라고 해도요.

변성은 제네릭 매개변수를 통한 부분타입들간의 관계를 정의하기 위해 러스트가 빌린 개념입니다.

주의: 편의를 위해 우리는 제네릭 타입을 F<T>로 정의하여 T에 대해 쉽게 말할 것입니다. 이것이 문맥에서 잘 드러나길 바랍니다.

타입 F변성은 그 입력들의 부분타입 다형성이 출력들의 부분타입 다형성에 어떻게 영향을 주느냐 하는 것입니다. 러스트에서는 세 가지 종류의 변성이 있습니다. 두 타입 SubSuper가 있고, SubSuper의 부분타입일 때:

  • F<Sub>F<Super>의 부분타입일 경우 F공변(共變)합니다 (부분타입 특성이 전달됩니다)
  • F<Super>F<Sub>의 부분타입일 경우 F반변(反變)합니다 (부분타입 특성이 "뒤집힙니다")
  • 그 외에는 F무변(無變)합니다 (부분타입 관계가 존재하지 않습니다)

우리가 위의 예제에서 기억한다면, 'a <: 'b일 경우 &'a T&'b T의 부분타입으로 다뤄도 되었으니, &'a T'a에 대해서 공변하는 것이군요.

또한, 우리는 &mut &'a U&mut &'b U의 부분타입으로 다루면 안된다는 것을 보았으니, &mut TT에 대해서 무변하다고 말할 수 있겠습니다.

여기 다른 제네릭 타입들과 그들의 변성에 대한 표입니다:

'aTU
&'a T 공변공변
&'a mut T공변무변
Box<T>공변
Vec<T>공변
UnsafeCell<T>무변
Cell<T>무변
fn(T) -> U공변
*const T공변
*mut T무변

이 중의 몇 가지는 다른 것들과의 관계로 설명할 수 있습니다:

  • Vec<T>와 다른 모든 소유하는 포인터들과 컬렉션들은 Box<T>와 같은 논리를 따릅니다
  • Cell<T>와 다른 모든 내부 가변성이 있는 타입들은 UnsafeCell<T>와 같은 논리를 따릅니다
  • UnsafeCell<T>는 내부 가변성이 있으므로 &mut T와 같은 변성을 가지게 됩니다
  • *const T&T와 같은 논리를 따릅니다
  • *mut T&mut T(또는 UnsafeCell<T>)와 같은 논리를 따릅니다

더 많은 타입에 대해서는 참조서의 "Variance" 섹션을 보세요.

주의: 러스트 언어에서 반변 타입의 유일한 예는 함수의 매개변수이고, 따라서 실제 상황에서는 크게 와닿지 않습니다. 반변성을 끌어내려면 특정 수명을 가지고 있는 레퍼런스를 매개변수로 받는 함수 포인터를 가지고 고차원적인 프로그래밍을 해야 합니다 (만약 "아무 수명"을 모두 받는 레퍼런스였다면, 상계 수명을 이용하게 되는데, 이것은 부분타입 다형성과 독립적으로 작동하기 때문입니다).

이제 우리가 변성에 대한 좀 더 정식적인 이해를 했으니, 더 많은 예제를 더 자세히 살펴봅시다.

fn assign<T>(input: &mut T, val: T) {
    *input = val;
}

fn main() {
    let mut hello: &'static str = "hello";
    {
        let world = String::from("world");
        assign(&mut hello, &world);
    }
    println!("{hello}");
}

이것을 실행하면 어떤 결과가 나오나요?

error[E0597]: `world` does not live long enough
  --> src/main.rs:9:28
   |
6  |     let mut hello: &'static str = "hello";
   |                    ------------ type annotation requires that `world` is borrowed for `'static`
...
9  |         assign(&mut hello, &world);
   |                            ^^^^^^ borrowed value does not live long enough
10 |     }
   |     - `world` dropped here while still borrowed

다행이군요, 컴파일되지 않습니다! 여기서 무슨 일이 일어나고 있는 건지 자세하게 쪼개봅시다.

먼저 assign 함수를 봅시다:

#![allow(unused)]
fn main() {
fn assign<T>(input: &mut T, val: T) {
    *input = val;
}
}

이것이 하는 일은 가변 레퍼런스와 값을 받아서 가변 레퍼런스의 원본을 그 값으로 바꿔치기하는 것밖에 없습니다. 이 함수에 대해 중요한 것은 이 함수가 타입 동치 제약을 만든다는 점입니다. 이 함수는 시그니처에서 레퍼런스의 원본과 값은 아주 똑같은 타입이어야 한다고 명시하고 있습니다.

한편 우리는 이 함수에 &mut &'static str&'world str을 전달합니다.

&mut TT에 대해서 무변하기 때문에, 컴파일러는 첫째 매개변수에 아무런 부분타입 관계도 적용할 수 없다고 결론짓고, 따라서 T는 정확히 &'static str이어야만 하게 됩니다.

이것은 &T의 경우와 반대입니다:

#![allow(unused)]
fn main() {
fn debug<T: std::fmt::Debug>(a: T, b: T) {
    println!("a = {a:?} b = {b:?}");
}
}

여기도 비슷하게 ab는 같은 타입 T를 가져야만 하는군요. 하지만 &'a T'a에 대해서 공변하기 때문에, 우리는 부분타입 변환을 할 수 있습니다. 따라서 컴파일러는 &'static str&'b str의 부분타입인 경우에, 그리고 오직 그 경우에만, &'static str&'b str이 될 수 있다고 결정합니다. 이것은 'static <: 'b이면 성립할 텐데, 이 조건은 참이므로, 컴파일러는 행복하게 이 코드의 컴파일을 계속하게 됩니다.

보시다 보면 알겠지만, 왜 Box(와 Vec, HashMap, 등등)가 공변해도 괜찮은지는 수명이 왜 공변해도 괜찮은지와 비슷합니다: 당신이 이것들에 가변 레퍼런스 같은 것을 끼워넣으려고 한다면, 그들은 무변성을 상속받고 당신은 안 좋은 짓을 하는 것에서 방지될 테니까요.

한편 Box는 우리가 그냥 지나쳤던, 레퍼런스의 값의 측면에 집중하기 쉽게 해 줍니다.

값의 레퍼런스들이 얼마든지 복제되어서 자유롭게 읽고 쓸 수 있게 하는 많은 언어들과 달리, 러스트는 매우 엄격한 규칙이 있습니다: 당신이 값을 변경하거나 이동할 수 있다면, 당신이 접근 권한을 가진 유일한 사람이라는 뜻입니다.

다음 코드를 생각해 봅시다:

let hello: Box<&'static str> = Box::new("hello");

let mut world: Box<&'b str>;
world = hello;

우리가 hello'static 동안 살아 있었다는 것을 잊은 것은 아무런 문제가 되지 않습니다, 왜냐면 우리가 hello'b동안만 살아 있다고 알고 있는 변수에 옮겼을 때, 우리는 그것이 더 오래 살았다고 우주에서 유일하게 알고 있던 것을 없앴기 때문입니다!

이제 설명할 것이 하나만 남았군요: 함수 포인터입니다.

fn(T) -> U가 왜 U에 대해서 공변해야 하는지 보기 위해, 다음의 시그니처를 생각해 봅시다:

fn get_str() -> &'a str;

이 함수는 어떤 수명 'a에 묶인 str을 생산한다고 주장합니다. 그런 의미에서, 대신 이런 시그니처의 함수를 제공해도 완벽히 유효합니다:

fn get_static() -> &'static str;

이 함수를 호출할 때, 반환되길 기대하는 값은 최소한 'a만큼 사는 &str이니, 실제로는 값이 더 살아도 상관 없겠죠.

그러나, 매개변수들에는 같은 논리가 통하지 않습니다. 이 조건을:

fn store_ref(&'a str);

이것으로 만족시켜 보려고 생각해 보세요:

fn store_static(&'static str);

첫번째 함수는 최소한 'a만큼만 산다면 아무 문자열 레퍼런스를 받을 수 있지만, 두번째는 'static보다 짧게 사는 문자열 레퍼런스를 받을 수 없으니, 이것은 갈등을 초래하겠군요. 여기서는 공변성이 통하지 않습니다. 하지만 이것을 반대로 생각하면, 말이 됩니다! 만약 우리가 &'static str을 받는 함수가 필요하다면, 아무 레퍼런스 수명이나 받는 함수는 잘 동작할 겁니다.

실전에서 한번 살펴보죠.

use std::cell::RefCell;
thread_local! {
    pub static StaticVecs: RefCell<Vec<&'static str>> = RefCell::new(Vec::new());
}

/// 주어진 input을 스레드 지역변수 `Vec<&'static str>`에 집어넣습니다
fn store(input: &'static str) {
    StaticVecs.with_borrow_mut(|v| v.push(input));
}

/// 함수와 입력값을 받아서 입력값을 함수에 호출합니다 (같은 수명이어야 합니다!)
fn demo<'a>(input: &'a str, f: fn(&'a str)) {
    f(input);
}

fn main() {
    demo("hello", store); // "hello"는 'static입니다. `store`를 문제없이 호출할 수 있죠.

    {
        let smuggle = String::from("smuggle");

        // `&smuggle`은 'static이 아닙니다. 만약 우리가 `store`에 `&smuggle`을 전달하면,
        // `StaticVecs`에 잘못된 수명을 집어넣어 버린 게 될 겁니다.
        // 따라서, `fn(&'static str)`은 `fn(&'a str)`의 부분타입이 될 수 없습니다.
        demo(&smuggle, store);
    }

    // 해제 후 사용 😿
    StaticVecs.with_borrow(|v| println!("{v:?}"));
}

그리고 이것이 다른 타입들과 달리, 함수 타입들이 그 매개변수들에 대해서 변하는 이유입니다.

자, 이제 표준 라이브러리가 제공하는 타입들은 잘 살펴보았는데, 당신이 정의한 타입들의 변성은 어떨까요? 간단하게 말하자면, 구조체는 그 필드들의 변성을 상속받습니다. 만약 MyType 구조체가 필드 a에 쓰이는 제네릭 매개변수 A를 가지고 있다면, A에 대한 MyType의 변성은 A에 대한 a의 변성과 똑같습니다.

하지만 만약 A가 여러 필드에 쓰인다면:

  • A를 사용하는 모든 타입이 공변한다면, MyTypeA에 대해서 공변합니다
  • A를 사용하는 모든 타입이 반변한다면, MyTypeA에 대해서 반변합니다
  • 그 외에는, MyTypeA에 대해서 무변합니다
#![allow(unused)]
fn main() {
use std::cell::Cell;

struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> {
    a: &'a A,     // 'a와 A에 대해서 공변합니다
    b: &'b mut B, // 'b에 대해서 공변하고 B에 대해서 무변합니다

    c: *const C,  // C에 대해서 공변합니다
    d: *mut D,    // D에 대해서 무변합니다

    e: E,         // E에 대해서 공변합니다
    f: Vec<F>,    // F에 대해서 공변합니다
    g: Cell<G>,   // G에 대해서 무변합니다

    h1: H,        // 원래대로라면 H에 대해서 공변하겠지만...
    h2: Cell<H>,  // 변성이 충돌하면 무변성이 이기기 때문에, H에 대해서 무변하게 됩니다

    i: fn(In) -> Out,       // In에 대해서 반변하고, Out에 대해서 공변합니다

    k1: fn(Mixed) -> usize, // 원래대로라면 Mixed에 대해서 반변하겠지만..
    k2: Mixed,              // 변성이 충돌할 경우 무변성이 되기 때문에, Mixed에 대해서 무변하게 됩니다
}
}

해제 검사

우리는 수명이 간단한 규칙으로 우리가 달랑거리는 레퍼런스를 절대 읽지 않도록 보장하는 것을 봤습니다. 하지만 이 동안 우리는 한 수명이 다른 수명보다 오래 산다고 할 때, 두 수명이 같은 경우도 포함한 경우만 말했습니다. 즉, 우리가 'a: 'b라고 할 때, 'a 'b만큼만 살아도 괜찮았다는 말입니다. 처음 보면, 이것은 의미없는 구분 같습니다. 같은 시점에 동시에 해제되는 값은 없잖아요, 그렇죠? 그래서 우리는 이런 let 문장들을:

let x;
let y;

이렇게 해독할 수 있었죠:

{
    let x;
    {
        let y;
    }
}

이렇게 코드 구역을 이용해서 해독할 수 없는, 좀더 복잡한 상황들도 있지만, 순서는 여전히 정의되어 있습니다 - 변수들은 그 정의 순서의 반대로 해제되고, 구조체와 튜플의 필드들은 그 정의 순서대로 해제됩니다. RFC 1857에 해제 순서에 대한 좀더 자세한 내용이 있습니다.

이렇게 해 봅시다:

let tuple = (vec![], vec![]);

왼쪽 벡터가 먼저 해제됩니다. 하지만 이것이 대여 검사기의 눈에 오른쪽 벡터가 왼쪽 벡터보다 엄밀하게 더 오래 산다는 것을 뜻할까요? 이 질문의 답은 아니라는 겁니다. 대여 검사기는 튜플의 필드들을 분리해서 추적할 수 있지만, 벡터 원소들에 대해서는 무엇이 무엇보다 오래 사는지 결정할 수 없을 겁니다, 왜냐하면 그 원소들은 대여 검사기가 이해하지 못하는, 순수한 라이브러리 코드로 해제되기 때문입니다.

그래서 우리는 이것을 왜 신경쓸까요? 우리가 신경쓰는 이유는 만약 타입 시스템이 주의하지 않으면, 우발적으로 달랑거리는 포인터를 만들 수도 있기 때문입니다. 다음의 간단한 프로그램을 생각해 보세요:

struct Inspector<'a>(&'a u8);

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days));
}

이 프로그램은 완벽히 건전하고 오늘날 컴파일됩니다. daysinspector보다 엄밀하게 더 오래 살지는 않는다는 사실이 중요하지 않습니다. inspector가 살아있는 동안, days도 그럴 테니까요.

그러나 우리가 소멸자를 추가하면, 이 프로그램은 더 이상 컴파일되지 않습니다!

struct Inspector<'a>(&'a u8);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("I was only {} days from retirement!", self.0);
    }
}

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days));
    // `days`가 먼저 해제되게 되었다고 가정해 봅시다.
    // 그럼 `Inspector`가 해제될 때, 이미 해제된 메모리를 읽으려고 할 겁니다!
}
error[E0597]: `world.days` does not live long enough
  --> src/main.rs:19:38
   |
19 |     world.inspector = Some(Inspector(&world.days));
   |                                      ^^^^^^^^^^^ borrowed value does not live long enough
...
22 | }
   | -
   | |
   | `world.days` dropped here while still borrowed
   | borrow might be used here, when `world` is dropped and runs the destructor for type `World<'_>`

필드의 순서를 바꾸거나 구조체 대신 튜플을 쓴다 해도, 컴파일되지는 않을 겁니다.

Drop을 구현하는 것은 Inspector가 죽는 동안 어떤 임의의 코드를 실행하게 해 줍니다. 이것이 의미하는 것은 Inspector가 사는 동안 살기로 되어있는 타입들이 실제로는 먼저 해제되었는지 관찰할 수도 있다는 것입니다.

흥미롭게도, 제네릭 타입들만 이런 것에 대해서 걱정해야 합니다. 제네릭이 아니라면, 그들이 사용할 수 있는 수명은 'static밖에 없는데, 이것은 정말로 영원히 살 것이기 때문입니다. 이것이 바로 이런 문제가 건전한 제네릭 해제라고 불리는 이유입니다. 건전한 제네릭 해제는 해제 검사기에 의해 강제됩니다. 이 글을 쓰는 시점에서, 해제 검사기(dropck라고도 부릅니다)가 어떻게 타입을 검증하는지에 대한 자세한 내용은 전혀 정해져 있지 않습니다. 하지만 큰 틀에서의 규칙은 우리가 이 섹션 내내 집중해온 엄밀함입니다:

제네릭 타입이 건전하게 Drop을 구현하려면, 그 제네릭 매개변수들이 엄밀하게 더 오래 살아야 합니다.

이 규칙을 지키는 것은 (보통은) 대여 검사기를 만족시키기 위해서 필수적입니다; 이 규칙을 지키는 것은 건전하기에는 충분하지만 필수적이지는 않습니다. 즉, 당신의 타입이 이 규칙을 지킨다면 해제되기에 확실히 건전하다는 말입니다.

이 규칙을 만족시키는 것이 항상 필수는 아닌 이유는 타입이 빌린 데이터를 접근할 수 있음에도, 어떤 Drop 구현은 빌린 데이터를 접근하지 않거나, 아니면 우리가 세부적인 해제 순서를 알고, 그럼에도 빌린 데이터는 괜찮을 것을 알 수도 있기 때문입니다, 비록 대여 검사기가 이를 모르더라도요.

예를 들어, 위의 Inspector를 이렇게 변형하면 빌린 데이터를 절대 접근하지 않을 겁니다:

struct Inspector<'a>(&'a u8, &'static str);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("Inspector(_, {})는 보지 *않아야* 할 때를 압니다.", self.1);
    }
}

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days, "gadget"));
    // `days`가 먼저 해제되게 된다고 해 봅시다.
    // `Inspector`가 해제되어도, 그 소멸자는 빌린 `days`를
    // 접근하지 않을 겁니다.
}

마찬가지로, 이렇게 변형한 것도 빌린 데이터를 절대 접근하지 않을 겁니다:

struct Inspector<T>(T, &'static str);

impl<T> Drop for Inspector<T> {
    fn drop(&mut self) {
        println!("Inspector(_, {})는 보지 *않아야* 할 때를 압니다.", self.1);
    }
}

struct World<T> {
    inspector: Option<Inspector<T>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days, "gadget"));
    // `days`가 먼저 해제되게 된다고 해 봅시다.
    // `Inspector`가 해제되어도, 그 소멸자는 빌린 `days`를 
    // 접근하지 않을 겁니다.
}

하지만, 위의 수정된 코드들은 fn main을 분석할 때 둘 모두 대여 검사기에게 거부됩니다, "days가 충분히 오래 살지 않는다"고 말이죠.

그 이유는 main의 대여 검사는 InspectorDrop 구현의 내부는 모르기 때문입니다. 대여 검사기가 main을 분석하는 동안 아는 것이라고는, Inspector의 소멸자의 본문이 빌린 데이터를 접근할 수도 있다는 점입니다.

따라서, 해제 검사기는 어떤 값 안에 있는 모든 빌린 데이터(레퍼런스가 아닌 본체)가 그 값보다 오래 살도록 강제합니다.

탈출 장치

해제 검사를 좌우하는 정확한 규칙들은 미래에는 좀 덜 엄격할 수도 있겠습니다.

현재의 분석은 의도적으로 보수적이고 흔합니다; 어떤 값에 있는 모든 레퍼런스의 본체들이 그 값보다 오래 살도록 강제하는데, 이것은 확실히 건전하기 때문입니다.

언어의 미래 버전들은 분석을 더 예리하게 해서, 건전한 코드가 안전하지 않다고 거부되는 경우들을 줄일 수 있을 것입니다. 그러면 위의 두 Inspector들과 같이 소멸하는 동안 그 원소를 접근하지 않는 경우를 해결하는 데 도움이 될 것입니다.

그 동안, 제네릭 타입의 소멸자가 수명이 다한 데이터를 접근하지 않는다고 (불안전하게) 보장하는 불안정 속성이 있습니다, 비록 그 타입으로는 수명이 다한 데이터를 접근할 수 있더라도요.

이 속성은 may_dangle이라 부르고, RFC 1327에서 소개되었습니다. 이것을 위의 Inspector에 적용하려면, 이렇게 씁니다:

#![feature(dropck_eyepatch)]

struct Inspector<'a>(&'a u8, &'static str);

unsafe impl<#[may_dangle] 'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("Inspector(_, {})는 보지 *않아야* 할 때를 압니다.", self.1);
    }
}

struct World<'a> {
    days: Box<u8>,
    inspector: Option<Inspector<'a>>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days, "gadget"));
}

이 속성을 사용하려면 Drop 구현을 unsafe로 수식해야 하는데, 이는 수명이 다했을 수 있는 데이터(예를 들어 위의 self.0)를 접근하지 않는다는 보장을 컴파일러가 검사하지 않기 때문입니다.

이 속성은 어느 숫자의 수명이나 타입 매개변수에도 적용할 수 있습니다. 다음의 예제에서 우리는 'b의 수명을 갖는 레퍼런스를 접근하지 않고, T는 오직 이동 혹은 해제만 할 것이라고 컴파일러에게 알립니다. 하지만 'aU에 대해서는 이 속성을 쓰지 않음으로써 우리가 이 수명과 이 타입의 데이터를 접근할 것이라고 알립니다:

#![allow(unused)]
#![feature(dropck_eyepatch)]
fn main() {
use std::fmt::Display;

struct Inspector<'a, 'b, T, U: Display>(&'a u8, &'b u8, T, U);

unsafe impl<'a, #[may_dangle] 'b, #[may_dangle] T, U: Display> Drop for Inspector<'a, 'b, T, U> {
    fn drop(&mut self) {
        println!("Inspector({}, _, _, {})", self.0, self.3);
    }
}
}

보통은 위의 경우처럼, 필드 접근이 불가능하다는 것이 자명할 때가 많습니다. 그러나 제네릭 타입 매개변수를 가지고 작업하다 보면, 그런 접근이 간접적으로 일어날 수 있습니다. 간접적인 접근의 예는 다음과 같습니다:

  • 콜백 호출하기
  • 트레잇 메서드를 통해서

(impl 구체화 같은 러스트의 미래 변화에 따라, 이런 간접적 접근을 가능하게 하는 방법이 더 많아질 수 있습니다.)

여기 콜백을 호출하는 예제가 있습니다:

#![allow(unused)]
fn main() {
struct Inspector<T>(T, &'static str, Box<for <'r> fn(&'r T) -> String>);

impl<T> Drop for Inspector<T> {
    fn drop(&mut self) {
        // 예를 들어 만약 `T`가 `&'a _`라면, `self.2` 호출이 빌린 데이터를 접근할 수 있습니다.
        println!("Inspector({}, {})는 자신도 모르게 파기된 데이터를 봅니다.",
                 (self.2)(&self.0), self.1);
    }
}
}

이것은 트레잇 메서드를 호출하는 예제입니다:

#![allow(unused)]
fn main() {
use std::fmt;

struct Inspector<T: fmt::Display>(T, &'static str);

impl<T: fmt::Display> Drop for Inspector<T> {
    fn drop(&mut self) {
        // 예를 들어 만약 `T`가 `&'a _`이면, 밑에 숨겨진 `<T as Display>::fmt` 호출은
        // 빌린 데이터를 접근할 수 있습니다.
        println!("Inspector({}, {})는 자신도 모르게 파기된 데이터를 봅니다.",
                 self.0, self.1);
    }
}
}

그리고 당연히, 이런 접근들은 소멸자에 의해 호출되는 다른 어떤 메서드 안에 숩겨져 있을 수도 있습니다, 꼭 직접 쓰여지지 않고도요.

&'a u8이 소멸자에서 접근되는 위의 모든 경우에서, #[may_dangle] 속성을 추가하면 그 타입은 대여 검사기가 잡지 않을 오용을 막기 힘들어지고, 대혼란을 야기할 수 있습니다. 이 속성은 사용하지 않는 게 좋겠네요.

해제 순서에 관하여

구조체 안의 필드의 해제 순서는 정의되어 있지만, 이것에 의존하는 것은 불안정하고 애매합니다. 해제 순서가 중요할 때에는, ManuallyDrop 타입을 대신 쓰는 것이 좋습니다.

해제 검사기에 대해서는 이게 전부인가요?

사실 우리가 불안전한 코드를 작성할 때, 보통은 해제 검사기를 전혀 신경쓰지 않아도 됩니다. 하지만 우리가 신경써야 하는 한 가지 특수한 경우가 있는데, 이것에 관해서는 다음 섹션에서 살펴보겠습니다.

PhantomData

불안전한 코드와 작업을 하다 보면, 우리는 종종 타입이나 수명이 구조체와 논리적으로 연관되어 있지만, 실제로 그 필드의 일부분은 아닌 상황을 마주할 수 있습니다. 이런 상황은 보통 수명인 경우가 많은데요, 예를 들어 &'a [T]를 위한 Iter는 (거의) 다음과 같이 정의되어 있습니다:

#![allow(unused)]
fn main() {
struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}
}

하지만 'a가 구조체의 본문에 쓰이지 않았기 때문에, 이 수명은 무제한이 됩니다. 이것이 역사적으로 초래해왔던 문제들 때문에, 무제한 수명과 이를 사용하는 타입은 구조체 선언에서 금지되었습니다. 따라서 우리는 어떻게든 이 타입들을 구조체 안에서 참조해야 합니다. 이것을 올바르게 하는 것은 올바른 변성과 해제 검사에 있어서 필수적입니다.

우리는 이것을 특별한 표시 타입인 PhantomData를 통해서 합니다. PhantomData는 공간을 차지하지 않지만, 컴파일러의 분석을 위해 주어진 타입의 필드를 흉내냅니다. 이 방식은 우리가 원하는 변성을 직접 타입 시스템에 말하는 것보다 더 오류에 견고하다고 평가되었습니다. 또한 이 방식은 자동 트레잇과 해제 검사에 필요한 정보 등의 유용한 것들을 컴파일러에게 제공합니다.

Iter는 논리적으로 여러 개의 &'a T를 포함하므로, 바로 이렇게 우리는 PhantomData에게 흉내내라고 할 것입니다:

#![allow(unused)]
fn main() {
use std::marker;

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}
}

이렇게만 하면 됩니다. 수명은 제한될 것이고, 반복자는 'aT에 대해서 공변할 것입니다. 모든 게 그냥 마법처럼 동작할 겁니다.

제네릭 매개변수와 해제 검사

RFC 1238의 도움으로, 우리가 이런 코드를 쓴다면:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // 변성을 위한 `*const`
    len: usize,
    cap: usize,
}

#[cfg(any())]
impl<T> Drop for Vec<T> { /* … */ }
}

impl<T> Drop for Vec<T>의 존재가 러스트로 하여금 Vec<T>T 타입의 값들을 소유한다고 (더 정확히는: Drop 구현에서 T 타입의 값들을 사용할 수 있다고) 간주하게 만들고, 따라서 러스트는 Vec<T>가 해제될 때 T 타입의 값들이 달랑거리는 것을 허용하지 않을 것입니다.

따라서 어떤 타입이 Drop impl을 가지고 있다면, 추가적으로 _owns_T: PhantomData<T> 필드를 선언하는 것은 불필요하고 아무것도 달성하지 않습니다, 해제 검사기가 볼 때에는요 (변성과 자동 트레잇들에서는 영향을 줍니다).

특수한 경우: 만약 PhantomData를 포함하는 타입이 그 자체로는 Drop 구현이 전혀 없지만, Drop 구현이 있는 다른 필드를 포함한다면, 여기에 명시된 해제 검사기/#[may_dangle] 사항들이 적용될 것입니다: 포함하는 타입이 범위 밖으로 벗어날 때, PhantomData<T> 필드는 T 타입이 해제되어도 괜찮도록 할 것입니다.


하지만 이런 상황은 때때로 과도하게 제한된 코드로 이어질 수 있습니다. 바로 그래서 표준 라이브러리는 불안정하고 unsafe한 속성을 써서 바로 이 문서에서 경고했던, 구식의 "수동" 해제 검사 방식으로 돌아가는 겁니다: #[may_dangle] 속성으로요.

예외: 표준 라이브러리의 특수한 경우와 불안정한 #[may_dangle]

이 섹션은 당신이 자신의 라이브러리 코드만을 작성한다면 넘어가도 됩니다. 하지만 표준 라이브러리가 실제 Vec 정의를 가지고 무엇을 하는지 궁금하다면, 건전성을 위해 여전히 _owns_T: PhantomData<T>가 필요하다는 것을 알아차릴 겁니다.

그 이유를 보려면 클릭하세요

다음의 예제를 생각해 봅시다:

fn main() {
    let mut v: Vec<&str> = Vec::new();
    let s: String = "Short-lived".into();
    v.push(&s);
    drop(s);
} // <- `v` 는 여기서 해제됩니다

정석적으로 impl<T> Drop for Vec<T> {를 정의하면, 위의 코드는 부정됩니다.

확실히 이런 경우에서는, 우리는 Vec<&'s str>, 즉 str's만큼 사는 레퍼런스들의 벡터를 가지고 있습니다. 하지만 let s: String에서는, 이것이 Vec보다 먼저 해제되고, impl<'s> Drop for Vec<&'s str> {의 정의가 사용됩니다.

이것이 의미하는 것은 만약 이런 Drop 구현이 사용된다면, 파기된, 혹은 달랑거리는 수명 's로 작업을 할 것이라는 점입니다. 하지만 이것은 함수 시그니처에 있는 모든 러스트 레퍼런스는 기본적으로 달랑거리지 않고 역참조해도 문제가 없다는 러스트 규칙에 반대됩니다.

따라서 러스트는 보수적으로 이 코드를 부정할 수밖에 없습니다.

그런데 실제 Vec의 경우에서, Drop 구현은 &'s str에 대해 신경쓰지 않는데, 이는 &'s str이 따로 Drop 구현이 없기 때문입니다: VecDrop 구현은 그저 버퍼를 해제하고 싶을 뿐이죠.

즉, Vec의 경우를 특별하게 구분해서, 또는 Vec의 특수한 성질을 이용해서 위의 코드가 컴파일되면 좋겠네요: Vec가지고 있는 &'s str들을 해제될 때 사용하지 않도록 약속할 수도 있겠어요.

이 약속은 #[may_dangle]로 표현될 수 있는 unsafe한 약속입니다:

unsafe impl<#[may_dangle] 's> Drop for Vec<&'s str> { /* … */ }

아니면 좀더 일반적으로 표현하자면:

unsafe impl<#[may_dangle] T> Drop for Vec<T> { /* … */ }

이것이 러스트의 해제 검사기가 해제되는 값의 타입 매개변수가 달랑거리지 않도록 하는, 보수적인 추측에서 탈출하도록 하는 unsafe한 방법입니다.

표준 라이브러리와 같이 이렇게 했다면, 우리는 T가 자체의 Drop 구현이 있는 경우를 조심해야 합니다. 이 경우에는, &'s strstruct PrintOnDrop<'s>(&'s str);로 바꾸는 것을 상상해 봅시다. 이 구조체는 자체의 Drop 구현에서 내부의 &'s str를 역참조하여 화면에 출력할 것입니다.

확실히 버퍼를 해제하기 전에 Drop for Vec<T> {는, 내부의 각 T들이 Drop 구현이 있을 때, 각 T들을 해제시켜야 합니다. PrintOnDrop<'s>의 경우에 Drop for Vec<PrintOnDrop<'s>>는 버퍼를 해제하기 전에 PrintOnDrop<'s>의 원소들을 해제시켜야 합니다.

따라서 우리가 's#[may_dangle]하다고 말할 때, 이것은 심하게 모호하게 말했던 것입니다. 우리는 대신 이렇게 말해야 할 것입니다: "'sDrop 구현에 구속받지 않는 한에서 달랑거릴 수도 있습니다". 혹은 더 일반적으로 이렇게요: "TDrop 구현에 구속받지 않는 한에서 달랑거릴 수도 있습니다". 이런 "예외의 예외"는 우리가 T를 소유할 때마다 발생하는 흔한 현상입니다. 이래서 러스트의 #[may_dangle]은 이런 예외 상황에 대해 알고, 따라서 구조체의 필드들에 제네릭 매개변수가 소유되는 때에 비활성화될 것입니다.

따라서 표준 라이브러리는 이렇게 결론을 내립니다:

#![allow(unused)]
fn main() {
#[cfg(any())]
// 우리는 `Vec`을 해제할 때 `T`를 사용하지 않도록 약속합니다…
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            if mem::needs_drop::<T>() {
                /* … 이 경우는 제외하고요, 어떤 경우냐면 … */
                ptr::drop_in_place::<[T]>(/* … */);
            }
            // …
            dealloc(/* … */)
            // …
        }
    }
}

struct Vec<T> {
    // … `Vec`이 `T`의 원소들을 소유하고,
    // 따라서 해제될 때 `T`의 원소들을 해제시킬 때 말이죠!
    _owns_T: core::marker::PhantomData<T>,

    ptr: *const T, // 변성을 위한 `*const`입니다 (하지만 이것이 *그 자체로* `T`의 소유권을 나타내는 것은 아닙니다)
    len: usize,
    cap: usize,
}
}

할당된 메모리를 소유하는 생 포인터는 너무나 흔한 패턴이여서, 표준 라이브러리는 이것을 위한 도구인 Unique<T>를 만들었는데, 이것은:

  • 변성을 위해 *const T를 안에 포함합니다
  • PhantomData<T>를 포함합니다
  • 마치 T가 포함된 것처럼 Send/Sync를 자동으로 구현합니다
  • 널 포인터 최적화를 위해 포인터를 NonZero로 표시합니다

PhantomData 패턴의 표

여기 PhantomData가 사용될 수 있는 모든 경우의 놀라운 표가 있습니다:

흉내내는 타입'a의 변성T의 변성Send/Sync
(or lack thereof)
해제 구현에서 'aT의 달랑거림
(: #[may_dangle] Drop)
PhantomData<T>-전달불가 ("T를 소유")
PhantomData<&'a T>T : Sync
이면
Send + Sync
가능
PhantomData<&'a mut T>전달가능
PhantomData<*const T>-!Send + !Sync가능
PhantomData<*mut T>-!Send + !Sync가능
PhantomData<fn(T)>-Send + Sync가능
PhantomData<fn() -> T>-Send + Sync가능
PhantomData<fn(T) -> T>-Send + Sync가능
PhantomData<Cell<&'a ()>>-Send + !Sync가능
  • 주의: 자동으로 구현되는 Unpin 트레잇을 구현하지 않으려면 따로 선언된 PhantomPinned 타입을 사용해야 합니다.

대여 쪼개기

가변 레퍼런스의 상호 배제 규칙은 복잡한 구조와 작업할 때 굉장히 제한적일 수 있습니다. 대여 검사기(즉 borrowck)는 기본적인 내용은 이해하지만, 금세 쉽게 넘어질 겁니다. 이것은 한 구조체의 다른 필드들을 빌리는 것이 가능하다는 것은 알 정도로 구조체를 이해합니다. 따라서 이 코드는 오늘날 잘 작동합니다:

#![allow(unused)]
fn main() {
struct Foo {
    a: i32,
    b: i32,
    c: i32,
}

let mut x = Foo {a: 0, b: 0, c: 0};
let a = &mut x.a;
let b = &mut x.b;
let c = &x.c;
*b += 1;
let c2 = &x.c;
*a += 10;
println!("{} {} {} {}", a, b, c, c2);
}

하지만 borrowck는 배열이나 슬라이스는 전혀 이해하지 못해서, 이 코드는 동작하지 않습니다:

#![allow(unused)]
fn main() {
let mut x = [1, 2, 3];
let a = &mut x[0];
let b = &mut x[1];
println!("{} {}", a, b);
}
error[E0499]: cannot borrow `x[..]` as mutable more than once at a time
 --> src/lib.rs:4:18
  |
3 |     let a = &mut x[0];
  |                  ---- first mutable borrow occurs here
4 |     let b = &mut x[1];
  |                  ^^^^ second mutable borrow occurs here
5 |     println!("{} {}", a, b);
6 | }
  | - first borrow ends here

error: aborting due to previous error

borrowck가 이런 간단한 경우를 이해할 수 있다는 것은 좋지만, borrowck가 트리 같은 일반적인 컨테이너 타입들에서 "다른 부분"이라는 개념을 이해할 가능성은 거의 없습니다, 특히 다른 키들이 같은 값에 대응할 때는요.

borrowck에게 우리가 하는 작업이 괜찮다는 것을 "가르쳐 주기" 위해, 우리는 불안전한 코드로 내려와야 합니다. 예를 들어 가변 슬라이스는 슬라이스를 소비하고 두 개의 가변 슬라이스를 반환하는 split_at_mut 함수를 정의합니다. 하나는 인덱스의 왼쪽에 있는 모든 것의 슬라이스이고, 다른 하나는 인덱스의 오른쪽에 있는 모든 것의 슬라이스입니다. 직관적으로 우리는 이것이 안전하다는 것을 아는데, 이는 슬라이스가 서로 겹치지 않기 때문입니다. 하지만 이것을 구현하는 것은 어느 정도의 불안전성을 요구합니다:

#![allow(unused)]
fn main() {
use std::slice::from_raw_parts_mut;
struct FakeSlice<T>(T);
impl<T> FakeSlice<T> {
    fn len(&self) -> usize { unimplemented!() }
    fn as_mut_ptr(&mut self) -> *mut T { unimplemented!() }
    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        let len = self.len();
        let ptr = self.as_mut_ptr();

        unsafe {
            assert!(mid <= len);

            (from_raw_parts_mut(ptr, mid),
             from_raw_parts_mut(ptr.add(mid), len - mid))
        }
    }
}
}

이것은 사실 조금 애매합니다. 같은 값에 두 개의 &mut을 만드는 일이 없도록, 우리는 생 포인터를 통하여 명시적으로 새로운 슬라이스를 만듭니다.

그러나 더욱 애매한 것은 가변 레퍼런스를 반환하는 반복자들이 어떻게 작동하느냐 하는 것입니다. Iterator 트레잇은 다음과 같이 정의되어 있습니다:

#![allow(unused)]
fn main() {
trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
}

이 정의에 의하면, Self::Itemself와 어떤 관련도 없습니다. 이것이 의미하는 것은 우리가 next를 여러 번 연속으로 호출한 후, 결과들을 동시에 기다리는 것이 가능하다는 겁니다. 이것은 값을 넘겨주는 반복자들에게는 아무 영향도 없습니다, 정확히 이런 의미를 가지거든요. 불변 레퍼런스를 반환하는 반복자들에게도 문제 없습니다, 같은 값에 임의의 많은 레퍼런스들이 있는 것을 허용하기 때문이죠 (비록 반복자가 공유되는 값과 다른 객체여야 하지만요).

하지만 가변 레퍼런스들이 이것을 망칩니다. 처음에 보기에는, 이 API와 전혀 호환될 것 같지 않은데, 이는 같은 객체에 여러 개의 가변 레퍼런스를 만들 것이기 때문입니다!

그러나 실제로는 잘 동작하는데, 바로 반복자들이 일회용 객체들이기 때문입니다. IterMut이 반환하는 모든 것은 최대 1번 반환될 것이므로, 우리는 같은 데이터 조각에 여러 개의 가변 레퍼런스를 반환하는 일은 없을 겁니다.

놀라울지는 모르겠지만, 가변 반복자들은 많은 타입들의 경우에 구현하는 데에 불안전한 코드가 필요하지 않습니다!

예를 들어 단일 링크드 리스트입니다:

fn main() {}
type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

pub struct LinkedList<T> {
    head: Link<T>,
}

pub struct IterMut<'a, T: 'a>(Option<&'a mut Node<T>>);

impl<T> LinkedList<T> {
    fn iter_mut(&mut self) -> IterMut<T> {
        IterMut(self.head.as_mut().map(|node| &mut **node))
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        self.0.take().map(|node| {
            self.0 = node.next.as_mut().map(|node| &mut **node);
            &mut node.elem
        })
    }
}

가변 슬라이스입니다:

fn main() {}
use std::mem;

pub struct IterMut<'a, T: 'a>(&'a mut[T]);

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        let slice = mem::take(&mut self.0);
        if slice.is_empty() { return None; }

        let (l, r) = slice.split_at_mut(1);
        self.0 = r;
        l.get_mut(0)
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        let slice = mem::take(&mut self.0);
        if slice.is_empty() { return None; }

        let new_len = slice.len() - 1;
        let (l, r) = slice.split_at_mut(new_len);
        self.0 = l;
        r.get_mut(0)
    }
}

그리고 이진 트리입니다:

fn main() {}
use std::collections::VecDeque;

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    left: Link<T>,
    right: Link<T>,
}

pub struct Tree<T> {
    root: Link<T>,
}

struct NodeIterMut<'a, T: 'a> {
    elem: Option<&'a mut T>,
    left: Option<&'a mut Node<T>>,
    right: Option<&'a mut Node<T>>,
}

enum State<'a, T: 'a> {
    Elem(&'a mut T),
    Node(&'a mut Node<T>),
}

pub struct IterMut<'a, T: 'a>(VecDeque<NodeIterMut<'a, T>>);

impl<T> Tree<T> {
    pub fn iter_mut(&mut self) -> IterMut<T> {
        let mut deque = VecDeque::new();
        self.root.as_mut().map(|root| deque.push_front(root.iter_mut()));
        IterMut(deque)
    }
}

impl<T> Node<T> {
    pub fn iter_mut(&mut self) -> NodeIterMut<T> {
        NodeIterMut {
            elem: Some(&mut self.elem),
            left: self.left.as_mut().map(|node| &mut **node),
            right: self.right.as_mut().map(|node| &mut **node),
        }
    }
}


impl<'a, T> Iterator for NodeIterMut<'a, T> {
    type Item = State<'a, T>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.left.take() {
            Some(node) => Some(State::Node(node)),
            None => match self.elem.take() {
                Some(elem) => Some(State::Elem(elem)),
                None => match self.right.take() {
                    Some(node) => Some(State::Node(node)),
                    None => None,
                }
            }
        }
    }
}

impl<'a, T> DoubleEndedIterator for NodeIterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        match self.right.take() {
            Some(node) => Some(State::Node(node)),
            None => match self.elem.take() {
                Some(elem) => Some(State::Elem(elem)),
                None => match self.left.take() {
                    Some(node) => Some(State::Node(node)),
                    None => None,
                }
            }
        }
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;
    fn next(&mut self) -> Option<Self::Item> {
        loop {
            match self.0.front_mut().and_then(|node_it| node_it.next()) {
                Some(State::Elem(elem)) => return Some(elem),
                Some(State::Node(node)) => self.0.push_front(node.iter_mut()),
                None => if let None = self.0.pop_front() { return None },
            }
        }
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        loop {
            match self.0.back_mut().and_then(|node_it| node_it.next_back()) {
                Some(State::Elem(elem)) => return Some(elem),
                Some(State::Node(node)) => self.0.push_back(node.iter_mut()),
                None => if let None = self.0.pop_back() { return None },
            }
        }
    }
}

이 모든 것들은 완벽하게 안전하고 안정적인 러스트에서 동작합니다! 이것은 우리가 봤던 간단한 구조체의 경우를 넘어섭니다: 러스트는 가변 레퍼런스를 안전하게 그 필드들로 쪼갤 수 있다는 것을 이해합니다. 그 다음 우리는 영구적으로 레퍼런스를 소비하는 코드를 Option으로 짜게 됩니다 (혹은 슬라이스의 경우, 빈 슬라이스로 바꾸는 식으로요).

타입 변환

결국 모든 것은 어딘가에 있는 비트 덩어리일 뿐이고, 타입 시스템은 우리가 그 비트들을 잘 쓰게 하기 위해 존재할 뿐입니다. 비트들에 타입을 입히는 것에는 두 가지 흔한 문제점이 있습니다: 비트들 그대로 다른 타입으로 재해석해야 하는 것과, 다른 타입에서 동일한 의미를 가지도록 비트들을 바꿔야 하는 경우입니다. 러스트가 중요한 속성들을 타입 시스템에 녹여내는 것을 권장하기 때문에, 이런 문제들은 매우 널리 퍼지는 문제들입니다. 따라서 러스트는 이런 종류의 문제들을 해결하는 몇 가지 방법들을 제공합니다.

먼저 우리는 안전한 러스트가 값을 재해석하게 해주는 방법들을 살펴보겠습니다. 가장 흔한 방법은 값을 작은 부분으로 해체시킨 후 그것들로 새 타입을 만드는 것입니다. 예를 들면:

#![allow(unused)]
fn main() {
struct Foo {
    x: u32,
    y: u16,
}

struct Bar {
    a: u32,
    b: u16,
}

fn reinterpret(foo: Foo) -> Bar {
    let Foo { x, y } = foo;
    Bar { a: x, b: y }
}
}

하지만 이것은, 좋게 말해도, 짜증납니다. 흔한 변환들에 있어서 러스트는 좀더 효율적이고 편리한 방식들을 제공합니다.

강제 변환

타입들은 특정한 상황들에서는 묵시적으로 강제 변환될 수 있습니다. 이런 변화들은 일반적으로 타입 시스템을 약화시키는 것인데, 주로 포인터와 수명에 초점이 맞춰져 있습니다. 이런 것들은 주로 많은 상황에서 러스트가 "그냥 잘 작동"하도록 하기 위하여 존재하며, 꽤나 위험성이 없습니다.

모든 강제 변환의 종류에 대해서는 참조서의 강제 변환 타입 섹션을 보세요.

트레잇을 매칭할 때는 강제 변환을 실행하지 않는다는 것을 유의하세요 (수신자를 위한 경우는 제외하고요, 다음 페이지를 보세요). 만약 어떤 타입 U를 위한 impl이 있고 TU로 강제 변환된다면, T를 위한 구현으로 인정되지는 않습니다. 예를 들어 다음의 코드는 타입 검사를 통과하지 못할 텐데, t&T로 강제 변환해도 괜찮고 &T를 위한 impl이 있는데도 그렇습니다:

trait Trait {}

fn foo<X: Trait>(t: X) {}

impl<'a> Trait for &'a i32 {}

fn main() {
    let t: &mut i32 = &mut 0;
    foo(t);
}

이는 다음의 에러를 내뱉습니다:

error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
 --> src/main.rs:9:9
  |
3 | fn foo<X: Trait>(t: X) {}
  |           ----- required by this bound in `foo`
...
9 |     foo(t);
  |         ^ the trait `Trait` is not implemented for `&mut i32`
  |
  = help: the following implementations were found:
            <&'a i32 as Trait>
  = note: `Trait` is implemented for `&i32`, but not for `&mut i32`

점 연산자

점 연산자는 타입을 변환하기 위해 많은 마법을 사용할 겁니다. 타입이 맞을 때까지 자동 레퍼런싱, 자동 역참조, 강제 변환을 수행하겠죠. 메서드 조회의 자세한 작동 방식은 여기에 정의되어 있지만, 기본적인 절차를 여기서 간단하게 설명하겠습니다.

어떤 수신자(self, &self 또는 &mut self 매개변수)가 있는 함수 foo가 있다고 해 봅시다. 만약 우리가 value.foo()를 호출하면, 컴파일러는 이 함수의 올바른 구현을 호출하기 전에 Self가 어떤 타입인지 밝혀내야 합니다. 이 예제에서는 valueT 타입이라고 하겠습니다.

우리는 완전 정식화 문법을 써서 우리가 어떤 타입의 함수를 호출하는지를 명확히 하겠습니다.

  • 먼저 컴파일러는 T::foo(value)를 직접 호출할 수 있는지 검사합니다. 이것은 "값에 의한" 메서드 호출이라 부릅니다.
  • 이 함수를 호출할 수 없다면 (예를 들어 함수가 잘못된 타입을 가진다거나 Self에 대해 트레잇이 구현되지 않았을 경우), 컴파일러는 자동으로 레퍼런스를 추가합니다. 이 말은 컴파일러가 <&T>::foo(value)<&mut T>::foo(value)를 시도해 본다는 뜻입니다. 이것은 자동 레퍼런스 메서드 호출이라 부릅니다.
  • 여기까지의 후보들이 실패했다면 컴파일러는 T를 역참조하여 다시 시도합니다. 이것은 Deref 트레잇을 사용합니다 - 만약 T: Deref<Target = U>라면 T 대신 U가 사용됩니다. 만약 T를 역참조할 수 없다면 T크기 지정 해제를 시도할 수도 있습니다. 이것은 만약 T가 컴파일 시간에 알려진 크기 매개변수가 있다면, 메서드를 찾기 위해 이것을 "잊어버린다는" 말입니다. 예를 들어, 이런 크기 지정 해제 작업은 배열의 크기를 "잊어버림으로써" [i32; 2][i32]로 변환할 수 있습니다.

여기 메서드 조회 알고리즘의 예제가 있습니다:

let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];

배열로 가는 길에 돌아가는 길이 이렇게 많은데 컴파일러는 어떻게 실제 array[0]을 계산할 수 있을까요? 먼저, array[0]은 그냥 Index 트레잇을 위한 문법적 설탕입니다 - 컴파일러는 array[0]array.index(0)으로 변환할 겁니다. 이제, 컴파일러는 함수를 호출하기 위해 arrayIndex를 구현하는지 봅니다.

그럼 컴파일러는 Rc<Box<[T; 3]>>Index를 구현하는지 보는데, 구현하지 않고, &Rc<Box<[T; 3]>>&mut Rc<Box<[T; 3]>>Index를 구현하지 않습니다. 여태까지 아무것도 맞지 않았으니, 컴파일러는 Rc<Box<[T; 3]>>Box<[T; 3]>로 역참조하여 다시 시도합니다. Box<[T; 3]>, &Box<[T; 3]>, 그리고 &mut Box<[T; 3]>Index를 구현하지 않으므로, 컴파일러는 다시 역참조합니다. [T; 3]과 그의 자동 참조들도 Index를 구현하지 않습니다. 컴파일러는 [T; 3]를 역참조할 수 없으므로, 크기 지정을 해제하여, [T]를 얻어냅니다. 마지막으로, [T]Index를 구현하므로, 컴파일러는 실제로 index 함수를 호출할 수 있게 됩니다.

점 연산자가 작동하는 좀더 복잡한 다음의 예제를 생각해 봅시다:

#![allow(unused)]
fn main() {
fn do_stuff<T: Clone>(value: &T) {
    let cloned = value.clone();
}
}

cloned는 어떤 타입일까요? 먼저, 컴파일러는 값으로 호출할 수 있는지 알아봅니다. value의 타입은 &T이고, clone 함수는 fn clone(&T) -> T의 시그니처를 가지고 있습니다. 컴파일러는 T: Clone을 알고 있으니, cloned: T인 것을 찾아냅니다.

만약 T: Clone 제한이 없어졌다면 무슨 일이 일어날까요? T를 위한 Clone 구현이 없으므로, 컴파일러는 값으로 호출하지 못할 것입니다. 따라서 컴파일러는 자동 참조로 호출을 시도합니다. 이 경우에는 Self = &T이므로 함수는 fn clone(&&T) -> &T의 시그니처를 가지게 됩니다. 컴파일러는 &T: Clone을 알아차리고, cloned: &T라고 결론짓습니다.

여기, 자동 참조 동작이 잘 보이지 않는 변화를 만들어내는 데 쓰이는, 다른 예제가 있습니다.

#![allow(unused)]
fn main() {
use std::sync::Arc;

#[derive(Clone)]
struct Container<T>(Arc<T>);

fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
    let foo_cloned = foo.clone();
    let bar_cloned = bar.clone();
}
}

foo_clonedbar_cloned는 어떤 타입일까요? 우리는 Container<i32>: Clone이라는 것을 알기 때문에, 컴파일러는 clone을 값으로 호출하여 foo_cloned: Container<i32>를 얻어냅니다. 그러나, bar_cloned는 실제로는 &Container<T>를 타입으로 가지게 됩니다. 확실히 이것은 말이 되지 않습니다 - 우리는 Container#[derive(Clone)]을 추가했으므로, ContainerClone을 구현해야 합니다! 좀더 가까이 보자면, derive 매크로에 의해 생성된 코드는 (대강) 다음과 같습니다:

impl<T> Clone for Container<T> where T: Clone {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

파생된 Clone 구현은 T: Clone일 때만 정의되어 있으며, 따라서 일반적인 T에 대해 Container<T>: Clone 구현은 없는 것입니다. 그럼 컴파일러는 &Container<T>Clone을 구현하는지 봅니다. &Container<T>Clone을 구현하므로, 컴파일러는 clone이 자동 참조로 호출된다고 결론 내리고, 따라서 bar_cloned&Container<T>의 타입을 갖게 됩니다.

우리는 T: Clone을 요구하지 않는 Clone을 수동으로 구현함으로써 이 문제를 해결할 수 있습니다:

impl<T> Clone for Container<T> {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

이제 타입 검사기는 bar_cloned: Container<T>라고 결론짓습니다.

변형

변형은 강제 변환을 포함하는 더 큰 개념입니다: 모든 강제 변환은 변형으로 명시적으로 호출할 수 있습니다. 하지만 어떤 변환들은 변형을 필요로 합니다. 강제 변환은 흔하고 보통은 위험하지 않지만, 이런 "진짜 변형"은 희귀하고, 위험할 수 있습니다. 그런 면에서, 변형은 명시적으로 as 키워드를 사용해서 호출해야 합니다: expr as Type.

모든 변형과 그 의미들은 참조서에서 전체 목록을 볼 수 있습니다.

변형의 안전성

True casts generally revolve around raw pointers and the primitive numeric types. Even though they're dangerous, these casts are infallible at runtime. If a cast triggers some subtle corner case no indication will be given that this occurred. The cast will simply succeed. That said, casts must be valid at the type level, or else they will be prevented statically. For instance, 7u8 as bool will not compile.

That said, casts aren't unsafe because they generally can't violate memory safety on their own. For instance, converting an integer to a raw pointer can very easily lead to terrible things. However the act of creating the pointer itself is safe, because actually using a raw pointer is already marked as unsafe.

Some notes about casting

Lengths when casting raw slices

Note that lengths are not adjusted when casting raw slices; *const [u16] as *const [u8] creates a slice that only includes half of the original memory.

Transitivity

Casting is not transitive, that is, even if e as U1 as U2 is a valid expression, e as U2 is not necessarily so.

Transmutes

Get out of our way type system! We're going to reinterpret these bits or die trying! Even though this book is all about doing things that are unsafe, I really can't emphasize enough that you should deeply think about finding Another Way than the operations covered in this section. This is really, truly, the most horribly unsafe thing you can do in Rust. The guardrails here are dental floss.

mem::transmute<T, U> takes a value of type T and reinterprets it to have type U. The only restriction is that the T and U are verified to have the same size. The ways to cause Undefined Behavior with this are mind boggling.

  • First and foremost, creating an instance of any type with an invalid state is going to cause arbitrary chaos that can't really be predicted. Do not transmute 3 to bool. Even if you never do anything with the bool. Just don't.

  • Transmute has an overloaded return type. If you do not specify the return type it may produce a surprising type to satisfy inference.

  • Transmuting an & to &mut is Undefined Behavior. While certain usages may appear safe, note that the Rust optimizer is free to assume that a shared reference won't change through its lifetime and thus such transmutation will run afoul of those assumptions. So:

    • Transmuting an & to &mut is always Undefined Behavior.
    • No you can't do it.
    • No you're not special.
  • Transmuting to a reference without an explicitly provided lifetime produces an unbounded lifetime.

  • When transmuting between different compound types, you have to make sure they are laid out the same way! If layouts differ, the wrong fields are going to get filled with the wrong data, which will make you unhappy and can also be Undefined Behavior (see above).

    So how do you know if the layouts are the same? For repr(C) types and repr(transparent) types, layout is precisely defined. But for your run-of-the-mill repr(Rust), it is not. Even different instances of the same generic type can have wildly different layout. Vec<i32> and Vec<u32> might have their fields in the same order, or they might not. The details of what exactly is and is not guaranteed for data layout are still being worked out over at the UCG WG.

mem::transmute_copy<T, U> somehow manages to be even more wildly unsafe than this. It copies size_of<U> bytes out of an &T and interprets them as a U. The size check that mem::transmute has is gone (as it may be valid to copy out a prefix), though it is Undefined Behavior for U to be larger than T.

Also of course you can get all of the functionality of these functions using raw pointer casts or unions, but without any of the lints or other basic sanity checks. Raw pointer casts and unions do not magically avoid the above rules.

Working With Uninitialized Memory

All runtime-allocated memory in a Rust program begins its life as uninitialized. In this state the value of the memory is an indeterminate pile of bits that may or may not even reflect a valid state for the type that is supposed to inhabit that location of memory. Attempting to interpret this memory as a value of any type will cause Undefined Behavior. Do Not Do This.

Rust provides mechanisms to work with uninitialized memory in checked (safe) and unchecked (unsafe) ways.

Checked Uninitialized Memory

Like C, all stack variables in Rust are uninitialized until a value is explicitly assigned to them. Unlike C, Rust statically prevents you from ever reading them until you do:

fn main() {
    let x: i32;
    println!("{}", x);
}
  |
3 |     println!("{}", x);
  |                    ^ use of possibly uninitialized `x`

This is based off of a basic branch analysis: every branch must assign a value to x before it is first used. For short, we also say that "x is init" or "x is uninit".

Interestingly, Rust doesn't require the variable to be mutable to perform a delayed initialization if every branch assigns exactly once. However the analysis does not take advantage of constant analysis or anything like that. So this compiles:

fn main() {
    let x: i32;

    if true {
        x = 1;
    } else {
        x = 2;
    }

    println!("{}", x);
}

but this doesn't:

fn main() {
    let x: i32;
    if true {
        x = 1;
    }
    println!("{}", x);
}
  |
6 |     println!("{}", x);
  |                    ^ use of possibly uninitialized `x`

while this does:

fn main() {
    let x: i32;
    if true {
        x = 1;
        println!("{}", x);
    }
    // Don't care that there are branches where it's not initialized
    // since we don't use the value in those branches
}

Of course, while the analysis doesn't consider actual values, it does have a relatively sophisticated understanding of dependencies and control flow. For instance, this works:

#![allow(unused)]
fn main() {
let x: i32;

loop {
    // Rust doesn't understand that this branch will be taken unconditionally,
    // because it relies on actual values.
    if true {
        // But it does understand that it will only be taken once because
        // we unconditionally break out of it. Therefore `x` doesn't
        // need to be marked as mutable.
        x = 0;
        break;
    }
}
// It also knows that it's impossible to get here without reaching the break.
// And therefore that `x` must be initialized here!
println!("{}", x);
}

If a value is moved out of a variable, that variable becomes logically uninitialized if the type of the value isn't Copy. That is:

fn main() {
    let x = 0;
    let y = Box::new(0);
    let z1 = x; // x is still valid because i32 is Copy
    let z2 = y; // y is now logically uninitialized because Box isn't Copy
}

However reassigning y in this example would require y to be marked as mutable, as a Safe Rust program could observe that the value of y changed:

fn main() {
    let mut y = Box::new(0);
    let z = y; // y is now logically uninitialized because Box isn't Copy
    y = Box::new(1); // reinitialize y
}

Otherwise it's like y is a brand new variable.

Drop Flags

The examples in the previous section introduce an interesting problem for Rust. We have seen that it's possible to conditionally initialize, deinitialize, and reinitialize locations of memory totally safely. For Copy types, this isn't particularly notable since they're just a random pile of bits. However types with destructors are a different story: Rust needs to know whether to call a destructor whenever a variable is assigned to, or a variable goes out of scope. How can it do this with conditional initialization?

Note that this is not a problem that all assignments need worry about. In particular, assigning through a dereference unconditionally drops, and assigning in a let unconditionally doesn't drop:

#![allow(unused)]
fn main() {
let mut x = Box::new(0); // let makes a fresh variable, so never need to drop
let y = &mut x;
*y = Box::new(1); // Deref assumes the referent is initialized, so always drops
}

This is only a problem when overwriting a previously initialized variable or one of its subfields.

It turns out that Rust actually tracks whether a type should be dropped or not at runtime. As a variable becomes initialized and uninitialized, a drop flag for that variable is toggled. When a variable might need to be dropped, this flag is evaluated to determine if it should be dropped.

Of course, it is often the case that a value's initialization state can be statically known at every point in the program. If this is the case, then the compiler can theoretically generate more efficient code! For instance, straight- line code has such static drop semantics:

#![allow(unused)]
fn main() {
let mut x = Box::new(0); // x was uninit; just overwrite.
let mut y = x;           // y was uninit; just overwrite and make x uninit.
x = Box::new(0);         // x was uninit; just overwrite.
y = x;                   // y was init; Drop y, overwrite it, and make x uninit!
                         // y goes out of scope; y was init; Drop y!
                         // x goes out of scope; x was uninit; do nothing.
}

Similarly, branched code where all branches have the same behavior with respect to initialization has static drop semantics:

#![allow(unused)]
fn main() {
let condition = true;
let mut x = Box::new(0);    // x was uninit; just overwrite.
if condition {
    drop(x)                 // x gets moved out; make x uninit.
} else {
    println!("{}", x);
    drop(x)                 // x gets moved out; make x uninit.
}
x = Box::new(0);            // x was uninit; just overwrite.
                            // x goes out of scope; x was init; Drop x!
}

However code like this requires runtime information to correctly Drop:

#![allow(unused)]
fn main() {
let condition = true;
let x;
if condition {
    x = Box::new(0);        // x was uninit; just overwrite.
    println!("{}", x);
}
                            // x goes out of scope; x might be uninit;
                            // check the flag!
}

Of course, in this case it's trivial to retrieve static drop semantics:

#![allow(unused)]
fn main() {
let condition = true;
if condition {
    let x = Box::new(0);
    println!("{}", x);
}
}

The drop flags are tracked on the stack. In old Rust versions, drop flags were stashed in a hidden field of types that implement Drop.

Unchecked Uninitialized Memory

One interesting exception to this rule is working with arrays. Safe Rust doesn't permit you to partially initialize an array. When you initialize an array, you can either set every value to the same thing with let x = [val; N], or you can specify each member individually with let x = [val1, val2, val3]. Unfortunately this is pretty rigid, especially if you need to initialize your array in a more incremental or dynamic way.

Unsafe Rust gives us a powerful tool to handle this problem: MaybeUninit. This type can be used to handle memory that has not been fully initialized yet.

With MaybeUninit, we can initialize an array element by element as follows:

#![allow(unused)]
fn main() {
use std::mem::{self, MaybeUninit};

// Size of the array is hard-coded but easy to change (meaning, changing just
// the constant is sufficient). This means we can't use [a, b, c] syntax to
// initialize the array, though, as we would have to keep that in sync
// with `SIZE`!
const SIZE: usize = 10;

let x = {
    // Create an uninitialized array of `MaybeUninit`. The `assume_init` is
    // safe because the type we are claiming to have initialized here is a
    // bunch of `MaybeUninit`s, which do not require initialization.
    let mut x: [MaybeUninit<Box<u32>>; SIZE] = unsafe {
        MaybeUninit::uninit().assume_init()
    };

    // Dropping a `MaybeUninit` does nothing. Thus using raw pointer
    // assignment instead of `ptr::write` does not cause the old
    // uninitialized value to be dropped.
    // Exception safety is not a concern because Box can't panic
    for i in 0..SIZE {
        x[i] = MaybeUninit::new(Box::new(i as u32));
    }

    // Everything is initialized. Transmute the array to the
    // initialized type.
    unsafe { mem::transmute::<_, [Box<u32>; SIZE]>(x) }
};

dbg!(x);
}

This code proceeds in three steps:

  1. Create an array of MaybeUninit<T>. With current stable Rust, we have to use unsafe code for this: we take some uninitialized piece of memory (MaybeUninit::uninit()) and claim we have fully initialized it (assume_init()). This seems ridiculous, because we didn't! The reason this is correct is that the array consists itself entirely of MaybeUninit, which do not actually require initialization. For most other types, doing MaybeUninit::uninit().assume_init() produces an invalid instance of said type, so you got yourself some Undefined Behavior.

  2. Initialize the array. The subtle aspect of this is that usually, when we use = to assign to a value that the Rust type checker considers to already be initialized (like x[i]), the old value stored on the left-hand side gets dropped. This would be a disaster. However, in this case, the type of the left-hand side is MaybeUninit<Box<u32>>, and dropping that does not do anything! See below for some more discussion of this drop issue.

  3. Finally, we have to change the type of our array to remove the MaybeUninit. With current stable Rust, this requires a transmute. This transmute is legal because in memory, MaybeUninit<T> looks the same as T.

    However, note that in general, Container<MaybeUninit<T>>> does not look the same as Container<T>! Imagine if Container was Option, and T was bool, then Option<bool> exploits that bool only has two valid values, but Option<MaybeUninit<bool>> cannot do that because the bool does not have to be initialized.

    So, it depends on Container whether transmuting away the MaybeUninit is allowed. For arrays, it is (and eventually the standard library will acknowledge that by providing appropriate methods).

It's worth spending a bit more time on the loop in the middle, and in particular the assignment operator and its interaction with drop. If we wrote something like:

*x[i].as_mut_ptr() = Box::new(i as u32); // WRONG!

we would actually overwrite a Box<u32>, leading to drop of uninitialized data, which would cause much sadness and pain.

The correct alternative, if for some reason we cannot use MaybeUninit::new, is to use the ptr module. In particular, it provides three functions that allow us to assign bytes to a location in memory without dropping the old value: write, copy, and copy_nonoverlapping.

  • ptr::write(ptr, val) takes a val and moves it into the address pointed to by ptr.
  • ptr::copy(src, dest, count) copies the bits that count T items would occupy from src to dest. (this is equivalent to C's memmove -- note that the argument order is reversed!)
  • ptr::copy_nonoverlapping(src, dest, count) does what copy does, but a little faster on the assumption that the two ranges of memory don't overlap. (this is equivalent to C's memcpy -- note that the argument order is reversed!)

It should go without saying that these functions, if misused, will cause serious havoc or just straight up Undefined Behavior. The only requirement of these functions themselves is that the locations you want to read and write are allocated and properly aligned. However, the ways writing arbitrary bits to arbitrary locations of memory can break things are basically uncountable!

It's worth noting that you don't need to worry about ptr::write-style shenanigans with types which don't implement Drop or contain Drop types, because Rust knows not to try to drop them. This is what we relied on in the above example.

However when working with uninitialized memory you need to be ever-vigilant for Rust trying to drop values you make like this before they're fully initialized. Every control path through that variable's scope must initialize the value before it ends, if it has a destructor. This includes code panicking. MaybeUninit helps a bit here, because it does not implicitly drop its content - but all this really means in case of a panic is that instead of a double-free of the not yet initialized parts, you end up with a memory leak of the already initialized parts.

Note that, to use the ptr methods, you need to first obtain a raw pointer to the data you want to initialize. It is illegal to construct a reference to uninitialized data, which implies that you have to be careful when obtaining said raw pointer:

  • For an array of T, you can use base_ptr.add(idx) where base_ptr: *mut T to compute the address of array index idx. This relies on how arrays are laid out in memory.
  • For a struct, however, in general we do not know how it is laid out, and we also cannot use &mut base_ptr.field as that would be creating a reference. So, you must carefully use the addr_of_mut macro. This creates a raw pointer to the field without creating an intermediate reference:
#![allow(unused)]
fn main() {
use std::{ptr, mem::MaybeUninit};

struct Demo {
    field: bool,
}

let mut uninit = MaybeUninit::<Demo>::uninit();
// `&uninit.as_mut().field` would create a reference to an uninitialized `bool`,
// and thus be Undefined Behavior!
let f1_ptr = unsafe { ptr::addr_of_mut!((*uninit.as_mut_ptr()).field) };
unsafe { f1_ptr.write(true); }

let init = unsafe { uninit.assume_init() };
}

One last remark: when reading old Rust code, you might stumble upon the deprecated mem::uninitialized function. That function used to be the only way to deal with uninitialized memory on the stack, but it turned out to be impossible to properly integrate with the rest of the language. Always use MaybeUninit instead in new code, and port old code over when you get the opportunity.

And that's about it for working with uninitialized memory! Basically nothing anywhere expects to be handed uninitialized memory, so if you're going to pass it around at all, be sure to be really careful.

The Perils Of Ownership Based Resource Management (OBRM)

OBRM (AKA RAII: Resource Acquisition Is Initialization) is something you'll interact with a lot in Rust. Especially if you use the standard library.

Roughly speaking the pattern is as follows: to acquire a resource, you create an object that manages it. To release the resource, you simply destroy the object, and it cleans up the resource for you. The most common "resource" this pattern manages is simply memory. Box, Rc, and basically everything in std::collections is a convenience to enable correctly managing memory. This is particularly important in Rust because we have no pervasive GC to rely on for memory management. Which is the point, really: Rust is about control. However we are not limited to just memory. Pretty much every other system resource like a thread, file, or socket is exposed through this kind of API.

Constructors

There is exactly one way to create an instance of a user-defined type: name it, and initialize all its fields at once:

#![allow(unused)]
fn main() {
struct Foo {
    a: u8,
    b: u32,
    c: bool,
}

enum Bar {
    X(u32),
    Y(bool),
}

struct Unit;

let foo = Foo { a: 0, b: 1, c: false };
let bar = Bar::X(0);
let empty = Unit;
}

That's it. Every other way you make an instance of a type is just calling a totally vanilla function that does some stuff and eventually bottoms out to The One True Constructor.

Unlike C++, Rust does not come with a slew of built-in kinds of constructor. There are no Copy, Default, Assignment, Move, or whatever constructors. The reasons for this are varied, but it largely boils down to Rust's philosophy of being explicit.

Move constructors are meaningless in Rust because we don't enable types to "care" about their location in memory. Every type must be ready for it to be blindly memcopied to somewhere else in memory. This means pure on-the-stack-but- still-movable intrusive linked lists are simply not happening in Rust (safely).

Assignment and copy constructors similarly don't exist because move semantics are the only semantics in Rust. At most x = y just moves the bits of y into the x variable. Rust does provide two facilities for providing C++'s copy- oriented semantics: Copy and Clone. Clone is our moral equivalent of a copy constructor, but it's never implicitly invoked. You have to explicitly call clone on an element you want to be cloned. Copy is a special case of Clone where the implementation is just "copy the bits". Copy types are implicitly cloned whenever they're moved, but because of the definition of Copy this just means not treating the old copy as uninitialized -- a no-op.

While Rust provides a Default trait for specifying the moral equivalent of a default constructor, it's incredibly rare for this trait to be used. This is because variables aren't implicitly initialized. Default is basically only useful for generic programming. In concrete contexts, a type will provide a static new method for any kind of "default" constructor. This has no relation to new in other languages and has no special meaning. It's just a naming convention.

TODO: talk about "placement new"?

Destructors

What the language does provide is full-blown automatic destructors through the Drop trait, which provides the following method:

fn drop(&mut self);

This method gives the type time to somehow finish what it was doing.

After drop is run, Rust will recursively try to drop all of the fields of self.

This is a convenience feature so that you don't have to write "destructor boilerplate" to drop children. If a struct has no special logic for being dropped other than dropping its children, then it means Drop doesn't need to be implemented at all!

There is no stable way to prevent this behavior in Rust 1.0.

Note that taking &mut self means that even if you could suppress recursive Drop, Rust will prevent you from e.g. moving fields out of self. For most types, this is totally fine.

For instance, a custom implementation of Box might write Drop like this:

#![feature(ptr_internals, allocator_api)]

use std::alloc::{Allocator, Global, GlobalAlloc, Layout};
use std::mem;
use std::ptr::{drop_in_place, NonNull, Unique};

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>())
        }
    }
}
fn main() {}

and this works fine because when Rust goes to drop the ptr field it just sees a Unique that has no actual Drop implementation. Similarly nothing can use-after-free the ptr because when drop exits, it becomes inaccessible.

However this wouldn't work:

#![feature(allocator_api, ptr_internals)]

use std::alloc::{Allocator, Global, GlobalAlloc, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
        }
    }
}

struct SuperBox<T> { my_box: Box<T> }

impl<T> Drop for SuperBox<T> {
    fn drop(&mut self) {
        unsafe {
            // Hyper-optimized: deallocate the box's contents for it
            // without `drop`ing the contents
            let c: NonNull<T> = self.my_box.ptr.into();
            Global.deallocate(c.cast::<u8>(), Layout::new::<T>());
        }
    }
}
fn main() {}

After we deallocate the box's ptr in SuperBox's destructor, Rust will happily proceed to tell the box to Drop itself and everything will blow up with use-after-frees and double-frees.

Note that the recursive drop behavior applies to all structs and enums regardless of whether they implement Drop. Therefore something like

#![allow(unused)]
fn main() {
struct Boxy<T> {
    data1: Box<T>,
    data2: Box<T>,
    info: u32,
}
}

will have its data1 and data2's fields destructors whenever it "would" be dropped, even though it itself doesn't implement Drop. We say that such a type needs Drop, even though it is not itself Drop.

Similarly,

#![allow(unused)]
fn main() {
enum Link {
    Next(Box<Link>),
    None,
}
}

will have its inner Box field dropped if and only if an instance stores the Next variant.

In general this works really nicely because you don't need to worry about adding/removing drops when you refactor your data layout. Still there's certainly many valid use cases for needing to do trickier things with destructors.

The classic safe solution to overriding recursive drop and allowing moving out of Self during drop is to use an Option:

#![feature(allocator_api, ptr_internals)]

use std::alloc::{Allocator, GlobalAlloc, Global, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
        }
    }
}

struct SuperBox<T> { my_box: Option<Box<T>> }

impl<T> Drop for SuperBox<T> {
    fn drop(&mut self) {
        unsafe {
            // Hyper-optimized: deallocate the box's contents for it
            // without `drop`ing the contents. Need to set the `box`
            // field as `None` to prevent Rust from trying to Drop it.
            let my_box = self.my_box.take().unwrap();
            let c: NonNull<T> = my_box.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
            mem::forget(my_box);
        }
    }
}
fn main() {}

However this has fairly odd semantics: you're saying that a field that should always be Some may be None, just because that happens in the destructor. Of course this conversely makes a lot of sense: you can call arbitrary methods on self during the destructor, and this should prevent you from ever doing so after deinitializing the field. Not that it will prevent you from producing any other arbitrarily invalid state in there.

On balance this is an ok choice. Certainly what you should reach for by default. However, in the future we expect there to be a first-class way to announce that a field shouldn't be automatically dropped.

Leaking

Ownership-based resource management is intended to simplify composition. You acquire resources when you create the object, and you release the resources when it gets destroyed. Since destruction is handled for you, it means you can't forget to release the resources, and it happens as soon as possible! Surely this is perfect and all of our problems are solved.

Everything is terrible and we have new and exotic problems to try to solve.

Many people like to believe that Rust eliminates resource leaks. In practice, this is basically true. You would be surprised to see a Safe Rust program leak resources in an uncontrolled way.

However from a theoretical perspective this is absolutely not the case, no matter how you look at it. In the strictest sense, "leaking" is so abstract as to be unpreventable. It's quite trivial to initialize a collection at the start of a program, fill it with tons of objects with destructors, and then enter an infinite event loop that never refers to it. The collection will sit around uselessly, holding on to its precious resources until the program terminates (at which point all those resources would have been reclaimed by the OS anyway).

We may consider a more restricted form of leak: failing to drop a value that is unreachable. Rust also doesn't prevent this. In fact Rust has a function for doing this: mem::forget. This function consumes the value it is passed and then doesn't run its destructor.

In the past mem::forget was marked as unsafe as a sort of lint against using it, since failing to call a destructor is generally not a well-behaved thing to do (though useful for some special unsafe code). However this was generally determined to be an untenable stance to take: there are many ways to fail to call a destructor in safe code. The most famous example is creating a cycle of reference-counted pointers using interior mutability.

It is reasonable for safe code to assume that destructor leaks do not happen, as any program that leaks destructors is probably wrong. However unsafe code cannot rely on destructors to be run in order to be safe. For most types this doesn't matter: if you leak the destructor then the type is by definition inaccessible, so it doesn't matter, right? For instance, if you leak a Box<u8> then you waste some memory but that's hardly going to violate memory-safety.

However where we must be careful with destructor leaks are proxy types. These are types which manage access to a distinct object, but don't actually own it. Proxy objects are quite rare. Proxy objects you'll need to care about are even rarer. However we'll focus on three interesting examples in the standard library:

  • vec::Drain
  • Rc
  • thread::scoped::JoinGuard

Drain

drain is a collections API that moves data out of the container without consuming the container. This enables us to reuse the allocation of a Vec after claiming ownership over all of its contents. It produces an iterator (Drain) that returns the contents of the Vec by-value.

Now, consider Drain in the middle of iteration: some values have been moved out, and others haven't. This means that part of the Vec is now full of logically uninitialized data! We could backshift all the elements in the Vec every time we remove a value, but this would have pretty catastrophic performance consequences.

Instead, we would like Drain to fix the Vec's backing storage when it is dropped. It should run itself to completion, backshift any elements that weren't removed (drain supports subranges), and then fix Vec's len. It's even unwinding-safe! Easy!

Now consider the following:

let mut vec = vec![Box::new(0); 4];

{
    // start draining, vec can no longer be accessed
    let mut drainer = vec.drain(..);

    // pull out two elements and immediately drop them
    drainer.next();
    drainer.next();

    // get rid of drainer, but don't call its destructor
    mem::forget(drainer);
}

// Oops, vec[0] was dropped, we're reading a pointer into free'd memory!
println!("{}", vec[0]);

This is pretty clearly Not Good. Unfortunately, we're kind of stuck between a rock and a hard place: maintaining consistent state at every step has an enormous cost (and would negate any benefits of the API). Failing to maintain consistent state gives us Undefined Behavior in safe code (making the API unsound).

So what can we do? Well, we can pick a trivially consistent state: set the Vec's len to be 0 when we start the iteration, and fix it up if necessary in the destructor. That way, if everything executes like normal we get the desired behavior with minimal overhead. But if someone has the audacity to mem::forget us in the middle of the iteration, all that does is leak even more (and possibly leave the Vec in an unexpected but otherwise consistent state). Since we've accepted that mem::forget is safe, this is definitely safe. We call leaks causing more leaks a leak amplification.

Rc

Rc is an interesting case because at first glance it doesn't appear to be a proxy value at all. After all, it manages the data it points to, and dropping all the Rcs for a value will drop that value. Leaking an Rc doesn't seem like it would be particularly dangerous. It will leave the refcount permanently incremented and prevent the data from being freed or dropped, but that seems just like Box, right?

Nope.

Let's consider a simplified implementation of Rc:

struct Rc<T> {
    ptr: *mut RcBox<T>,
}

struct RcBox<T> {
    data: T,
    ref_count: usize,
}

impl<T> Rc<T> {
    fn new(data: T) -> Self {
        unsafe {
            // Wouldn't it be nice if heap::allocate worked like this?
            let ptr = heap::allocate::<RcBox<T>>();
            ptr::write(ptr, RcBox {
                data,
                ref_count: 1,
            });
            Rc { ptr }
        }
    }

    fn clone(&self) -> Self {
        unsafe {
            (*self.ptr).ref_count += 1;
        }
        Rc { ptr: self.ptr }
    }
}

impl<T> Drop for Rc<T> {
    fn drop(&mut self) {
        unsafe {
            (*self.ptr).ref_count -= 1;
            if (*self.ptr).ref_count == 0 {
                // drop the data and then free it
                ptr::read(self.ptr);
                heap::deallocate(self.ptr);
            }
        }
    }
}

This code contains an implicit and subtle assumption: ref_count can fit in a usize, because there can't be more than usize::MAX Rcs in memory. However this itself assumes that the ref_count accurately reflects the number of Rcs in memory, which we know is false with mem::forget. Using mem::forget we can overflow the ref_count, and then get it down to 0 with outstanding Rcs. Then we can happily use-after-free the inner data. Bad Bad Not Good.

This can be solved by just checking the ref_count and doing something. The standard library's stance is to just abort, because your program has become horribly degenerate. Also oh my gosh it's such a ridiculous corner case.

thread::scoped::JoinGuard

Note: This API has already been removed from std, for more information you may refer issue #24292.

This section remains here because we think this example is still important, regardless of whether it is part of std or not.

The thread::scoped API intended to allow threads to be spawned that reference data on their parent's stack without any synchronization over that data by ensuring the parent joins the thread before any of the shared data goes out of scope.

pub fn scoped<'a, F>(f: F) -> JoinGuard<'a>
    where F: FnOnce() + Send + 'a

Here f is some closure for the other thread to execute. Saying that F: Send + 'a is saying that it closes over data that lives for 'a, and it either owns that data or the data was Sync (implying &data is Send).

Because JoinGuard has a lifetime, it keeps all the data it closes over borrowed in the parent thread. This means the JoinGuard can't outlive the data that the other thread is working on. When the JoinGuard does get dropped it blocks the parent thread, ensuring the child terminates before any of the closed-over data goes out of scope in the parent.

Usage looked like:

let mut data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
{
    let mut guards = vec![];
    for x in &mut data {
        // Move the mutable reference into the closure, and execute
        // it on a different thread. The closure has a lifetime bound
        // by the lifetime of the mutable reference `x` we store in it.
        // The guard that is returned is in turn assigned the lifetime
        // of the closure, so it also mutably borrows `data` as `x` did.
        // This means we cannot access `data` until the guard goes away.
        let guard = thread::scoped(move || {
            *x *= 2;
        });
        // store the thread's guard for later
        guards.push(guard);
    }
    // All guards are dropped here, forcing the threads to join
    // (this thread blocks here until the others terminate).
    // Once the threads join, the borrow expires and the data becomes
    // accessible again in this thread.
}
// data is definitely mutated here.

In principle, this totally works! Rust's ownership system perfectly ensures it! ...except it relies on a destructor being called to be safe.

let mut data = Box::new(0);
{
    let guard = thread::scoped(|| {
        // This is at best a data race. At worst, it's also a use-after-free.
        *data += 1;
    });
    // Because the guard is forgotten, expiring the loan without blocking this
    // thread.
    mem::forget(guard);
}
// So the Box is dropped here while the scoped thread may or may not be trying
// to access it.

Dang. Here the destructor running was pretty fundamental to the API, and it had to be scrapped in favor of a completely different design.

Unwinding

Rust has a tiered error-handling scheme:

  • If something might reasonably be absent, Option is used.
  • If something goes wrong and can reasonably be handled, Result is used.
  • If something goes wrong and cannot reasonably be handled, the thread panics.
  • If something catastrophic happens, the program aborts.

Option and Result are overwhelmingly preferred in most situations, especially since they can be promoted into a panic or abort at the API user's discretion. Panics cause the thread to halt normal execution and unwind its stack, calling destructors as if every function instantly returned.

As of 1.0, Rust is of two minds when it comes to panics. In the long-long-ago, Rust was much more like Erlang. Like Erlang, Rust had lightweight tasks, and tasks were intended to kill themselves with a panic when they reached an untenable state. Unlike an exception in Java or C++, a panic could not be caught at any time. Panics could only be caught by the owner of the task, at which point they had to be handled or that task would itself panic.

Unwinding was important to this story because if a task's destructors weren't called, it would cause memory and other system resources to leak. Since tasks were expected to die during normal execution, this would make Rust very poor for long-running systems!

As the Rust we know today came to be, this style of programming grew out of fashion in the push for less-and-less abstraction. Light-weight tasks were killed in the name of heavy-weight OS threads. Still, on stable Rust as of 1.0 panics can only be caught by the parent thread. This means catching a panic requires spinning up an entire OS thread! This unfortunately stands in conflict to Rust's philosophy of zero-cost abstractions.

There is an API called catch_unwind that enables catching a panic without spawning a thread. Still, we would encourage you to only do this sparingly. In particular, Rust's current unwinding implementation is heavily optimized for the "doesn't unwind" case. If a program doesn't unwind, there should be no runtime cost for the program being ready to unwind. As a consequence, actually unwinding will be more expensive than in e.g. Java. Don't build your programs to unwind under normal circumstances. Ideally, you should only panic for programming errors or extreme problems.

Rust's unwinding strategy is not specified to be fundamentally compatible with any other language's unwinding. As such, unwinding into Rust from another language, or unwinding into another language from Rust is Undefined Behavior. You must absolutely catch any panics at the FFI boundary! What you do at that point is up to you, but something must be done. If you fail to do this, at best, your application will crash and burn. At worst, your application won't crash and burn, and will proceed with completely clobbered state.

Exception Safety

Although programs should use unwinding sparingly, there's a lot of code that can panic. If you unwrap a None, index out of bounds, or divide by 0, your program will panic. On debug builds, every arithmetic operation can panic if it overflows. Unless you are very careful and tightly control what code runs, pretty much everything can unwind, and you need to be ready for it.

Being ready for unwinding is often referred to as exception safety in the broader programming world. In Rust, there are two levels of exception safety that one may concern themselves with:

  • In unsafe code, we must be exception safe to the point of not violating memory safety. We'll call this minimal exception safety.

  • In safe code, it is good to be exception safe to the point of your program doing the right thing. We'll call this maximal exception safety.

As is the case in many places in Rust, Unsafe code must be ready to deal with bad Safe code when it comes to unwinding. Code that transiently creates unsound states must be careful that a panic does not cause that state to be used. Generally this means ensuring that only non-panicking code is run while these states exist, or making a guard that cleans up the state in the case of a panic. This does not necessarily mean that the state a panic witnesses is a fully coherent state. We need only guarantee that it's a safe state.

Most Unsafe code is leaf-like, and therefore fairly easy to make exception-safe. It controls all the code that runs, and most of that code can't panic. However it is not uncommon for Unsafe code to work with arrays of temporarily uninitialized data while repeatedly invoking caller-provided code. Such code needs to be careful and consider exception safety.

Vec::push_all

Vec::push_all is a temporary hack to get extending a Vec by a slice reliably efficient without specialization. Here's a simple implementation:

impl<T: Clone> Vec<T> {
    fn push_all(&mut self, to_push: &[T]) {
        self.reserve(to_push.len());
        unsafe {
            // can't overflow because we just reserved this
            self.set_len(self.len() + to_push.len());

            for (i, x) in to_push.iter().enumerate() {
                self.ptr().add(i).write(x.clone());
            }
        }
    }
}

We bypass push in order to avoid redundant capacity and len checks on the Vec that we definitely know has capacity. The logic is totally correct, except there's a subtle problem with our code: it's not exception-safe! set_len, add, and write are all fine; clone is the panic bomb we over-looked.

Clone is completely out of our control, and is totally free to panic. If it does, our function will exit early with the length of the Vec set too large. If the Vec is looked at or dropped, uninitialized memory will be read!

The fix in this case is fairly simple. If we want to guarantee that the values we did clone are dropped, we can set the len every loop iteration. If we just want to guarantee that uninitialized memory can't be observed, we can set the len after the loop.

BinaryHeap::sift_up

Bubbling an element up a heap is a bit more complicated than extending a Vec. The pseudocode is as follows:

bubble_up(heap, index):
    while index != 0 && heap[index] < heap[parent(index)]:
        heap.swap(index, parent(index))
        index = parent(index)

A literal transcription of this code to Rust is totally fine, but has an annoying performance characteristic: the self element is swapped over and over again uselessly. We would rather have the following:

bubble_up(heap, index):
    let elem = heap[index]
    while index != 0 && elem < heap[parent(index)]:
        heap[index] = heap[parent(index)]
        index = parent(index)
    heap[index] = elem

This code ensures that each element is copied as little as possible (it is in fact necessary that elem be copied twice in general). However it now exposes some exception safety trouble! At all times, there exists two copies of one value. If we panic in this function something will be double-dropped. Unfortunately, we also don't have full control of the code: that comparison is user-defined!

Unlike Vec, the fix isn't as easy here. One option is to break the user-defined code and the unsafe code into two separate phases:

bubble_up(heap, index):
    let end_index = index;
    while end_index != 0 && heap[end_index] < heap[parent(end_index)]:
        end_index = parent(end_index)

    let elem = heap[index]
    while index != end_index:
        heap[index] = heap[parent(index)]
        index = parent(index)
    heap[index] = elem

If the user-defined code blows up, that's no problem anymore, because we haven't actually touched the state of the heap yet. Once we do start messing with the heap, we're working with only data and functions that we trust, so there's no concern of panics.

Perhaps you're not happy with this design. Surely it's cheating! And we have to do the complex heap traversal twice! Alright, let's bite the bullet. Let's intermix untrusted and unsafe code for reals.

If Rust had try and finally like in Java, we could do the following:

bubble_up(heap, index):
    let elem = heap[index]
    try:
        while index != 0 && elem < heap[parent(index)]:
            heap[index] = heap[parent(index)]
            index = parent(index)
    finally:
        heap[index] = elem

The basic idea is simple: if the comparison panics, we just toss the loose element in the logically uninitialized index and bail out. Anyone who observes the heap will see a potentially inconsistent heap, but at least it won't cause any double-drops! If the algorithm terminates normally, then this operation happens to coincide precisely with how we finish up regardless.

Sadly, Rust has no such construct, so we're going to need to roll our own! The way to do this is to store the algorithm's state in a separate struct with a destructor for the "finally" logic. Whether we panic or not, that destructor will run and clean up after us.

struct Hole<'a, T: 'a> {
    data: &'a mut [T],
    /// `elt` is always `Some` from new until drop.
    elt: Option<T>,
    pos: usize,
}

impl<'a, T> Hole<'a, T> {
    fn new(data: &'a mut [T], pos: usize) -> Self {
        unsafe {
            let elt = ptr::read(&data[pos]);
            Hole {
                data,
                elt: Some(elt),
                pos,
            }
        }
    }

    fn pos(&self) -> usize { self.pos }

    fn removed(&self) -> &T { self.elt.as_ref().unwrap() }

    fn get(&self, index: usize) -> &T { &self.data[index] }

    unsafe fn move_to(&mut self, index: usize) {
        let index_ptr: *const _ = &self.data[index];
        let hole_ptr = &mut self.data[self.pos];
        ptr::copy_nonoverlapping(index_ptr, hole_ptr, 1);
        self.pos = index;
    }
}

impl<'a, T> Drop for Hole<'a, T> {
    fn drop(&mut self) {
        // fill the hole again
        unsafe {
            let pos = self.pos;
            ptr::write(&mut self.data[pos], self.elt.take().unwrap());
        }
    }
}

impl<T: Ord> BinaryHeap<T> {
    fn sift_up(&mut self, pos: usize) {
        unsafe {
            // Take out the value at `pos` and create a hole.
            let mut hole = Hole::new(&mut self.data, pos);

            while hole.pos() != 0 {
                let parent = parent(hole.pos());
                if hole.removed() <= hole.get(parent) { break }
                hole.move_to(parent);
            }
            // Hole will be unconditionally filled here; panic or not!
        }
    }
}

Poisoning

Although all unsafe code must ensure it has minimal exception safety, not all types ensure maximal exception safety. Even if the type does, your code may ascribe additional meaning to it. For instance, an integer is certainly exception-safe, but has no semantics on its own. It's possible that code that panics could fail to correctly update the integer, producing an inconsistent program state.

This is usually fine, because anything that witnesses an exception is about to get destroyed. For instance, if you send a Vec to another thread and that thread panics, it doesn't matter if the Vec is in a weird state. It will be dropped and go away forever. However some types are especially good at smuggling values across the panic boundary.

These types may choose to explicitly poison themselves if they witness a panic. Poisoning doesn't entail anything in particular. Generally it just means preventing normal usage from proceeding. The most notable example of this is the standard library's Mutex type. A Mutex will poison itself if one of its MutexGuards (the thing it returns when a lock is obtained) is dropped during a panic. Any future attempts to lock the Mutex will return an Err or panic.

Mutex poisons not for true safety in the sense that Rust normally cares about. It poisons as a safety-guard against blindly using the data that comes out of a Mutex that has witnessed a panic while locked. The data in such a Mutex was likely in the middle of being modified, and as such may be in an inconsistent or incomplete state. It is important to note that one cannot violate memory safety with such a type if it is correctly written. After all, it must be minimally exception-safe!

However if the Mutex contained, say, a BinaryHeap that does not actually have the heap property, it's unlikely that any code that uses it will do what the author intended. As such, the program should not proceed normally. Still, if you're double-plus-sure that you can do something with the value, the Mutex exposes a method to get the lock anyway. It is safe, after all. Just maybe nonsense.

Concurrency and Parallelism

Rust as a language doesn't really have an opinion on how to do concurrency or parallelism. The standard library exposes OS threads and blocking sys-calls because everyone has those, and they're uniform enough that you can provide an abstraction over them in a relatively uncontroversial way. Message passing, green threads, and async APIs are all diverse enough that any abstraction over them tends to involve trade-offs that we weren't willing to commit to for 1.0.

However the way Rust models concurrency makes it relatively easy to design your own concurrency paradigm as a library and have everyone else's code Just Work with yours. Just require the right lifetimes and Send and Sync where appropriate and you're off to the races. Or rather, off to the... not... having... races.

Data Races and Race Conditions

Safe Rust guarantees an absence of data races, which are defined as:

  • two or more threads concurrently accessing a location of memory
  • one or more of them is a write
  • one or more of them is unsynchronized

A data race has Undefined Behavior, and is therefore impossible to perform in Safe Rust. Data races are mostly prevented through Rust's ownership system: it's impossible to alias a mutable reference, so it's impossible to perform a data race. Interior mutability makes this more complicated, which is largely why we have the Send and Sync traits (see the next section for more on this).

However Rust does not prevent general race conditions.

This is mathematically impossible in situations where you do not control the scheduler, which is true for the normal OS environment. If you do control preemption, it can be possible to prevent general races - this technique is used by frameworks such as RTIC. However, actually having control over scheduling is a very uncommon case.

For this reason, it is considered "safe" for Rust to get deadlocked or do something nonsensical with incorrect synchronization: this is known as a general race condition or resource race. Obviously such a program isn't very good, but Rust of course cannot prevent all logic errors.

In any case, a race condition cannot violate memory safety in a Rust program on its own. Only in conjunction with some other unsafe code can a race condition actually violate memory safety. For instance, a correct program looks like this:

#![allow(unused)]
fn main() {
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

let data = vec![1, 2, 3, 4];
// Arc so that the memory the AtomicUsize is stored in still exists for
// the other thread to increment, even if we completely finish executing
// before it. Rust won't compile the program without it, because of the
// lifetime requirements of thread::spawn!
let idx = Arc::new(AtomicUsize::new(0));
let other_idx = idx.clone();

// `move` captures other_idx by-value, moving it into this thread
thread::spawn(move || {
    // It's ok to mutate idx because this value
    // is an atomic, so it can't cause a Data Race.
    other_idx.fetch_add(10, Ordering::SeqCst);
});

// Index with the value loaded from the atomic. This is safe because we
// read the atomic memory only once, and then pass a copy of that value
// to the Vec's indexing implementation. This indexing will be correctly
// bounds checked, and there's no chance of the value getting changed
// in the middle. However our program may panic if the thread we spawned
// managed to increment before this ran. A race condition because correct
// program execution (panicking is rarely correct) depends on order of
// thread execution.
println!("{}", data[idx.load(Ordering::SeqCst)]);
}

We can cause a data race if we instead do the bound check in advance, and then unsafely access the data with an unchecked value:

#![allow(unused)]
fn main() {
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

let data = vec![1, 2, 3, 4];

let idx = Arc::new(AtomicUsize::new(0));
let other_idx = idx.clone();

// `move` captures other_idx by-value, moving it into this thread
thread::spawn(move || {
    // It's ok to mutate idx because this value
    // is an atomic, so it can't cause a Data Race.
    other_idx.fetch_add(10, Ordering::SeqCst);
});

if idx.load(Ordering::SeqCst) < data.len() {
    unsafe {
        // Incorrectly loading the idx after we did the bounds check.
        // It could have changed. This is a race condition, *and dangerous*
        // because we decided to do `get_unchecked`, which is `unsafe`.
        println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst)));
    }
}
}

Send and Sync

Not everything obeys inherited mutability, though. Some types allow you to have multiple aliases of a location in memory while mutating it. Unless these types use synchronization to manage this access, they are absolutely not thread-safe. Rust captures this through the Send and Sync traits.

  • A type is Send if it is safe to send it to another thread.
  • A type is Sync if it is safe to share between threads (T is Sync if and only if &T is Send).

Send and Sync are fundamental to Rust's concurrency story. As such, a substantial amount of special tooling exists to make them work right. First and foremost, they're unsafe traits. This means that they are unsafe to implement, and other unsafe code can assume that they are correctly implemented. Since they're marker traits (they have no associated items like methods), correctly implemented simply means that they have the intrinsic properties an implementor should have. Incorrectly implementing Send or Sync can cause Undefined Behavior.

Send and Sync are also automatically derived traits. This means that, unlike every other trait, if a type is composed entirely of Send or Sync types, then it is Send or Sync. Almost all primitives are Send and Sync, and as a consequence pretty much all types you'll ever interact with are Send and Sync.

Major exceptions include:

  • raw pointers are neither Send nor Sync (because they have no safety guards).
  • UnsafeCell isn't Sync (and therefore Cell and RefCell aren't).
  • Rc isn't Send or Sync (because the refcount is shared and unsynchronized).

Rc and UnsafeCell are very fundamentally not thread-safe: they enable unsynchronized shared mutable state. However raw pointers are, strictly speaking, marked as thread-unsafe as more of a lint. Doing anything useful with a raw pointer requires dereferencing it, which is already unsafe. In that sense, one could argue that it would be "fine" for them to be marked as thread safe.

However it's important that they aren't thread-safe to prevent types that contain them from being automatically marked as thread-safe. These types have non-trivial untracked ownership, and it's unlikely that their author was necessarily thinking hard about thread safety. In the case of Rc, we have a nice example of a type that contains a *mut that is definitely not thread-safe.

Types that aren't automatically derived can simply implement them if desired:

#![allow(unused)]
fn main() {
struct MyBox(*mut u8);

unsafe impl Send for MyBox {}
unsafe impl Sync for MyBox {}
}

In the incredibly rare case that a type is inappropriately automatically derived to be Send or Sync, then one can also unimplement Send and Sync:

#![allow(unused)]
#![feature(negative_impls)]

fn main() {
// I have some magic semantics for some synchronization primitive!
struct SpecialThreadToken(u8);

impl !Send for SpecialThreadToken {}
impl !Sync for SpecialThreadToken {}
}

Note that in and of itself it is impossible to incorrectly derive Send and Sync. Only types that are ascribed special meaning by other unsafe code can possibly cause trouble by being incorrectly Send or Sync.

Most uses of raw pointers should be encapsulated behind a sufficient abstraction that Send and Sync can be derived. For instance all of Rust's standard collections are Send and Sync (when they contain Send and Sync types) in spite of their pervasive use of raw pointers to manage allocations and complex ownership. Similarly, most iterators into these collections are Send and Sync because they largely behave like an & or &mut into the collection.

Example

Box is implemented as its own special intrinsic type by the compiler for various reasons, but we can implement something with similar-ish behavior ourselves to see an example of when it is sound to implement Send and Sync. Let's call it a Carton.

We start by writing code to take a value allocated on the stack and transfer it to the heap.

#![allow(unused)]
fn main() {
pub mod libc {
   pub use ::std::os::raw::{c_int, c_void};
   #[allow(non_camel_case_types)]
   pub type size_t = usize;
   extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; }
}
use std::{
    mem::{align_of, size_of},
    ptr,
    cmp::max,
};

struct Carton<T>(ptr::NonNull<T>);

impl<T> Carton<T> {
    pub fn new(value: T) -> Self {
        // Allocate enough memory on the heap to store one T.
        assert_ne!(size_of::<T>(), 0, "Zero-sized types are out of the scope of this example");
        let mut memptr: *mut T = ptr::null_mut();
        unsafe {
            let ret = libc::posix_memalign(
                (&mut memptr as *mut *mut T).cast(),
                max(align_of::<T>(), size_of::<usize>()),
                size_of::<T>()
            );
            assert_eq!(ret, 0, "Failed to allocate or invalid alignment");
        };

        // NonNull is just a wrapper that enforces that the pointer isn't null.
        let ptr = {
            // Safety: memptr is dereferenceable because we created it from a
            // reference and have exclusive access.
            ptr::NonNull::new(memptr)
                .expect("Guaranteed non-null if posix_memalign returns 0")
        };

        // Move value from the stack to the location we allocated on the heap.
        unsafe {
            // Safety: If non-null, posix_memalign gives us a ptr that is valid
            // for writes and properly aligned.
            ptr.as_ptr().write(value);
        }

        Self(ptr)
    }
}
}

This isn't very useful, because once our users give us a value they have no way to access it. Box implements Deref and DerefMut so that you can access the inner value. Let's do that.

#![allow(unused)]
fn main() {
use std::ops::{Deref, DerefMut};

struct Carton<T>(std::ptr::NonNull<T>);

impl<T> Deref for Carton<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        unsafe {
            // Safety: The pointer is aligned, initialized, and dereferenceable
            //   by the logic in [`Self::new`]. We require readers to borrow the
            //   Carton, and the lifetime of the return value is elided to the
            //   lifetime of the input. This means the borrow checker will
            //   enforce that no one can mutate the contents of the Carton until
            //   the reference returned is dropped.
            self.0.as_ref()
        }
    }
}

impl<T> DerefMut for Carton<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe {
            // Safety: The pointer is aligned, initialized, and dereferenceable
            //   by the logic in [`Self::new`]. We require writers to mutably
            //   borrow the Carton, and the lifetime of the return value is
            //   elided to the lifetime of the input. This means the borrow
            //   checker will enforce that no one else can access the contents
            //   of the Carton until the mutable reference returned is dropped.
            self.0.as_mut()
        }
    }
}
}

Finally, let's think about whether our Carton is Send and Sync. Something can safely be Send unless it shares mutable state with something else without enforcing exclusive access to it. Each Carton has a unique pointer, so we're good.

#![allow(unused)]
fn main() {
struct Carton<T>(std::ptr::NonNull<T>);
// Safety: No one besides us has the raw pointer, so we can safely transfer the
// Carton to another thread if T can be safely transferred.
unsafe impl<T> Send for Carton<T> where T: Send {}
}

What about Sync? For Carton to be Sync we have to enforce that you can't write to something stored in a &Carton while that same something could be read or written to from another &Carton. Since you need an &mut Carton to write to the pointer, and the borrow checker enforces that mutable references must be exclusive, there are no soundness issues making Carton sync either.

#![allow(unused)]
fn main() {
struct Carton<T>(std::ptr::NonNull<T>);
// Safety: Since there exists a public way to go from a `&Carton<T>` to a `&T`
// in an unsynchronized fashion (such as `Deref`), then `Carton<T>` can't be
// `Sync` if `T` isn't.
// Conversely, `Carton` itself does not use any interior mutability whatsoever:
// all the mutations are performed through an exclusive reference (`&mut`). This
// means it suffices that `T` be `Sync` for `Carton<T>` to be `Sync`:
unsafe impl<T> Sync for Carton<T> where T: Sync  {}
}

When we assert our type is Send and Sync we usually need to enforce that every contained type is Send and Sync. When writing custom types that behave like standard library types we can assert that we have the same requirements. For example, the following code asserts that a Carton is Send if the same sort of Box would be Send, which in this case is the same as saying T is Send.

#![allow(unused)]
fn main() {
struct Carton<T>(std::ptr::NonNull<T>);
unsafe impl<T> Send for Carton<T> where Box<T>: Send {}
}

Right now Carton<T> has a memory leak, as it never frees the memory it allocates. Once we fix that we have a new requirement we have to ensure we meet to be Send: we need to know free can be called on a pointer that was yielded by an allocation done on another thread. We can check this is true in the docs for libc::free.

#![allow(unused)]
fn main() {
struct Carton<T>(std::ptr::NonNull<T>);
mod libc {
    pub use ::std::os::raw::c_void;
    extern "C" { pub fn free(p: *mut c_void); }
}
impl<T> Drop for Carton<T> {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.0.as_ptr().cast());
        }
    }
}
}

A nice example where this does not happen is with a MutexGuard: notice how it is not Send. The implementation of MutexGuard uses libraries that require you to ensure you don't try to free a lock that you acquired in a different thread. If you were able to Send a MutexGuard to another thread the destructor would run in the thread you sent it to, violating the requirement. MutexGuard can still be Sync because all you can send to another thread is an &MutexGuard and dropping a reference does nothing.

TODO: better explain what can or can't be Send or Sync. Sufficient to appeal only to data races?

Atomics

Rust pretty blatantly just inherits the memory model for atomics from C++20. This is not due to this model being particularly excellent or easy to understand. Indeed, this model is quite complex and known to have several flaws. Rather, it is a pragmatic concession to the fact that everyone is pretty bad at modeling atomics. At very least, we can benefit from existing tooling and research around the C/C++ memory model. (You'll often see this model referred to as "C/C++11" or just "C11". C just copies the C++ memory model; and C++11 was the first version of the model but it has received some bugfixes since then.)

Trying to fully explain the model in this book is fairly hopeless. It's defined in terms of madness-inducing causality graphs that require a full book to properly understand in a practical way. If you want all the nitty-gritty details, you should check out the C++ specification. Still, we'll try to cover the basics and some of the problems Rust developers face.

The C++ memory model is fundamentally about trying to bridge the gap between the semantics we want, the optimizations compilers want, and the inconsistent chaos our hardware wants. We would like to just write programs and have them do exactly what we said but, you know, fast. Wouldn't that be great?

Compiler Reordering

Compilers fundamentally want to be able to do all sorts of complicated transformations to reduce data dependencies and eliminate dead code. In particular, they may radically change the actual order of events, or make events never occur! If we write something like:

x = 1;
y = 3;
x = 2;

The compiler may conclude that it would be best if your program did:

x = 2;
y = 3;

This has inverted the order of events and completely eliminated one event. From a single-threaded perspective this is completely unobservable: after all the statements have executed we are in exactly the same state. But if our program is multi-threaded, we may have been relying on x to actually be assigned to 1 before y was assigned. We would like the compiler to be able to make these kinds of optimizations, because they can seriously improve performance. On the other hand, we'd also like to be able to depend on our program doing the thing we said.

Hardware Reordering

On the other hand, even if the compiler totally understood what we wanted and respected our wishes, our hardware might instead get us in trouble. Trouble comes from CPUs in the form of memory hierarchies. There is indeed a global shared memory space somewhere in your hardware, but from the perspective of each CPU core it is so very far away and so very slow. Each CPU would rather work with its local cache of the data and only go through all the anguish of talking to shared memory only when it doesn't actually have that memory in cache.

After all, that's the whole point of the cache, right? If every read from the cache had to run back to shared memory to double check that it hadn't changed, what would the point be? The end result is that the hardware doesn't guarantee that events that occur in some order on one thread, occur in the same order on another thread. To guarantee this, we must issue special instructions to the CPU telling it to be a bit less smart.

For instance, say we convince the compiler to emit this logic:

initial state: x = 0, y = 1

THREAD 1        THREAD 2
y = 3;          if x == 1 {
x = 1;              y *= 2;
                }

Ideally this program has 2 possible final states:

  • y = 3: (thread 2 did the check before thread 1 completed)
  • y = 6: (thread 2 did the check after thread 1 completed)

However there's a third potential state that the hardware enables:

  • y = 2: (thread 2 saw x = 1, but not y = 3, and then overwrote y = 3)

It's worth noting that different kinds of CPU provide different guarantees. It is common to separate hardware into two categories: strongly-ordered and weakly-ordered. Most notably x86/64 provides strong ordering guarantees, while ARM provides weak ordering guarantees. This has two consequences for concurrent programming:

  • Asking for stronger guarantees on strongly-ordered hardware may be cheap or even free because they already provide strong guarantees unconditionally. Weaker guarantees may only yield performance wins on weakly-ordered hardware.

  • Asking for guarantees that are too weak on strongly-ordered hardware is more likely to happen to work, even though your program is strictly incorrect. If possible, concurrent algorithms should be tested on weakly-ordered hardware.

Data Accesses

The C++ memory model attempts to bridge the gap by allowing us to talk about the causality of our program. Generally, this is by establishing a happens before relationship between parts of the program and the threads that are running them. This gives the hardware and compiler room to optimize the program more aggressively where a strict happens-before relationship isn't established, but forces them to be more careful where one is established. The way we communicate these relationships are through data accesses and atomic accesses.

Data accesses are the bread-and-butter of the programming world. They are fundamentally unsynchronized and compilers are free to aggressively optimize them. In particular, data accesses are free to be reordered by the compiler on the assumption that the program is single-threaded. The hardware is also free to propagate the changes made in data accesses to other threads as lazily and inconsistently as it wants. Most critically, data accesses are how data races happen. Data accesses are very friendly to the hardware and compiler, but as we've seen they offer awful semantics to try to write synchronized code with. Actually, that's too weak.

It is literally impossible to write correct synchronized code using only data accesses.

Atomic accesses are how we tell the hardware and compiler that our program is multi-threaded. Each atomic access can be marked with an ordering that specifies what kind of relationship it establishes with other accesses. In practice, this boils down to telling the compiler and hardware certain things they can't do. For the compiler, this largely revolves around re-ordering of instructions. For the hardware, this largely revolves around how writes are propagated to other threads. The set of orderings Rust exposes are:

  • Sequentially Consistent (SeqCst)
  • Release
  • Acquire
  • Relaxed

(Note: We explicitly do not expose the C++ consume ordering)

TODO: negative reasoning vs positive reasoning? TODO: "can't forget to synchronize"

Sequentially Consistent

Sequentially Consistent is the most powerful of all, implying the restrictions of all other orderings. Intuitively, a sequentially consistent operation cannot be reordered: all accesses on one thread that happen before and after a SeqCst access stay before and after it. A data-race-free program that uses only sequentially consistent atomics and data accesses has the very nice property that there is a single global execution of the program's instructions that all threads agree on. This execution is also particularly nice to reason about: it's just an interleaving of each thread's individual executions. This does not hold if you start using the weaker atomic orderings.

The relative developer-friendliness of sequential consistency doesn't come for free. Even on strongly-ordered platforms sequential consistency involves emitting memory fences.

In practice, sequential consistency is rarely necessary for program correctness. However sequential consistency is definitely the right choice if you're not confident about the other memory orders. Having your program run a bit slower than it needs to is certainly better than it running incorrectly! It's also mechanically trivial to downgrade atomic operations to have a weaker consistency later on. Just change SeqCst to Relaxed and you're done! Of course, proving that this transformation is correct is a whole other matter.

Acquire-Release

Acquire and Release are largely intended to be paired. Their names hint at their use case: they're perfectly suited for acquiring and releasing locks, and ensuring that critical sections don't overlap.

Intuitively, an acquire access ensures that every access after it stays after it. However operations that occur before an acquire are free to be reordered to occur after it. Similarly, a release access ensures that every access before it stays before it. However operations that occur after a release are free to be reordered to occur before it.

When thread A releases a location in memory and then thread B subsequently acquires the same location in memory, causality is established. Every write (including non-atomic and relaxed atomic writes) that happened before A's release will be observed by B after its acquisition. However no causality is established with any other threads. Similarly, no causality is established if A and B access different locations in memory.

Basic use of release-acquire is therefore simple: you acquire a location of memory to begin the critical section, and then release that location to end it. For instance, a simple spinlock might look like:

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;

fn main() {
    let lock = Arc::new(AtomicBool::new(false)); // value answers "am I locked?"

    // ... distribute lock to threads somehow ...

    // Try to acquire the lock by setting it to true
    while lock.compare_and_swap(false, true, Ordering::Acquire) { }
    // broke out of the loop, so we successfully acquired the lock!

    // ... scary data accesses ...

    // ok we're done, release the lock
    lock.store(false, Ordering::Release);
}

On strongly-ordered platforms most accesses have release or acquire semantics, making release and acquire often totally free. This is not the case on weakly-ordered platforms.

Relaxed

Relaxed accesses are the absolute weakest. They can be freely re-ordered and provide no happens-before relationship. Still, relaxed operations are still atomic. That is, they don't count as data accesses and any read-modify-write operations done to them occur atomically. Relaxed operations are appropriate for things that you definitely want to happen, but don't particularly otherwise care about. For instance, incrementing a counter can be safely done by multiple threads using a relaxed fetch_add if you're not using the counter to synchronize any other accesses.

There's rarely a benefit in making an operation relaxed on strongly-ordered platforms, since they usually provide release-acquire semantics anyway. However relaxed operations can be cheaper on weakly-ordered platforms.

Example: Implementing Vec

To bring everything together, we're going to write std::Vec from scratch. We will limit ourselves to stable Rust. In particular we won't use any intrinsics that could make our code a little bit nicer or efficient because intrinsics are permanently unstable. Although many intrinsics do become stabilized elsewhere (std::ptr and std::mem consist of many intrinsics).

Ultimately this means our implementation may not take advantage of all possible optimizations, though it will be by no means naive. We will definitely get into the weeds over nitty-gritty details, even when the problem doesn't really merit it.

You wanted advanced. We're gonna go advanced.

Layout

First off, we need to come up with the struct layout. A Vec has three parts: a pointer to the allocation, the size of the allocation, and the number of elements that have been initialized.

Naively, this means we just want this design:

pub struct Vec<T> {
    ptr: *mut T,
    cap: usize,
    len: usize,
}

And indeed this would compile. Unfortunately, it would be too strict. The compiler will give us too strict variance. So a &Vec<&'static str> couldn't be used where a &Vec<&'a str> was expected. See the chapter on ownership and lifetimes for all the details on variance.

As we saw in the ownership chapter, the standard library uses Unique<T> in place of *mut T when it has a raw pointer to an allocation that it owns. Unique is unstable, so we'd like to not use it if possible, though.

As a recap, Unique is a wrapper around a raw pointer that declares that:

  • We are covariant over T
  • We may own a value of type T (this is not relevant for our example here, but see the chapter on PhantomData on why the real std::vec::Vec<T> needs this)
  • We are Send/Sync if T is Send/Sync
  • Our pointer is never null (so Option<Vec<T>> is null-pointer-optimized)

We can implement all of the above requirements in stable Rust. To do this, instead of using Unique<T> we will use NonNull<T>, another wrapper around a raw pointer, which gives us two of the above properties, namely it is covariant over T and is declared to never be null. By implementing Send/Sync if T is, we get the same results as using Unique<T>:

use std::ptr::NonNull;

pub struct Vec<T> {
    ptr: NonNull<T>,
    cap: usize,
    len: usize,
}

unsafe impl<T: Send> Send for Vec<T> {}
unsafe impl<T: Sync> Sync for Vec<T> {}
fn main() {}

Allocating Memory

Using NonNull throws a wrench in an important feature of Vec (and indeed all of the std collections): creating an empty Vec doesn't actually allocate at all. This is not the same as allocating a zero-sized memory block, which is not allowed by the global allocator (it results in undefined behavior!). So if we can't allocate, but also can't put a null pointer in ptr, what do we do in Vec::new? Well, we just put some other garbage in there!

This is perfectly fine because we already have cap == 0 as our sentinel for no allocation. We don't even need to handle it specially in almost any code because we usually need to check if cap > len or len > 0 anyway. The recommended Rust value to put here is mem::align_of::<T>(). NonNull provides a convenience for this: NonNull::dangling(). There are quite a few places where we'll want to use dangling because there's no real allocation to talk about but null would make the compiler do bad things.

So:

use std::mem;

impl<T> Vec<T> {
    pub fn new() -> Self {
        assert!(mem::size_of::<T>() != 0, "We're not ready to handle ZSTs");
        Vec {
            ptr: NonNull::dangling(),
            len: 0,
            cap: 0,
        }
    }
}
fn main() {}

I slipped in that assert there because zero-sized types will require some special handling throughout our code, and I want to defer the issue for now. Without this assert, some of our early drafts will do some Very Bad Things.

Next we need to figure out what to actually do when we do want space. For that, we use the global allocation functions alloc, realloc, and dealloc which are available in stable Rust in std::alloc. These functions are expected to become deprecated in favor of the methods of std::alloc::Global after this type is stabilized.

We'll also need a way to handle out-of-memory (OOM) conditions. The standard library provides a function alloc::handle_alloc_error, which will abort the program in a platform-specific manner. The reason we abort and don't panic is because unwinding can cause allocations to happen, and that seems like a bad thing to do when your allocator just came back with "hey I don't have any more memory".

Of course, this is a bit silly since most platforms don't actually run out of memory in a conventional way. Your operating system will probably kill the application by another means if you legitimately start using up all the memory. The most likely way we'll trigger OOM is by just asking for ludicrous quantities of memory at once (e.g. half the theoretical address space). As such it's probably fine to panic and nothing bad will happen. Still, we're trying to be like the standard library as much as possible, so we'll just kill the whole program.

Okay, now we can write growing. Roughly, we want to have this logic:

if cap == 0:
    allocate()
    cap = 1
else:
    reallocate()
    cap *= 2

But Rust's only supported allocator API is so low level that we'll need to do a fair bit of extra work. We also need to guard against some special conditions that can occur with really large allocations or empty allocations.

In particular, ptr::offset will cause us a lot of trouble, because it has the semantics of LLVM's GEP inbounds instruction. If you're fortunate enough to not have dealt with this instruction, here's the basic story with GEP: alias analysis, alias analysis, alias analysis. It's super important to an optimizing compiler to be able to reason about data dependencies and aliasing.

As a simple example, consider the following fragment of code:

*x *= 7;
*y *= 3;

If the compiler can prove that x and y point to different locations in memory, the two operations can in theory be executed in parallel (by e.g. loading them into different registers and working on them independently). However the compiler can't do this in general because if x and y point to the same location in memory, the operations need to be done to the same value, and they can't just be merged afterwards.

When you use GEP inbounds, you are specifically telling LLVM that the offsets you're about to do are within the bounds of a single "allocated" entity. The ultimate payoff being that LLVM can assume that if two pointers are known to point to two disjoint objects, all the offsets of those pointers are also known to not alias (because you won't just end up in some random place in memory). LLVM is heavily optimized to work with GEP offsets, and inbounds offsets are the best of all, so it's important that we use them as much as possible.

So that's what GEP's about, how can it cause us trouble?

The first problem is that we index into arrays with unsigned integers, but GEP (and as a consequence ptr::offset) takes a signed integer. This means that half of the seemingly valid indices into an array will overflow GEP and actually go in the wrong direction! As such we must limit all allocations to isize::MAX elements. This actually means we only need to worry about byte-sized objects, because e.g. > isize::MAX u16s will truly exhaust all of the system's memory. However in order to avoid subtle corner cases where someone reinterprets some array of < isize::MAX objects as bytes, std limits all allocations to isize::MAX bytes.

On all 64-bit targets that Rust currently supports we're artificially limited to significantly less than all 64 bits of the address space (modern x64 platforms only expose 48-bit addressing), so we can rely on just running out of memory first. However on 32-bit targets, particularly those with extensions to use more of the address space (PAE x86 or x32), it's theoretically possible to successfully allocate more than isize::MAX bytes of memory.

However since this is a tutorial, we're not going to be particularly optimal here, and just unconditionally check, rather than use clever platform-specific cfgs.

The other corner-case we need to worry about is empty allocations. There will be two kinds of empty allocations we need to worry about: cap = 0 for all T, and cap > 0 for zero-sized types.

These cases are tricky because they come down to what LLVM means by "allocated". LLVM's notion of an allocation is significantly more abstract than how we usually use it. Because LLVM needs to work with different languages' semantics and custom allocators, it can't really intimately understand allocation. Instead, the main idea behind allocation is "doesn't overlap with other stuff". That is, heap allocations, stack allocations, and globals don't randomly overlap. Yep, it's about alias analysis. As such, Rust can technically play a bit fast and loose with the notion of an allocation as long as it's consistent.

Getting back to the empty allocation case, there are a couple of places where we want to offset by 0 as a consequence of generic code. The question is then: is it consistent to do so? For zero-sized types, we have concluded that it is indeed consistent to do a GEP inbounds offset by an arbitrary number of elements. This is a runtime no-op because every element takes up no space, and it's fine to pretend that there's infinite zero-sized types allocated at 0x01. No allocator will ever allocate that address, because they won't allocate 0x00 and they generally allocate to some minimal alignment higher than a byte. Also generally the whole first page of memory is protected from being allocated anyway (a whole 4k, on many platforms).

However what about for positive-sized types? That one's a bit trickier. In principle, you can argue that offsetting by 0 gives LLVM no information: either there's an element before the address or after it, but it can't know which. However we've chosen to conservatively assume that it may do bad things. As such we will guard against this case explicitly.

Phew

Ok with all the nonsense out of the way, let's actually allocate some memory:

use std::alloc::{self, Layout};

impl<T> Vec<T> {
    fn grow(&mut self) {
        let (new_cap, new_layout) = if self.cap == 0 {
            (1, Layout::array::<T>(1).unwrap())
        } else {
            // This can't overflow since self.cap <= isize::MAX.
            let new_cap = 2 * self.cap;

            // `Layout::array` checks that the number of bytes is <= usize::MAX,
            // but this is redundant since old_layout.size() <= isize::MAX,
            // so the `unwrap` should never fail.
            let new_layout = Layout::array::<T>(new_cap).unwrap();
            (new_cap, new_layout)
        };

        // Ensure that the new allocation doesn't exceed `isize::MAX` bytes.
        assert!(new_layout.size() <= isize::MAX as usize, "Allocation too large");

        let new_ptr = if self.cap == 0 {
            unsafe { alloc::alloc(new_layout) }
        } else {
            let old_layout = Layout::array::<T>(self.cap).unwrap();
            let old_ptr = self.ptr.as_ptr() as *mut u8;
            unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) }
        };

        // If allocation fails, `new_ptr` will be null, in which case we abort.
        self.ptr = match NonNull::new(new_ptr as *mut T) {
            Some(p) => p,
            None => alloc::handle_alloc_error(new_layout),
        };
        self.cap = new_cap;
    }
}
fn main() {}

Push and Pop

Alright. We can initialize. We can allocate. Let's actually implement some functionality! Let's start with push. All it needs to do is check if we're full to grow, unconditionally write to the next index, and then increment our length.

To do the write we have to be careful not to evaluate the memory we want to write to. At worst, it's truly uninitialized memory from the allocator. At best it's the bits of some old value we popped off. Either way, we can't just index to the memory and dereference it, because that will evaluate the memory as a valid instance of T. Worse, foo[idx] = x will try to call drop on the old value of foo[idx]!

The correct way to do this is with ptr::write, which just blindly overwrites the target address with the bits of the value we provide. No evaluation involved.

For push, if the old len (before push was called) is 0, then we want to write to the 0th index. So we should offset by the old len.

pub fn push(&mut self, elem: T) {
    if self.len == self.cap { self.grow(); }

    unsafe {
        ptr::write(self.ptr.as_ptr().add(self.len), elem);
    }

    // Can't fail, we'll OOM first.
    self.len += 1;
}

Easy! How about pop? Although this time the index we want to access is initialized, Rust won't just let us dereference the location of memory to move the value out, because that would leave the memory uninitialized! For this we need ptr::read, which just copies out the bits from the target address and interprets it as a value of type T. This will leave the memory at this address logically uninitialized, even though there is in fact a perfectly good instance of T there.

For pop, if the old len is 1, for example, we want to read out of the 0th index. So we should offset by the new len.

pub fn pop(&mut self) -> Option<T> {
    if self.len == 0 {
        None
    } else {
        self.len -= 1;
        unsafe {
            Some(ptr::read(self.ptr.as_ptr().add(self.len)))
        }
    }
}

Deallocating

Next we should implement Drop so that we don't massively leak tons of resources. The easiest way is to just call pop until it yields None, and then deallocate our buffer. Note that calling pop is unneeded if T: !Drop. In theory we can ask Rust if T needs_drop and omit the calls to pop. However in practice LLVM is really good at removing simple side-effect free code like this, so I wouldn't bother unless you notice it's not being stripped (in this case it is).

We must not call alloc::dealloc when self.cap == 0, as in this case we haven't actually allocated any memory.

impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        if self.cap != 0 {
            while let Some(_) = self.pop() { }
            let layout = Layout::array::<T>(self.cap).unwrap();
            unsafe {
                alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
            }
        }
    }
}

Deref

Alright! We've got a decent minimal stack implemented. We can push, we can pop, and we can clean up after ourselves. However there's a whole mess of functionality we'd reasonably want. In particular, we have a proper array, but none of the slice functionality. That's actually pretty easy to solve: we can implement Deref<Target=[T]>. This will magically make our Vec coerce to, and behave like, a slice in all sorts of conditions.

All we need is slice::from_raw_parts. It will correctly handle empty slices for us. Later once we set up zero-sized type support it will also Just Work for those too.

use std::ops::Deref;

impl<T> Deref for Vec<T> {
    type Target = [T];
    fn deref(&self) -> &[T] {
        unsafe {
            std::slice::from_raw_parts(self.ptr.as_ptr(), self.len)
        }
    }
}

And let's do DerefMut too:

use std::ops::DerefMut;

impl<T> DerefMut for Vec<T> {
    fn deref_mut(&mut self) -> &mut [T] {
        unsafe {
            std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len)
        }
    }
}

Now we have len, first, last, indexing, slicing, sorting, iter, iter_mut, and all other sorts of bells and whistles provided by slice. Sweet!

Insert and Remove

Something not provided by slice is insert and remove, so let's do those next.

Insert needs to shift all the elements at the target index to the right by one. To do this we need to use ptr::copy, which is our version of C's memmove. This copies some chunk of memory from one location to another, correctly handling the case where the source and destination overlap (which will definitely happen here).

If we insert at index i, we want to shift the [i .. len] to [i+1 .. len+1] using the old len.

pub fn insert(&mut self, index: usize, elem: T) {
    // Note: `<=` because it's valid to insert after everything
    // which would be equivalent to push.
    assert!(index <= self.len, "index out of bounds");
    if self.len == self.cap { self.grow(); }

    unsafe {
        // ptr::copy(src, dest, len): "copy from src to dest len elems"
        ptr::copy(
            self.ptr.as_ptr().add(index),
            self.ptr.as_ptr().add(index + 1),
            self.len - index,
        );
        ptr::write(self.ptr.as_ptr().add(index), elem);
    }

    self.len += 1;
}

Remove behaves in the opposite manner. We need to shift all the elements from [i+1 .. len + 1] to [i .. len] using the new len.

pub fn remove(&mut self, index: usize) -> T {
    // Note: `<` because it's *not* valid to remove after everything
    assert!(index < self.len, "index out of bounds");
    unsafe {
        self.len -= 1;
        let result = ptr::read(self.ptr.as_ptr().add(index));
        ptr::copy(
            self.ptr.as_ptr().add(index + 1),
            self.ptr.as_ptr().add(index),
            self.len - index,
        );
        result
    }
}

IntoIter

Let's move on to writing iterators. iter and iter_mut have already been written for us thanks to The Magic of Deref. However there's two interesting iterators that Vec provides that slices can't: into_iter and drain.

IntoIter consumes the Vec by-value, and can consequently yield its elements by-value. In order to enable this, IntoIter needs to take control of Vec's allocation.

IntoIter needs to be DoubleEnded as well, to enable reading from both ends. Reading from the back could just be implemented as calling pop, but reading from the front is harder. We could call remove(0) but that would be insanely expensive. Instead we're going to just use ptr::read to copy values out of either end of the Vec without mutating the buffer at all.

To do this we're going to use a very common C idiom for array iteration. We'll make two pointers; one that points to the start of the array, and one that points to one-element past the end. When we want an element from one end, we'll read out the value pointed to at that end and move the pointer over by one. When the two pointers are equal, we know we're done.

Note that the order of read and offset are reversed for next and next_back For next_back the pointer is always after the element it wants to read next, while for next the pointer is always at the element it wants to read next. To see why this is, consider the case where every element but one has been yielded.

The array looks like this:

          S  E
[X, X, X, O, X, X, X]

If E pointed directly at the element it wanted to yield next, it would be indistinguishable from the case where there are no more elements to yield.

Although we don't actually care about it during iteration, we also need to hold onto the Vec's allocation information in order to free it once IntoIter is dropped.

So we're going to use the following struct:

pub struct IntoIter<T> {
    buf: NonNull<T>,
    cap: usize,
    start: *const T,
    end: *const T,
}

And this is what we end up with for initialization:

impl<T> IntoIterator for Vec<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;
    fn into_iter(self) -> IntoIter<T> {
        // Make sure not to drop Vec since that would free the buffer
        let vec = ManuallyDrop::new(self);

        // Can't destructure Vec since it's Drop
        let ptr = vec.ptr;
        let cap = vec.cap;
        let len = vec.len;

        IntoIter {
            buf: ptr,
            cap,
            start: ptr.as_ptr(),
            end: if cap == 0 {
                // can't offset off this pointer, it's not allocated!
                ptr.as_ptr()
            } else {
                unsafe { ptr.as_ptr().add(len) }
            },
        }
    }
}

Here's iterating forward:

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                let result = ptr::read(self.start);
                self.start = self.start.offset(1);
                Some(result)
            }
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let len = (self.end as usize - self.start as usize)
                  / mem::size_of::<T>();
        (len, Some(len))
    }
}

And here's iterating backwards.

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                self.end = self.end.offset(-1);
                Some(ptr::read(self.end))
            }
        }
    }
}

Because IntoIter takes ownership of its allocation, it needs to implement Drop to free it. However it also wants to implement Drop to drop any elements it contains that weren't yielded.

impl<T> Drop for IntoIter<T> {
    fn drop(&mut self) {
        if self.cap != 0 {
            // drop any remaining elements
            for _ in &mut *self {}
            let layout = Layout::array::<T>(self.cap).unwrap();
            unsafe {
                alloc::dealloc(self.buf.as_ptr() as *mut u8, layout);
            }
        }
    }
}

RawVec

We've actually reached an interesting situation here: we've duplicated the logic for specifying a buffer and freeing its memory in Vec and IntoIter. Now that we've implemented it and identified actual logic duplication, this is a good time to perform some logic compression.

We're going to abstract out the (ptr, cap) pair and give them the logic for allocating, growing, and freeing:

struct RawVec<T> {
    ptr: NonNull<T>,
    cap: usize,
}

unsafe impl<T: Send> Send for RawVec<T> {}
unsafe impl<T: Sync> Sync for RawVec<T> {}

impl<T> RawVec<T> {
    fn new() -> Self {
        assert!(mem::size_of::<T>() != 0, "TODO: implement ZST support");
        RawVec {
            ptr: NonNull::dangling(),
            cap: 0,
        }
    }

    fn grow(&mut self) {
        // This can't overflow because we ensure self.cap <= isize::MAX.
        let new_cap = if self.cap == 0 { 1 } else { 2 * self.cap };

        // Layout::array checks that the number of bytes is <= usize::MAX,
        // but this is redundant since old_layout.size() <= isize::MAX,
        // so the `unwrap` should never fail.
        let new_layout = Layout::array::<T>(new_cap).unwrap();

        // Ensure that the new allocation doesn't exceed `isize::MAX` bytes.
        assert!(new_layout.size() <= isize::MAX as usize, "Allocation too large");

        let new_ptr = if self.cap == 0 {
            unsafe { alloc::alloc(new_layout) }
        } else {
            let old_layout = Layout::array::<T>(self.cap).unwrap();
            let old_ptr = self.ptr.as_ptr() as *mut u8;
            unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) }
        };

        // If allocation fails, `new_ptr` will be null, in which case we abort.
        self.ptr = match NonNull::new(new_ptr as *mut T) {
            Some(p) => p,
            None => alloc::handle_alloc_error(new_layout),
        };
        self.cap = new_cap;
    }
}

impl<T> Drop for RawVec<T> {
    fn drop(&mut self) {
        if self.cap != 0 {
            let layout = Layout::array::<T>(self.cap).unwrap();
            unsafe {
                alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
            }
        }
    }
}

And change Vec as follows:

pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

impl<T> Vec<T> {
    fn ptr(&self) -> *mut T {
        self.buf.ptr.as_ptr()
    }

    fn cap(&self) -> usize {
        self.buf.cap
    }

    pub fn new() -> Self {
        Vec {
            buf: RawVec::new(),
            len: 0,
        }
    }

    // push/pop/insert/remove largely unchanged:
    // * `self.ptr.as_ptr() -> self.ptr()`
    // * `self.cap -> self.cap()`
    // * `self.grow() -> self.buf.grow()`
}

impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        while let Some(_) = self.pop() {}
        // deallocation is handled by RawVec
    }
}

And finally we can really simplify IntoIter:

pub struct IntoIter<T> {
    _buf: RawVec<T>, // we don't actually care about this. Just need it to live.
    start: *const T,
    end: *const T,
}

// next and next_back literally unchanged since they never referred to the buf

impl<T> Drop for IntoIter<T> {
    fn drop(&mut self) {
        // only need to ensure all our elements are read;
        // buffer will clean itself up afterwards.
        for _ in &mut *self {}
    }
}

impl<T> IntoIterator for Vec<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;
    fn into_iter(self) -> IntoIter<T> {
        // need to use ptr::read to unsafely move the buf out since it's
        // not Copy, and Vec implements Drop (so we can't destructure it).
        let buf = unsafe { ptr::read(&self.buf) };
        let len = self.len;
        mem::forget(self);

        IntoIter {
            start: buf.ptr.as_ptr(),
            end: if buf.cap == 0 {
                // can't offset off of a pointer unless it's part of an allocation
                buf.ptr.as_ptr()
            } else {
                unsafe { buf.ptr.as_ptr().add(len) }
            },
            _buf: buf,
        }
    }
}

Much better.

Drain

Let's move on to Drain. Drain is largely the same as IntoIter, except that instead of consuming the Vec, it borrows the Vec and leaves its allocation untouched. For now we'll only implement the "basic" full-range version.

use std::marker::PhantomData;

struct Drain<'a, T: 'a> {
    // Need to bound the lifetime here, so we do it with `&'a mut Vec<T>`
    // because that's semantically what we contain. We're "just" calling
    // `pop()` and `remove(0)`.
    vec: PhantomData<&'a mut Vec<T>>,
    start: *const T,
    end: *const T,
}

impl<'a, T> Iterator for Drain<'a, T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        if self.start == self.end {
            None

-- wait, this is seeming familiar. Let's do some more compression. Both IntoIter and Drain have the exact same structure, let's just factor it out.

struct RawValIter<T> {
    start: *const T,
    end: *const T,
}

impl<T> RawValIter<T> {
    // unsafe to construct because it has no associated lifetimes.
    // This is necessary to store a RawValIter in the same struct as
    // its actual allocation. OK since it's a private implementation
    // detail.
    unsafe fn new(slice: &[T]) -> Self {
        RawValIter {
            start: slice.as_ptr(),
            end: if slice.len() == 0 {
                // if `len = 0`, then this is not actually allocated memory.
                // Need to avoid offsetting because that will give wrong
                // information to LLVM via GEP.
                slice.as_ptr()
            } else {
                slice.as_ptr().add(slice.len())
            }
        }
    }
}

// Iterator and DoubleEndedIterator impls identical to IntoIter.

And IntoIter becomes the following:

pub struct IntoIter<T> {
    _buf: RawVec<T>, // we don't actually care about this. Just need it to live.
    iter: RawValIter<T>,
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> { self.iter.next() }
    fn size_hint(&self) -> (usize, Option<usize>) { self.iter.size_hint() }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<T> { self.iter.next_back() }
}

impl<T> Drop for IntoIter<T> {
    fn drop(&mut self) {
        for _ in &mut *self {}
    }
}

impl<T> IntoIterator for Vec<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;
    fn into_iter(self) -> IntoIter<T> {
        unsafe {
            let iter = RawValIter::new(&self);

            let buf = ptr::read(&self.buf);
            mem::forget(self);

            IntoIter {
                iter,
                _buf: buf,
            }
        }
    }
}

Note that I've left a few quirks in this design to make upgrading Drain to work with arbitrary subranges a bit easier. In particular we could have RawValIter drain itself on drop, but that won't work right for a more complex Drain. We also take a slice to simplify Drain initialization.

Alright, now Drain is really easy:

use std::marker::PhantomData;

pub struct Drain<'a, T: 'a> {
    vec: PhantomData<&'a mut Vec<T>>,
    iter: RawValIter<T>,
}

impl<'a, T> Iterator for Drain<'a, T> {
    type Item = T;
    fn next(&mut self) -> Option<T> { self.iter.next() }
    fn size_hint(&self) -> (usize, Option<usize>) { self.iter.size_hint() }
}

impl<'a, T> DoubleEndedIterator for Drain<'a, T> {
    fn next_back(&mut self) -> Option<T> { self.iter.next_back() }
}

impl<'a, T> Drop for Drain<'a, T> {
    fn drop(&mut self) {
        for _ in &mut *self {}
    }
}

impl<T> Vec<T> {
    pub fn drain(&mut self) -> Drain<T> {
        let iter = unsafe { RawValIter::new(&self) };

        // this is a mem::forget safety thing. If Drain is forgotten, we just
        // leak the whole Vec's contents. Also we need to do this *eventually*
        // anyway, so why not do it now?
        self.len = 0;

        Drain {
            iter,
            vec: PhantomData,
        }
    }
}

For more details on the mem::forget problem, see the section on leaks.

Handling Zero-Sized Types

It's time. We're going to fight the specter that is zero-sized types. Safe Rust never needs to care about this, but Vec is very intensive on raw pointers and raw allocations, which are exactly the two things that care about zero-sized types. We need to be careful of two things:

  • The raw allocator API has undefined behavior if you pass in 0 for an allocation size.
  • raw pointer offsets are no-ops for zero-sized types, which will break our C-style pointer iterator.

Thankfully we abstracted out pointer-iterators and allocating handling into RawValIter and RawVec respectively. How mysteriously convenient.

Allocating Zero-Sized Types

So if the allocator API doesn't support zero-sized allocations, what on earth do we store as our allocation? NonNull::dangling() of course! Almost every operation with a ZST is a no-op since ZSTs have exactly one value, and therefore no state needs to be considered to store or load them. This actually extends to ptr::read and ptr::write: they won't actually look at the pointer at all. As such we never need to change the pointer.

Note however that our previous reliance on running out of memory before overflow is no longer valid with zero-sized types. We must explicitly guard against capacity overflow for zero-sized types.

Due to our current architecture, all this means is writing 3 guards, one in each method of RawVec.

impl<T> RawVec<T> {
    fn new() -> Self {
        // This branch should be stripped at compile time.
        let cap = if mem::size_of::<T>() == 0 { usize::MAX } else { 0 };

        // `NonNull::dangling()` doubles as "unallocated" and "zero-sized allocation"
        RawVec {
            ptr: NonNull::dangling(),
            cap,
        }
    }

    fn grow(&mut self) {
        // since we set the capacity to usize::MAX when T has size 0,
        // getting to here necessarily means the Vec is overfull.
        assert!(mem::size_of::<T>() != 0, "capacity overflow");

        let (new_cap, new_layout) = if self.cap == 0 {
            (1, Layout::array::<T>(1).unwrap())
        } else {
            // This can't overflow because we ensure self.cap <= isize::MAX.
            let new_cap = 2 * self.cap;

            // `Layout::array` checks that the number of bytes is <= usize::MAX,
            // but this is redundant since old_layout.size() <= isize::MAX,
            // so the `unwrap` should never fail.
            let new_layout = Layout::array::<T>(new_cap).unwrap();
            (new_cap, new_layout)
        };

        // Ensure that the new allocation doesn't exceed `isize::MAX` bytes.
        assert!(new_layout.size() <= isize::MAX as usize, "Allocation too large");

        let new_ptr = if self.cap == 0 {
            unsafe { alloc::alloc(new_layout) }
        } else {
            let old_layout = Layout::array::<T>(self.cap).unwrap();
            let old_ptr = self.ptr.as_ptr() as *mut u8;
            unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) }
        };

        // If allocation fails, `new_ptr` will be null, in which case we abort.
        self.ptr = match NonNull::new(new_ptr as *mut T) {
            Some(p) => p,
            None => alloc::handle_alloc_error(new_layout),
        };
        self.cap = new_cap;
    }
}

impl<T> Drop for RawVec<T> {
    fn drop(&mut self) {
        let elem_size = mem::size_of::<T>();

        if self.cap != 0 && elem_size != 0 {
            unsafe {
                alloc::dealloc(
                    self.ptr.as_ptr() as *mut u8,
                    Layout::array::<T>(self.cap).unwrap(),
                );
            }
        }
    }
}

That's it. We support pushing and popping zero-sized types now. Our iterators (that aren't provided by slice Deref) are still busted, though.

Iterating Zero-Sized Types

Zero-sized offsets are no-ops. This means that our current design will always initialize start and end as the same value, and our iterators will yield nothing. The current solution to this is to cast the pointers to integers, increment, and then cast them back:

impl<T> RawValIter<T> {
    unsafe fn new(slice: &[T]) -> Self {
        RawValIter {
            start: slice.as_ptr(),
            end: if mem::size_of::<T>() == 0 {
                ((slice.as_ptr() as usize) + slice.len()) as *const _
            } else if slice.len() == 0 {
                slice.as_ptr()
            } else {
                slice.as_ptr().add(slice.len())
            },
        }
    }
}

Now we have a different bug. Instead of our iterators not running at all, our iterators now run forever. We need to do the same trick in our iterator impls. Also, our size_hint computation code will divide by 0 for ZSTs. Since we'll basically be treating the two pointers as if they point to bytes, we'll just map size 0 to divide by 1. Here's what next will be:

fn next(&mut self) -> Option<T> {
    if self.start == self.end {
        None
    } else {
        unsafe {
            let result = ptr::read(self.start);
            self.start = if mem::size_of::<T>() == 0 {
                (self.start as usize + 1) as *const _
            } else {
                self.start.offset(1)
            };
            Some(result)
        }
    }
}

Do you see the "bug"? No one else did! The original author only noticed the problem when linking to this page years later. This code is kind of dubious because abusing the iterator pointers to be counters makes them unaligned! Our one job when using ZSTs is to keep pointers aligned! forehead slap

Raw pointers don't need to be aligned at all times, so the basic trick of using pointers as counters is fine, but they should definitely be aligned when passed to ptr::read! This is possibly needless pedantry because ptr::read is a noop for a ZST, but let's be a little more responsible and read from NonNull::dangling on the ZST path.

(Alternatively you could call read_unaligned on the ZST path. Either is fine, because either way we're making up a value from nothing and it all compiles to doing nothing.)

impl<T> Iterator for RawValIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                if mem::size_of::<T>() == 0 {
                    self.start = (self.start as usize + 1) as *const _;
                    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))
                } else {
                    let old_ptr = self.start;
                    self.start = self.start.offset(1);
                    Some(ptr::read(old_ptr))
                }
            }
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let elem_size = mem::size_of::<T>();
        let len = (self.end as usize - self.start as usize)
                  / if elem_size == 0 { 1 } else { elem_size };
        (len, Some(len))
    }
}

impl<T> DoubleEndedIterator for RawValIter<T> {
    fn next_back(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                if mem::size_of::<T>() == 0 {
                    self.end = (self.end as usize - 1) as *const _;
                    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))
                } else {
                    self.end = self.end.offset(-1);
                    Some(ptr::read(self.end))
                }
            }
        }
    }
}

And that's it. Iteration works!

The Final Code

use std::alloc::{self, Layout};
use std::marker::PhantomData;
use std::mem;
use std::ops::{Deref, DerefMut};
use std::ptr::{self, NonNull};

struct RawVec<T> {
    ptr: NonNull<T>,
    cap: usize,
}

unsafe impl<T: Send> Send for RawVec<T> {}
unsafe impl<T: Sync> Sync for RawVec<T> {}

impl<T> RawVec<T> {
    fn new() -> Self {
        // !0 is usize::MAX. This branch should be stripped at compile time.
        let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };

        // `NonNull::dangling()` doubles as "unallocated" and "zero-sized allocation"
        RawVec {
            ptr: NonNull::dangling(),
            cap,
        }
    }

    fn grow(&mut self) {
        // since we set the capacity to usize::MAX when T has size 0,
        // getting to here necessarily means the Vec is overfull.
        assert!(mem::size_of::<T>() != 0, "capacity overflow");

        let (new_cap, new_layout) = if self.cap == 0 {
            (1, Layout::array::<T>(1).unwrap())
        } else {
            // This can't overflow because we ensure self.cap <= isize::MAX.
            let new_cap = 2 * self.cap;

            // `Layout::array` checks that the number of bytes is <= usize::MAX,
            // but this is redundant since old_layout.size() <= isize::MAX,
            // so the `unwrap` should never fail.
            let new_layout = Layout::array::<T>(new_cap).unwrap();
            (new_cap, new_layout)
        };

        // Ensure that the new allocation doesn't exceed `isize::MAX` bytes.
        assert!(
            new_layout.size() <= isize::MAX as usize,
            "Allocation too large"
        );

        let new_ptr = if self.cap == 0 {
            unsafe { alloc::alloc(new_layout) }
        } else {
            let old_layout = Layout::array::<T>(self.cap).unwrap();
            let old_ptr = self.ptr.as_ptr() as *mut u8;
            unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) }
        };

        // If allocation fails, `new_ptr` will be null, in which case we abort.
        self.ptr = match NonNull::new(new_ptr as *mut T) {
            Some(p) => p,
            None => alloc::handle_alloc_error(new_layout),
        };
        self.cap = new_cap;
    }
}

impl<T> Drop for RawVec<T> {
    fn drop(&mut self) {
        let elem_size = mem::size_of::<T>();

        if self.cap != 0 && elem_size != 0 {
            unsafe {
                alloc::dealloc(
                    self.ptr.as_ptr() as *mut u8,
                    Layout::array::<T>(self.cap).unwrap(),
                );
            }
        }
    }
}

pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

impl<T> Vec<T> {
    fn ptr(&self) -> *mut T {
        self.buf.ptr.as_ptr()
    }

    fn cap(&self) -> usize {
        self.buf.cap
    }

    pub fn new() -> Self {
        Vec {
            buf: RawVec::new(),
            len: 0,
        }
    }
    pub fn push(&mut self, elem: T) {
        if self.len == self.cap() {
            self.buf.grow();
        }

        unsafe {
            ptr::write(self.ptr().add(self.len), elem);
        }

        // Can't overflow, we'll OOM first.
        self.len += 1;
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            None
        } else {
            self.len -= 1;
            unsafe { Some(ptr::read(self.ptr().add(self.len))) }
        }
    }

    pub fn insert(&mut self, index: usize, elem: T) {
        assert!(index <= self.len, "index out of bounds");
        if self.len == self.cap() {
            self.buf.grow();
        }

        unsafe {
            ptr::copy(
                self.ptr().add(index),
                self.ptr().add(index + 1),
                self.len - index,
            );
            ptr::write(self.ptr().add(index), elem);
        }

        self.len += 1;
    }

    pub fn remove(&mut self, index: usize) -> T {
        assert!(index < self.len, "index out of bounds");

        self.len -= 1;

        unsafe {
            let result = ptr::read(self.ptr().add(index));
            ptr::copy(
                self.ptr().add(index + 1),
                self.ptr().add(index),
                self.len - index,
            );
            result
        }
    }

    pub fn drain(&mut self) -> Drain<T> {
        let iter = unsafe { RawValIter::new(&self) };

        // this is a mem::forget safety thing. If Drain is forgotten, we just
        // leak the whole Vec's contents. Also we need to do this *eventually*
        // anyway, so why not do it now?
        self.len = 0;

        Drain {
            iter,
            vec: PhantomData,
        }
    }
}

impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        while let Some(_) = self.pop() {}
        // deallocation is handled by RawVec
    }
}

impl<T> Deref for Vec<T> {
    type Target = [T];
    fn deref(&self) -> &[T] {
        unsafe { std::slice::from_raw_parts(self.ptr(), self.len) }
    }
}

impl<T> DerefMut for Vec<T> {
    fn deref_mut(&mut self) -> &mut [T] {
        unsafe { std::slice::from_raw_parts_mut(self.ptr(), self.len) }
    }
}

impl<T> IntoIterator for Vec<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;
    fn into_iter(self) -> IntoIter<T> {
        let (iter, buf) = unsafe {
            (RawValIter::new(&self), ptr::read(&self.buf))
        };

        mem::forget(self);

        IntoIter {
            iter,
            _buf: buf,
        }
    }
}

struct RawValIter<T> {
    start: *const T,
    end: *const T,
}

impl<T> RawValIter<T> {
    unsafe fn new(slice: &[T]) -> Self {
        RawValIter {
            start: slice.as_ptr(),
            end: if mem::size_of::<T>() == 0 {
                ((slice.as_ptr() as usize) + slice.len()) as *const _
            } else if slice.len() == 0 {
                slice.as_ptr()
            } else {
                slice.as_ptr().add(slice.len())
            },
        }
    }
}

impl<T> Iterator for RawValIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                if mem::size_of::<T>() == 0 {
                    self.start = (self.start as usize + 1) as *const _;
                    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))
                } else {
                    let old_ptr = self.start;
                    self.start = self.start.offset(1);
                    Some(ptr::read(old_ptr))
                }
            }
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let elem_size = mem::size_of::<T>();
        let len = (self.end as usize - self.start as usize)
                  / if elem_size == 0 { 1 } else { elem_size };
        (len, Some(len))
    }
}

impl<T> DoubleEndedIterator for RawValIter<T> {
    fn next_back(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                if mem::size_of::<T>() == 0 {
                    self.end = (self.end as usize - 1) as *const _;
                    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))
                } else {
                    self.end = self.end.offset(-1);
                    Some(ptr::read(self.end))
                }
            }
        }
    }
}

pub struct IntoIter<T> {
    _buf: RawVec<T>, // we don't actually care about this. Just need it to live.
    iter: RawValIter<T>,
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        self.iter.next()
    }
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.iter.size_hint()
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<T> {
        self.iter.next_back()
    }
}

impl<T> Drop for IntoIter<T> {
    fn drop(&mut self) {
        for _ in &mut *self {}
    }
}

pub struct Drain<'a, T: 'a> {
    vec: PhantomData<&'a mut Vec<T>>,
    iter: RawValIter<T>,
}

impl<'a, T> Iterator for Drain<'a, T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        self.iter.next()
    }
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.iter.size_hint()
    }
}

impl<'a, T> DoubleEndedIterator for Drain<'a, T> {
    fn next_back(&mut self) -> Option<T> {
        self.iter.next_back()
    }
}

impl<'a, T> Drop for Drain<'a, T> {
    fn drop(&mut self) {
        // pre-drain the iter
        for _ in &mut *self {}
    }
}

fn main() {
    tests::create_push_pop();
    tests::iter_test();
    tests::test_drain();
    tests::test_zst();
    println!("All tests finished OK");
}

mod tests {
    use super::*;

    pub fn create_push_pop() {
        let mut v = Vec::new();
        v.push(1);
        assert_eq!(1, v.len());
        assert_eq!(1, v[0]);
        for i in v.iter_mut() {
            *i += 1;
        }
        v.insert(0, 5);
        let x = v.pop();
        assert_eq!(Some(2), x);
        assert_eq!(1, v.len());
        v.push(10);
        let x = v.remove(0);
        assert_eq!(5, x);
        assert_eq!(1, v.len());
    }

    pub fn iter_test() {
        let mut v = Vec::new();
        for i in 0..10 {
            v.push(Box::new(i))
        }
        let mut iter = v.into_iter();
        let first = iter.next().unwrap();
        let last = iter.next_back().unwrap();
        drop(iter);
        assert_eq!(0, *first);
        assert_eq!(9, *last);
    }

    pub fn test_drain() {
        let mut v = Vec::new();
        for i in 0..10 {
            v.push(Box::new(i))
        }
        {
            let mut drain = v.drain();
            let first = drain.next().unwrap();
            let last = drain.next_back().unwrap();
            assert_eq!(0, *first);
            assert_eq!(9, *last);
        }
        assert_eq!(0, v.len());
        v.push(Box::new(1));
        assert_eq!(1, *v.pop().unwrap());
    }

    pub fn test_zst() {
        let mut v = Vec::new();
        for _i in 0..10 {
            v.push(())
        }

        let mut count = 0;

        for _ in v.into_iter() {
            count += 1
        }

        assert_eq!(10, count);
    }
}

Implementing Arc and Mutex

Knowing the theory is all fine and good, but the best way to understand something is to use it. To better understand atomics and interior mutability, we'll be implementing versions of the standard library's Arc and Mutex types.

TODO: Write Mutex chapters.

Implementing Arc

In this section, we'll be implementing a simpler version of std::sync::Arc. Similarly to the implementation of Vec we made earlier, we won't be taking advantage of as many optimizations, intrinsics, or unstable code as the standard library may.

This implementation is loosely based on the standard library's implementation (technically taken from alloc::sync in 1.49, as that's where it's actually implemented), but it will not support weak references at the moment as they make the implementation slightly more complex.

Please note that this section is very work-in-progress at the moment.

Layout

Let's start by making the layout for our implementation of Arc.

An Arc<T> provides thread-safe shared ownership of a value of type T, allocated in the heap. Sharing implies immutability in Rust, so we don't need to design anything that manages access to that value, right? Although interior mutability types like Mutex allow Arc's users to create shared mutability, Arc itself doesn't need to concern itself with these issues.

However there is one place where Arc needs to concern itself with mutation: destruction. When all the owners of the Arc go away, we need to be able to drop its contents and free its allocation. So we need a way for an owner to know if it's the last owner, and the simplest way to do that is with a count of the owners -- Reference Counting.

Unfortunately, this reference count is inherently shared mutable state, so Arc does need to think about synchronization. We could use a Mutex for this, but that's overkill. Instead, we'll use atomics. And since everyone already needs a pointer to the T's allocation, we might as well put the reference count in that same allocation.

Naively, it would look something like this:

#![allow(unused)]
fn main() {
use std::sync::atomic;

pub struct Arc<T> {
    ptr: *mut ArcInner<T>,
}

pub struct ArcInner<T> {
    rc: atomic::AtomicUsize,
    data: T,
}
}

This would compile, however it would be incorrect. First of all, the compiler will give us too strict variance. For example, an Arc<&'static str> couldn't be used where an Arc<&'a str> was expected. More importantly, it will give incorrect ownership information to the drop checker, as it will assume we don't own any values of type T. As this is a structure providing shared ownership of a value, at some point there will be an instance of this structure that entirely owns its data. See the chapter on ownership and lifetimes for all the details on variance and drop check.

To fix the first problem, we can use NonNull<T>. Note that NonNull<T> is a wrapper around a raw pointer that declares that:

  • We are covariant over T
  • Our pointer is never null

To fix the second problem, we can include a PhantomData marker containing an ArcInner<T>. This will tell the drop checker that we have some notion of ownership of a value of ArcInner<T> (which itself contains some T).

With these changes we get our final structure:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::sync::atomic::AtomicUsize;

pub struct Arc<T> {
    ptr: NonNull<ArcInner<T>>,
    phantom: PhantomData<ArcInner<T>>,
}

pub struct ArcInner<T> {
    rc: AtomicUsize,
    data: T,
}
}

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 only Arc referencing that data (which only happens in Drop)
  • 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 Arcs (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
    }
}

Cloning

Now that we've got some basic code set up, we'll need a way to clone the Arc.

Basically, we need to:

  1. Increment the atomic reference count
  2. Construct a new instance of the Arc from the inner pointer

First, we need to get access to the ArcInner:

let inner = unsafe { self.ptr.as_ref() };

We can update the atomic reference count as follows:

let old_rc = inner.rc.fetch_add(1, Ordering::???);

But what ordering should we use here? We don't really have any code that will need atomic synchronization when cloning, as we do not modify the internal value while cloning. Thus, we can use a Relaxed ordering here, which implies no happens-before relationship but is atomic. When Dropping the Arc, however, we'll need to atomically synchronize when decrementing the reference count. This is described more in the section on the Drop implementation for Arc. For more information on atomic relationships and Relaxed ordering, see the section on atomics.

Thus, the code becomes this:

let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);

We'll need to add another import to use Ordering:

#![allow(unused)]
fn main() {
use std::sync::atomic::Ordering;
}

However, we have one problem with this implementation right now. What if someone decides to mem::forget a bunch of Arcs? The code we have written so far (and will write) assumes that the reference count accurately portrays how many Arcs are in memory, but with mem::forget this is false. Thus, when more and more Arcs are cloned from this one without them being Dropped and the reference count being decremented, we can overflow! This will cause use-after-free which is INCREDIBLY BAD!

To handle this, we need to check that the reference count does not go over some arbitrary value (below usize::MAX, as we're storing the reference count as an AtomicUsize), and do something.

The standard library's implementation decides to just abort the program (as it is an incredibly unlikely case in normal code and if it happens, the program is probably incredibly degenerate) if the reference count reaches isize::MAX (about half of usize::MAX) on any thread, on the assumption that there are probably not about 2 billion threads (or about 9 quintillion on some 64-bit machines) incrementing the reference count at once. This is what we'll do.

It's pretty simple to implement this behavior:

if old_rc >= isize::MAX as usize {
    std::process::abort();
}

Then, we need to return a new instance of the Arc:

Self {
    ptr: self.ptr,
    phantom: PhantomData
}

Now, let's wrap this all up inside the Clone implementation:

use std::sync::atomic::Ordering;

impl<T> Clone for Arc<T> {
    fn clone(&self) -> Arc<T> {
        let inner = unsafe { self.ptr.as_ref() };
        // Using a relaxed ordering is alright here as we don't need any atomic
        // synchronization here as we're not modifying or accessing the inner
        // data.
        let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);

        if old_rc >= isize::MAX as usize {
            std::process::abort();
        }

        Self {
            ptr: self.ptr,
            phantom: PhantomData,
        }
    }
}

Dropping

We now need a way to decrease the reference count and drop the data once it is low enough, otherwise the data will live forever on the heap.

To do this, we can implement Drop.

Basically, we need to:

  1. Decrement the reference count
  2. If there is only one reference remaining to the data, then:
  3. Atomically fence the data to prevent reordering of the use and deletion of the data
  4. Drop the inner data

First, we'll need to get access to the ArcInner:

let inner = unsafe { self.ptr.as_ref() };

Now, we need to decrement the reference count. To streamline our code, we can also return if the returned value from fetch_sub (the value of the reference count before decrementing it) is not equal to 1 (which happens when we are not the last reference to the data).

if inner.rc.fetch_sub(1, Ordering::Release) != 1 {
    return;
}

We then need to create an atomic fence to prevent reordering of the use of the data and deletion of the data. As described in the standard library's implementation of Arc:

This fence is needed to prevent reordering of use of the data and deletion of the data. Because it is marked Release, the decreasing of the reference count synchronizes with this Acquire fence. This means that use of the data happens before decreasing the reference count, which happens before this fence, which happens before the deletion of the data.

As explained in the Boost documentation,

It is important to enforce any possible access to the object in one thread (through an existing reference) to happen before deleting the object in a different thread. This is achieved by a "release" operation after dropping a reference (any access to the object through this reference must obviously happened before), and an "acquire" operation before deleting the object.

In particular, while the contents of an Arc are usually immutable, it's possible to have interior writes to something like a Mutex. Since a Mutex is not acquired when it is deleted, we can't rely on its synchronization logic to make writes in thread A visible to a destructor running in thread B.

Also note that the Acquire fence here could probably be replaced with an Acquire load, which could improve performance in highly-contended situations. See 2.

To do this, we do the following:

#![allow(unused)]
fn main() {
use std::sync::atomic::Ordering;
use std::sync::atomic;
atomic::fence(Ordering::Acquire);
}

Finally, we can drop the data itself. We use Box::from_raw to drop the boxed ArcInner<T> and its data. This takes a *mut T and not a NonNull<T>, so we must convert using NonNull::as_ptr.

unsafe { Box::from_raw(self.ptr.as_ptr()); }

This is safe as we know we have the last pointer to the ArcInner and that its pointer is valid.

Now, let's wrap this all up inside the Drop implementation:

impl<T> Drop for Arc<T> {
    fn drop(&mut self) {
        let inner = unsafe { self.ptr.as_ref() };
        if inner.rc.fetch_sub(1, Ordering::Release) != 1 {
            return;
        }
        // This fence is needed to prevent reordering of the use and deletion
        // of the data.
        atomic::fence(Ordering::Acquire);
        // This is safe as we know we have the last pointer to the `ArcInner`
        // and that its pointer is valid.
        unsafe { Box::from_raw(self.ptr.as_ptr()); }
    }
}

Final Code

Here's the final code, with some added comments and re-ordered imports:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;
use std::ops::Deref;
use std::ptr::NonNull;
use std::sync::atomic::{self, AtomicUsize, Ordering};

pub struct Arc<T> {
    ptr: NonNull<ArcInner<T>>,
    phantom: PhantomData<ArcInner<T>>,
}

pub struct ArcInner<T> {
    rc: AtomicUsize,
    data: T,
}

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
    }
}

impl<T> Clone for Arc<T> {
    fn clone(&self) -> Arc<T> {
        let inner = unsafe { self.ptr.as_ref() };
        // Using a relaxed ordering is alright here as we don't need any atomic
        // synchronization here as we're not modifying or accessing the inner
        // data.
        let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);

        if old_rc >= isize::MAX as usize {
            std::process::abort();
        }

        Self {
            ptr: self.ptr,
            phantom: PhantomData,
        }
    }
}

impl<T> Drop for Arc<T> {
    fn drop(&mut self) {
        let inner = unsafe { self.ptr.as_ref() };
        if inner.rc.fetch_sub(1, Ordering::Release) != 1 {
            return;
        }
        // This fence is needed to prevent reordering of the use and deletion
        // of the data.
        atomic::fence(Ordering::Acquire);
        // This is safe as we know we have the last pointer to the `ArcInner`
        // and that its pointer is valid.
        unsafe { Box::from_raw(self.ptr.as_ptr()); }
    }
}
}

Foreign Function Interface

Introduction

This guide will use the snappy compression/decompression library as an introduction to writing bindings for foreign code. Rust is currently unable to call directly into a C++ library, but snappy includes a C interface (documented in snappy-c.h).

A note about libc

Many of these examples use the libc crate, which provides various type definitions for C types, among other things. If you’re trying these examples yourself, you’ll need to add libc to your Cargo.toml:

[dependencies]
libc = "0.2.0"

Calling foreign functions

The following is a minimal example of calling a foreign function which will compile if snappy is installed:

use libc::size_t;

#[link(name = "snappy")]
extern {
    fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}

fn main() {
    let x = unsafe { snappy_max_compressed_length(100) };
    println!("max compressed length of a 100 byte buffer: {}", x);
}

The extern block is a list of function signatures in a foreign library, in this case with the platform's C ABI. The #[link(...)] attribute is used to instruct the linker to link against the snappy library so the symbols are resolved.

Foreign functions are assumed to be unsafe so calls to them need to be wrapped with unsafe {} as a promise to the compiler that everything contained within truly is safe. C libraries often expose interfaces that aren't thread-safe, and almost any function that takes a pointer argument isn't valid for all possible inputs since the pointer could be dangling, and raw pointers fall outside of Rust's safe memory model.

When declaring the argument types to a foreign function, the Rust compiler cannot check if the declaration is correct, so specifying it correctly is part of keeping the binding correct at runtime.

The extern block can be extended to cover the entire snappy API:

use libc::{c_int, size_t};

#[link(name = "snappy")]
extern {
    fn snappy_compress(input: *const u8,
                       input_length: size_t,
                       compressed: *mut u8,
                       compressed_length: *mut size_t) -> c_int;
    fn snappy_uncompress(compressed: *const u8,
                         compressed_length: size_t,
                         uncompressed: *mut u8,
                         uncompressed_length: *mut size_t) -> c_int;
    fn snappy_max_compressed_length(source_length: size_t) -> size_t;
    fn snappy_uncompressed_length(compressed: *const u8,
                                  compressed_length: size_t,
                                  result: *mut size_t) -> c_int;
    fn snappy_validate_compressed_buffer(compressed: *const u8,
                                         compressed_length: size_t) -> c_int;
}
fn main() {}

Creating a safe interface

The raw C API needs to be wrapped to provide memory safety and make use of higher-level concepts like vectors. A library can choose to expose only the safe, high-level interface and hide the unsafe internal details.

Wrapping the functions which expect buffers involves using the slice::raw module to manipulate Rust vectors as pointers to memory. Rust's vectors are guaranteed to be a contiguous block of memory. The length is the number of elements currently contained, and the capacity is the total size in elements of the allocated memory. The length is less than or equal to the capacity.

use libc::{c_int, size_t};
unsafe fn snappy_validate_compressed_buffer(_: *const u8, _: size_t) -> c_int { 0 }
fn main() {}
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
    unsafe {
        snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
    }
}

The validate_compressed_buffer wrapper above makes use of an unsafe block, but it makes the guarantee that calling it is safe for all inputs by leaving off unsafe from the function signature.

The snappy_compress and snappy_uncompress functions are more complex, since a buffer has to be allocated to hold the output too.

The snappy_max_compressed_length function can be used to allocate a vector with the maximum required capacity to hold the compressed output. The vector can then be passed to the snappy_compress function as an output parameter. An output parameter is also passed to retrieve the true length after compression for setting the length.

use libc::{size_t, c_int};
unsafe fn snappy_compress(a: *const u8, b: size_t, c: *mut u8,
                          d: *mut size_t) -> c_int { 0 }
unsafe fn snappy_max_compressed_length(a: size_t) -> size_t { a }
fn main() {}
pub fn compress(src: &[u8]) -> Vec<u8> {
    unsafe {
        let srclen = src.len() as size_t;
        let psrc = src.as_ptr();

        let mut dstlen = snappy_max_compressed_length(srclen);
        let mut dst = Vec::with_capacity(dstlen as usize);
        let pdst = dst.as_mut_ptr();

        snappy_compress(psrc, srclen, pdst, &mut dstlen);
        dst.set_len(dstlen as usize);
        dst
    }
}

Decompression is similar, because snappy stores the uncompressed size as part of the compression format and snappy_uncompressed_length will retrieve the exact buffer size required.

use libc::{size_t, c_int};
unsafe fn snappy_uncompress(compressed: *const u8,
                            compressed_length: size_t,
                            uncompressed: *mut u8,
                            uncompressed_length: *mut size_t) -> c_int { 0 }
unsafe fn snappy_uncompressed_length(compressed: *const u8,
                                     compressed_length: size_t,
                                     result: *mut size_t) -> c_int { 0 }
fn main() {}
pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
    unsafe {
        let srclen = src.len() as size_t;
        let psrc = src.as_ptr();

        let mut dstlen: size_t = 0;
        snappy_uncompressed_length(psrc, srclen, &mut dstlen);

        let mut dst = Vec::with_capacity(dstlen as usize);
        let pdst = dst.as_mut_ptr();

        if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
            dst.set_len(dstlen as usize);
            Some(dst)
        } else {
            None // SNAPPY_INVALID_INPUT
        }
    }
}

Then, we can add some tests to show how to use them.

use libc::{c_int, size_t};
unsafe fn snappy_compress(input: *const u8,
                          input_length: size_t,
                          compressed: *mut u8,
                          compressed_length: *mut size_t)
                          -> c_int { 0 }
unsafe fn snappy_uncompress(compressed: *const u8,
                            compressed_length: size_t,
                            uncompressed: *mut u8,
                            uncompressed_length: *mut size_t)
                            -> c_int { 0 }
unsafe fn snappy_max_compressed_length(source_length: size_t) -> size_t { 0 }
unsafe fn snappy_uncompressed_length(compressed: *const u8,
                                     compressed_length: size_t,
                                     result: *mut size_t)
                                     -> c_int { 0 }
unsafe fn snappy_validate_compressed_buffer(compressed: *const u8,
                                            compressed_length: size_t)
                                            -> c_int { 0 }
fn main() { }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn valid() {
        let d = vec![0xde, 0xad, 0xd0, 0x0d];
        let c: &[u8] = &compress(&d);
        assert!(validate_compressed_buffer(c));
        assert!(uncompress(c) == Some(d));
    }

    #[test]
    fn invalid() {
        let d = vec![0, 0, 0, 0];
        assert!(!validate_compressed_buffer(&d));
        assert!(uncompress(&d).is_none());
    }

    #[test]
    fn empty() {
        let d = vec![];
        assert!(!validate_compressed_buffer(&d));
        assert!(uncompress(&d).is_none());
        let c = compress(&d);
        assert!(validate_compressed_buffer(&c));
        assert!(uncompress(&c) == Some(d));
    }
}

Destructors

Foreign libraries often hand off ownership of resources to the calling code. When this occurs, we must use Rust's destructors to provide safety and guarantee the release of these resources (especially in the case of panic).

For more about destructors, see the Drop trait.

Calling Rust code from C

You may wish to compile Rust code in a way so that it can be called from C. This is fairly easy, but requires a few things.

Rust side

First, we assume you have a lib crate named as rust_from_c. lib.rs should have Rust code as following:

#[no_mangle]
pub extern "C" fn hello_from_rust() {
    println!("Hello from Rust!");
}
fn main() {}

The extern "C" makes this function adhere to the C calling convention, as discussed below in "Foreign Calling Conventions". The no_mangle attribute turns off Rust's name mangling, so that it has a well defined symbol to link to.

Then, to compile Rust code as a shared library that can be called from C, add the following to your Cargo.toml:

[lib]
crate-type = ["cdylib"]

(NOTE: We could also use the staticlib crate type but it needs to tweak some linking flags.)

Run cargo build and you're ready to go on the Rust side.

C side

We'll create a C file to call the hello_from_rust function and compile it by gcc.

C file should look like:

extern void hello_from_rust();

int main(void) {
    hello_from_rust();
    return 0;
}

We name the file as call_rust.c and place it on the crate root. Run the following to compile:

gcc call_rust.c -o call_rust -lrust_from_c -L./target/debug

-l and -L tell gcc to find our Rust library.

Finally, we can call Rust code from C with LD_LIBRARY_PATH specified:

$ LD_LIBRARY_PATH=./target/debug ./call_rust
Hello from Rust!

That's it! For more realistic example, check the cbindgen.

Callbacks from C code to Rust functions

Some external libraries require the usage of callbacks to report back their current state or intermediate data to the caller. It is possible to pass functions defined in Rust to an external library. The requirement for this is that the callback function is marked as extern with the correct calling convention to make it callable from C code.

The callback function can then be sent through a registration call to the C library and afterwards be invoked from there.

A basic example is:

Rust code:

extern fn callback(a: i32) {
    println!("I'm called from C with value {0}", a);
}

#[link(name = "extlib")]
extern {
   fn register_callback(cb: extern fn(i32)) -> i32;
   fn trigger_callback();
}

fn main() {
    unsafe {
        register_callback(callback);
        trigger_callback(); // Triggers the callback.
    }
}

C code:

typedef void (*rust_callback)(int32_t);
rust_callback cb;

int32_t register_callback(rust_callback callback) {
    cb = callback;
    return 1;
}

void trigger_callback() {
  cb(7); // Will call callback(7) in Rust.
}

In this example Rust's main() will call trigger_callback() in C, which would, in turn, call back to callback() in Rust.

Targeting callbacks to Rust objects

The former example showed how a global function can be called from C code. However it is often desired that the callback is targeted to a special Rust object. This could be the object that represents the wrapper for the respective C object.

This can be achieved by passing a raw pointer to the object down to the C library. The C library can then include the pointer to the Rust object in the notification. This will allow the callback to unsafely access the referenced Rust object.

Rust code:

struct RustObject {
    a: i32,
    // Other members...
}

extern "C" fn callback(target: *mut RustObject, a: i32) {
    println!("I'm called from C with value {0}", a);
    unsafe {
        // Update the value in RustObject with the value received from the callback:
        (*target).a = a;
    }
}

#[link(name = "extlib")]
extern {
   fn register_callback(target: *mut RustObject,
                        cb: extern fn(*mut RustObject, i32)) -> i32;
   fn trigger_callback();
}

fn main() {
    // Create the object that will be referenced in the callback:
    let mut rust_object = Box::new(RustObject { a: 5 });

    unsafe {
        register_callback(&mut *rust_object, callback);
        trigger_callback();
    }
}

C code:

typedef void (*rust_callback)(void*, int32_t);
void* cb_target;
rust_callback cb;

int32_t register_callback(void* callback_target, rust_callback callback) {
    cb_target = callback_target;
    cb = callback;
    return 1;
}

void trigger_callback() {
  cb(cb_target, 7); // Will call callback(&rustObject, 7) in Rust.
}

Asynchronous callbacks

In the previously given examples the callbacks are invoked as a direct reaction to a function call to the external C library. The control over the current thread is switched from Rust to C to Rust for the execution of the callback, but in the end the callback is executed on the same thread that called the function which triggered the callback.

Things get more complicated when the external library spawns its own threads and invokes callbacks from there. In these cases access to Rust data structures inside the callbacks is especially unsafe and proper synchronization mechanisms must be used. Besides classical synchronization mechanisms like mutexes, one possibility in Rust is to use channels (in std::sync::mpsc) to forward data from the C thread that invoked the callback into a Rust thread.

If an asynchronous callback targets a special object in the Rust address space it is also absolutely necessary that no more callbacks are performed by the C library after the respective Rust object gets destroyed. This can be achieved by unregistering the callback in the object's destructor and designing the library in a way that guarantees that no callback will be performed after deregistration.

Linking

The link attribute on extern blocks provides the basic building block for instructing rustc how it will link to native libraries. There are two accepted forms of the link attribute today:

  • #[link(name = "foo")]
  • #[link(name = "foo", kind = "bar")]

In both of these cases, foo is the name of the native library that we're linking to, and in the second case bar is the type of native library that the compiler is linking to. There are currently three known types of native libraries:

  • Dynamic - #[link(name = "readline")]
  • Static - #[link(name = "my_build_dependency", kind = "static")]
  • Frameworks - #[link(name = "CoreFoundation", kind = "framework")]

Note that frameworks are only available on macOS targets.

The different kind values are meant to differentiate how the native library participates in linkage. From a linkage perspective, the Rust compiler creates two flavors of artifacts: partial (rlib/staticlib) and final (dylib/binary). Native dynamic library and framework dependencies are propagated to the final artifact boundary, while static library dependencies are not propagated at all, because the static libraries are integrated directly into the subsequent artifact.

A few examples of how this model can be used are:

  • A native build dependency. Sometimes some C/C++ glue is needed when writing some Rust code, but distribution of the C/C++ code in a library format is a burden. In this case, the code will be archived into libfoo.a and then the Rust crate would declare a dependency via #[link(name = "foo", kind = "static")].

    Regardless of the flavor of output for the crate, the native static library will be included in the output, meaning that distribution of the native static library is not necessary.

  • A normal dynamic dependency. Common system libraries (like readline) are available on a large number of systems, and often a static copy of these libraries cannot be found. When this dependency is included in a Rust crate, partial targets (like rlibs) will not link to the library, but when the rlib is included in a final target (like a binary), the native library will be linked in.

On macOS, frameworks behave with the same semantics as a dynamic library.

Unsafe blocks

Some operations, like dereferencing raw pointers or calling functions that have been marked unsafe are only allowed inside unsafe blocks. Unsafe blocks isolate unsafety and are a promise to the compiler that the unsafety does not leak out of the block.

Unsafe functions, on the other hand, advertise it to the world. An unsafe function is written like this:

#![allow(unused)]
fn main() {
unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }
}

This function can only be called from an unsafe block or another unsafe function.

Accessing foreign globals

Foreign APIs often export a global variable which could do something like track global state. In order to access these variables, you declare them in extern blocks with the static keyword:

#[link(name = "readline")]
extern {
    static rl_readline_version: libc::c_int;
}

fn main() {
    println!("You have readline version {} installed.",
             unsafe { rl_readline_version as i32 });
}

Alternatively, you may need to alter global state provided by a foreign interface. To do this, statics can be declared with mut so we can mutate them.

use std::ffi::CString;
use std::ptr;

#[link(name = "readline")]
extern {
    static mut rl_prompt: *const libc::c_char;
}

fn main() {
    let prompt = CString::new("[my-awesome-shell] $").unwrap();
    unsafe {
        rl_prompt = prompt.as_ptr();

        println!("{:?}", rl_prompt);

        rl_prompt = ptr::null();
    }
}

Note that all interaction with a static mut is unsafe, both reading and writing. Dealing with global mutable state requires a great deal of care.

Foreign calling conventions

Most foreign code exposes a C ABI, and Rust uses the platform's C calling convention by default when calling foreign functions. Some foreign functions, most notably the Windows API, use other calling conventions. Rust provides a way to tell the compiler which convention to use:

#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "kernel32")]
#[allow(non_snake_case)]
extern "stdcall" {
    fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
}
fn main() { }

This applies to the entire extern block. The list of supported ABI constraints are:

  • stdcall
  • aapcs
  • cdecl
  • fastcall
  • thiscall
  • vectorcall This is currently hidden behind the abi_vectorcall gate and is subject to change.
  • Rust
  • rust-intrinsic
  • system
  • C
  • win64
  • sysv64

Most of the abis in this list are self-explanatory, but the system abi may seem a little odd. This constraint selects whatever the appropriate ABI is for interoperating with the target's libraries. For example, on win32 with a x86 architecture, this means that the abi used would be stdcall. On x86_64, however, windows uses the C calling convention, so C would be used. This means that in our previous example, we could have used extern "system" { ... } to define a block for all windows systems, not only x86 ones.

Interoperability with foreign code

Rust guarantees that the layout of a struct is compatible with the platform's representation in C only if the #[repr(C)] attribute is applied to it. #[repr(C, packed)] can be used to lay out struct members without padding. #[repr(C)] can also be applied to an enum.

Rust's owned boxes (Box<T>) use non-nullable pointers as handles which point to the contained object. However, they should not be manually created because they are managed by internal allocators. References can safely be assumed to be non-nullable pointers directly to the type. However, breaking the borrow checking or mutability rules is not guaranteed to be safe, so prefer using raw pointers (*) if that's needed because the compiler can't make as many assumptions about them.

Vectors and strings share the same basic memory layout, and utilities are available in the vec and str modules for working with C APIs. However, strings are not terminated with \0. If you need a NUL-terminated string for interoperability with C, you should use the CString type in the std::ffi module.

The libc crate on crates.io includes type aliases and function definitions for the C standard library in the libc module, and Rust links against libc and libm by default.

Variadic functions

In C, functions can be 'variadic', meaning they accept a variable number of arguments. This can be achieved in Rust by specifying ... within the argument list of a foreign function declaration:

extern {
    fn foo(x: i32, ...);
}

fn main() {
    unsafe {
        foo(10, 20, 30, 40, 50);
    }
}

Normal Rust functions can not be variadic:

#![allow(unused)]
fn main() {
// This will not compile

fn foo(x: i32, ...) {}
}

The "nullable pointer optimization"

Certain Rust types are defined to never be null. This includes references (&T, &mut T), boxes (Box<T>), and function pointers (extern "abi" fn()). When interfacing with C, pointers that might be null are often used, which would seem to require some messy transmutes and/or unsafe code to handle conversions to/from Rust types. However, trying to construct/work with these invalid values is undefined behavior, so you should use the following workaround instead.

As a special case, an enum is eligible for the "nullable pointer optimization" if it contains exactly two variants, one of which contains no data and the other contains a field of one of the non-nullable types listed above. This means no extra space is required for a discriminant; rather, the empty variant is represented by putting a null value into the non-nullable field. This is called an "optimization", but unlike other optimizations it is guaranteed to apply to eligible types.

The most common type that takes advantage of the nullable pointer optimization is Option<T>, where None corresponds to null. So Option<extern "C" fn(c_int) -> c_int> is a correct way to represent a nullable function pointer using the C ABI (corresponding to the C type int (*)(int)).

Here is a contrived example. Let's say some C library has a facility for registering a callback, which gets called in certain situations. The callback is passed a function pointer and an integer and it is supposed to run the function with the integer as a parameter. So we have function pointers flying across the FFI boundary in both directions.

use libc::c_int;

#[cfg(hidden)]
extern "C" {
    /// Registers the callback.
    fn register(cb: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>, c_int) -> c_int>);
}
unsafe fn register(_: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>,
                                           c_int) -> c_int>)
{}

/// This fairly useless function receives a function pointer and an integer
/// from C, and returns the result of calling the function with the integer.
/// In case no function is provided, it squares the integer by default.
extern "C" fn apply(process: Option<extern "C" fn(c_int) -> c_int>, int: c_int) -> c_int {
    match process {
        Some(f) => f(int),
        None    => int * int
    }
}

fn main() {
    unsafe {
        register(Some(apply));
    }
}

And the code on the C side looks like this:

void register(int (*f)(int (*)(int), int)) {
    ...
}

No transmute required!

FFI and unwinding

It’s important to be mindful of unwinding when working with FFI. Most ABI strings come in two variants, one with an -unwind suffix and one without. The Rust ABI always permits unwinding, so there is no Rust-unwind ABI.

If you expect Rust panics or foreign (e.g. C++) exceptions to cross an FFI boundary, that boundary must use the appropriate -unwind ABI string. Conversely, if you do not expect unwinding to cross an ABI boundary, use one of the non-unwind ABI strings.

Note: Compiling with panic=abort will still cause panic! to immediately abort the process, regardless of which ABI is specified by the function that panics.

If an unwinding operation does encounter an ABI boundary that is not permitted to unwind, the behavior depends on the source of the unwinding (Rust panic or a foreign exception):

  • panic will cause the process to safely abort.
  • A foreign exception entering Rust will cause undefined behavior.

Note that the interaction of catch_unwind with foreign exceptions is undefined, as is the interaction of panic with foreign exception-catching mechanisms (notably C++'s try/catch).

Rust panic with "C-unwind"

#[no_mangle]
extern "C-unwind" fn example() {
    panic!("Uh oh");
}

This function (when compiled with panic=unwind) is permitted to unwind C++ stack frames.

[Rust function with `catch_unwind`, which stops the unwinding]
      |
     ...
      |
[C++ frames]
      |                           ^
      | (calls)                   | (unwinding
      v                           |  goes this
[Rust function `example`]         |  way)
      |                           |
      +--- rust function panics --+

If the C++ frames have objects, their destructors will be called.

C++ throw with "C-unwind"

#[link(...)]
extern "C-unwind" {
    // A C++ function that may throw an exception
    fn may_throw();
}

#[no_mangle]
extern "C-unwind" fn rust_passthrough() {
    let b = Box::new(5);
    unsafe { may_throw(); }
    println!("{:?}", &b);
}

A C++ function with a try block may invoke rust_passthrough and catch an exception thrown by may_throw.

[C++ function with `try` block that invokes `rust_passthrough`]
      |
     ...
      |
[Rust function `rust_passthrough`]
      |                            ^
      | (calls)                    | (unwinding
      v                            |  goes this
[C++ function `may_throw`]         |  way)
      |                            |
      +--- C++ function throws ----+

If may_throw does throw an exception, b will be dropped. Otherwise, 5 will be printed.

panic can be stopped at an ABI boundary

#![allow(unused)]
fn main() {
#[no_mangle]
extern "C" fn assert_nonzero(input: u32) {
    assert!(input != 0)
}
}

If assert_nonzero is called with the argument 0, the runtime is guaranteed to (safely) abort the process, whether or not compiled with panic=abort.

Catching panic preemptively

If you are writing Rust code that may panic, and you don't wish to abort the process if it panics, you must use catch_unwind:

use std::panic::catch_unwind;

#[no_mangle]
pub extern "C" fn oh_no() -> i32 {
    let result = catch_unwind(|| {
        panic!("Oops!");
    });
    match result {
        Ok(_) => 0,
        Err(_) => 1,
    }
}

fn main() {}

Please note that catch_unwind will only catch unwinding panics, not those that abort the process. See the documentation of catch_unwind for more information.

Representing opaque structs

Sometimes, a C library wants to provide a pointer to something, but not let you know the internal details of the thing it wants. A stable and simple way is to use a void * argument:

void foo(void *arg);
void bar(void *arg);

We can represent this in Rust with the c_void type:

extern "C" {
    pub fn foo(arg: *mut libc::c_void);
    pub fn bar(arg: *mut libc::c_void);
}
fn main() {}

This is a perfectly valid way of handling the situation. However, we can do a bit better. To solve this, some C libraries will instead create a struct, where the details and memory layout of the struct are private. This gives some amount of type safety. These structures are called ‘opaque’. Here’s an example, in C:

struct Foo; /* Foo is a structure, but its contents are not part of the public interface */
struct Bar;
void foo(struct Foo *arg);
void bar(struct Bar *arg);

To do this in Rust, let’s create our own opaque types:

#[repr(C)]
pub struct Foo {
    _data: [u8; 0],
    _marker:
        core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
#[repr(C)]
pub struct Bar {
    _data: [u8; 0],
    _marker:
        core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}

extern "C" {
    pub fn foo(arg: *mut Foo);
    pub fn bar(arg: *mut Bar);
}
fn main() {}

By including at least one private field and no constructor, we create an opaque type that we can't instantiate outside of this module. (A struct with no field could be instantiated by anyone.) We also want to use this type in FFI, so we have to add #[repr(C)]. The marker ensures the compiler does not mark the struct as Send, Sync and Unpin are not applied to the struct. (*mut u8 is not Send or Sync, PhantomPinned is not Unpin)

But because our Foo and Bar types are different, we’ll get type safety between the two of them, so we cannot accidentally pass a pointer to Foo to bar().

Notice that it is a really bad idea to use an empty enum as FFI type. The compiler relies on empty enums being uninhabited, so handling values of type &Empty is a huge footgun and can lead to buggy program behavior (by triggering undefined behavior).

NOTE: The simplest way would use "extern types". But it's currently (as of June 2021) unstable and has some unresolved questions, see the RFC page and the tracking issue for more details.

Beneath std

This section documents features that are normally provided by the std crate and that #![no_std] developers have to deal with (i.e. provide) to build #![no_std] binary crates.

Using libc

In order to build a #[no_std] executable we will need libc as a dependency. We can specify this using our Cargo.toml file:

[dependencies]
libc = { version = "0.2.146", default-features = false }

Note that the default features have been disabled. This is a critical step - the default features of libc include the std crate and so must be disabled.

Alternatively, we can use the unstable rustc_private private feature together with an extern crate libc; declaration as shown in the examples below. Note that windows-msvc targets do not require a libc, and correspondingly there is no libc crate in their sysroot. We do not need the extern crate libc; below, and having it on a windows-msvc target would be a compile error.

Writing an executable without std

We will probably need a nightly version of the compiler to produce a #![no_std] executable because on many platforms, we have to provide the eh_personality lang item, which is unstable.

Controlling the entry point is possible in two ways: the #[start] attribute, or overriding the default shim for the C main function with your own. Additionally, it's required to define a panic handler function.

The function marked #[start] is passed the command line parameters in the same format as C (aside from the exact integer types being used):

#![feature(start, lang_items, core_intrinsics, rustc_private)]
#![allow(internal_features)]
#![no_std]

// Necessary for `panic = "unwind"` builds on cfg(unix) platforms.
#![feature(panic_unwind)]
extern crate unwind;

// Pull in the system libc library for what crt0.o likely requires.
#[cfg(not(windows))]
extern crate libc;

use core::panic::PanicInfo;

// Entry point for this program.
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
    0
}

// These functions are used by the compiler, but not for an empty program like this.
// They are normally provided by `std`.
#[lang = "eh_personality"]
fn rust_eh_personality() {}
#[panic_handler]
fn panic_handler(_info: &PanicInfo) -> ! { core::intrinsics::abort() }

To override the compiler-inserted main shim, we have to disable it with #![no_main] and then create the appropriate symbol with the correct ABI and the correct name, which requires overriding the compiler's name mangling too:

#![feature(lang_items, core_intrinsics, rustc_private)]
#![allow(internal_features)]
#![no_std]
#![no_main]

// Necessary for `panic = "unwind"` builds on cfg(unix) platforms.
#![feature(panic_unwind)]
extern crate unwind;

// Pull in the system libc library for what crt0.o likely requires.
#[cfg(not(windows))]
extern crate libc;

use core::ffi::{c_char, c_int};
use core::panic::PanicInfo;

// Entry point for this program.
#[no_mangle] // ensure that this symbol is included in the output as `main`
extern "C" fn main(_argc: c_int, _argv: *const *const c_char) -> c_int {
    0
}

// These functions are used by the compiler, but not for an empty program like this.
// They are normally provided by `std`.
#[lang = "eh_personality"]
fn rust_eh_personality() {}
#[panic_handler]
fn panic_handler(_info: &PanicInfo) -> ! { core::intrinsics::abort() }

If you are working with a target that doesn't have binary releases of the standard library available via rustup (this probably means you are building the core crate yourself) and need compiler-rt intrinsics (i.e. you are probably getting linker errors when building an executable: undefined reference to `__aeabi_memcpy'), you need to manually link to the compiler_builtins crate to get those intrinsics and solve the linker errors.

#[panic_handler]

#[panic_handler] is used to define the behavior of panic! in #![no_std] applications. The #[panic_handler] attribute must be applied to a function with signature fn(&PanicInfo) -> ! and such function must appear once in the dependency graph of a binary / dylib / cdylib crate. The API of PanicInfo can be found in the API docs.

Given that #![no_std] applications have no standard output and that some #![no_std] applications, e.g. embedded applications, need different panicking behaviors for development and for release it can be helpful to have panic crates, crate that only contain a #[panic_handler]. This way applications can easily swap the panicking behavior by simply linking to a different panic crate.

Below is shown an example where an application has a different panicking behavior depending on whether is compiled using the dev profile (cargo build) or using the release profile (cargo build --release).

panic-semihosting crate -- log panic messages to the host stderr using semihosting:

#![no_std]

use core::fmt::{Write, self};
use core::panic::PanicInfo;

struct HStderr {
    // ..
    _0: (),
}

impl HStderr {
    fn new() -> HStderr { HStderr { _0: () } }
}

impl fmt::Write for HStderr {
    fn write_str(&mut self, _: &str) -> fmt::Result { Ok(()) }
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    let mut host_stderr = HStderr::new();

    // logs "panicked at '$reason', src/main.rs:27:4" to the host stderr
    writeln!(host_stderr, "{}", info).ok();

    loop {}
}

panic-halt crate -- halt the thread on panic; messages are discarded:

#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

app crate:

#![no_std]

// dev profile
#[cfg(debug_assertions)]
extern crate panic_semihosting;

// release profile
#[cfg(not(debug_assertions))]
extern crate panic_halt;

fn main() {
    // ..
}