공통 루트 슈퍼트레이트를 공유하는 트레이트 객체 사이의 안전하고 상수 시간이며 최소 공간 오버헤드의 캐스팅을 Rust에 도입하는 RFC입니다.
bounded_intertrait_casting
공통 루트 슈퍼트레이트를 공유하는 트레이트 객체 사이의 안전하고 상수 시간이며 최소 공간 오버헤드의 캐스팅. bounded 트레이트 그래프는 하나의 명시적으로 선언된 슈퍼트레이트를 루트로 갖는 그래프이며, 그 루트는 캐스트가 대상으로 삼을 수 있는 트레이트들의 폐쇄를 이름 붙인다. 따라서 컴파일러는 타입별 메타데이터 테이블을 전역적으로 계산할 수 있고, 각 캐스트를 두 번의 로드와 한 번의 분기로 해결할 수 있다. 사용자 관점의 표면은 cast!(in dyn Root, expr => dyn U) 매크로(try_cast! 및 unchecked_cast! 변형 포함)이며, 참조, &mut, 그리고 소유된 Box/Rc/Arc에 대해 동작한다. 생태계의 해결책과 달리, 이 캐스팅은 'static, 전역 레지스트리, TypeId를 요구하지 않으며, 크레이트 경계와 제네릭 인스턴스화 전반에서도 올바름을 유지한다.
pub trait Root: TraitMetadataTable<dyn Root> {} pub trait Sub: Root { fn greet(&self); }
let r: &dyn Root = /* … /; match cast!(in dyn Root, r => dyn Sub) { Ok(s) => s.greet(), // r implemented Sub Err(_) => { / r did not implement Sub */ } }
Rust의 트레이트 객체는 강력한 추상화와 동적 다형성을 가능하게 하지만, 오늘날 언어에는 비자명한 트레이트 계층에서 관련된 트레이트 객체들 사이를 변환하는 안전하고 원칙적이며 효율적인 메커니즘이 없다. 실제로 대규모 Rust 코드베이스는 하나의 구체 타입이 개념적으로 같은 동작 "그래프"에 속하는 여러 트레이트를 구현하는 상호 관련된 트레이트들의 패밀리를 일상적으로 정의한다. 이러한 상황에서는 다음과 같은 변환을 원하게 되는 것이 자연스럽다.
&dyn TraitA를 &dyn TraitB로 변환하기'static 제약, 런타임 레지스트리, 또는 맞춤형 장치 없이 수행하기오늘날 이것은 Rust가 안전하거나 인체공학적으로 표현할 수 있는 것이 아니다.
생태계의 해결책은 존재하지만, 모두 근본적인 단점을 공유한다. 이들은 전역 레지스트리, 동적 맵, TypeId 조회, 또는 사용자가 관리하는 메타데이터에 의존한다. 이러한 접근은 런타임 의존성을 도입하고, 올바른 등록 규율을 요구하며, 성능 및 최적화 상의 불이익을 부과한다. 이들은 드물게만 상수 시간이고, 종종 'static 수명을 강제하며, 제네릭과의 상호작용도 좋지 않고, 크레이트 경계를 넘으면 취약하다.
한편 컴파일러는 이미 이 문제를 올바르게 해결하는 데 필요한 전역 지식을 갖고 있다. 모노모피제이션 이후 컴파일러는 사실상 다음을 안다.
그러나 Rust에는 현재 이 정보를 인터트레이트 캐스팅에 안전하게 노출하고 활용할 수 있는 메커니즘이 없다.
이 RFC는 명시적으로 선언된 "슈퍼 트레이트"를 루트로 하는 제한된 인터트레이트 캐스팅을 위한 언어 수준 기능을 제안한다. 주어진 계층에 참여하는 모든 타입에 대해, 컴파일러는 어떤 트레이트가 구현되었는지와 그 트레이트에 어떻게 도달하는지를 설명하는 전역적이고 타입별인 메타데이터를 계산한다. 이를 통해 다음이 가능해진다.
'static 전용 캐스팅이 아니라 완전한 수명 올바름개념적으로 이 기능은 C++의 dynamic_cast나 JVM 계열 언어의 인터페이스 캐스팅과 같은 틈새를 채우지만, Rust의 컴파일 및 트레이트 시스템에 맞게 설계되었다. 이는 더 풍부한 트레이트 계층, 더 유연한 동적 다형성, 더 표현력 있는 API 설계를 가능하게 하면서도 Rust의 zero-cost abstraction 원칙과 일관성을 유지한다.
요약하면, 개발자들은 이미 인터트레이트 캐스팅을 원하고 있으며, 오늘날 생태계의 해결책은 수요를 입증하지만 근본적으로 제약되어 있다. 이 RFC는 인터트레이트 캐스팅을 Rust의 일급 기능으로 만들기 위한 건전하고 효율적이며 언어가 지원하는 경로를 제공한다.
Rust는 트레이트를 제한된 트레이트 계층의 root 로 선언할 수 있게 한다. 그 루트를 전이적으로 상속하는 모든 트레이트는 하나의 trait graph 를 형성하고, 루트를 구현하는 모든 타입은 그 그래프의 구성원이 된다. 그래프 내부에서 cast! 매크로는 트레이트 객체 참조(Box, Rc, Arc 안의 소유된 트레이트 객체 포함) 사이를 상수 시간에 변환하며, 대상 트레이트가 구현되지 않았거나 캐스트가 수명 소거를 위반할 경우 Err를 반환한다.
루트 슈퍼트레이트는 TraitMetadataTable 슈퍼트레이트 바운드에 자기 자신을 이름 붙여 선언한다.
pub trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { }
자기 참조적 형태 — 즉 그 트레이트 자신의 dyn 타입이 슈퍼트레이트 바운드에 나타나는 것 — 가 SuperTrait를 루트로 표시하고, dyn SuperTrait에 범위가 지정된 캐스트에 메타데이터 테이블을 보이게 만든다. 이 RFC의 본문은 때때로 이를 TraitMetadataTable<dyn Self>로 적는데, 이는 "선언 중인 루트 X에 대해 TraitMetadataTable<dyn X>"의 축약 표기이다. dyn Self는 유효한 Rust 문법이 아니며 실제 선언에는 나타나지 않는다.
직접이든 전이적이든 SuperTrait를 슈퍼트레이트로 이름 붙이는 모든 트레이트는 SuperTrait를 루트로 하는 그래프에 참여한다. 작은 실행 예시는 다음과 같다.
pub trait Trait1: SuperTrait { } pub trait Trait2: SuperTrait { } pub trait Trait3: Trait1 + Trait2 { }
struct S; impl SuperTrait for S { } impl Trait1 for S { } impl Trait2 for S { } impl Trait3 for S { }
let s: &dyn SuperTrait = &S;
// 특정 서브트레이트로 다운캐스트: let t1 = cast!(in dyn SuperTrait, s => dyn Trait1).unwrap();
// 캐스트 체인: 일단 &dyn Trait1을 얻으면, 구체 타입으로 돌아가지 않고도 // 형제 또는 자손으로 이동할 수 있다. let t3 = cast!(in dyn SuperTrait, t1 => dyn Trait3).unwrap();
// 누락된 impl은 panic이 아니라 Err를 반환한다: struct Loner; impl SuperTrait for Loner { } let l: &dyn SuperTrait = &Loner; assert!(cast!(in dyn SuperTrait, l => dyn Trait1).is_err());
세 가지 속성이 이 설계를 이끈다.
SuperTrait로 가는 전이 경로가 없음)는 캐스트 대상으로 나타날 수 없으며, 시도하면 컴파일 타임 오류다.cast!는 루트를 문맥으로 받는다 — cast!(in dyn SuperTrait, …) — 메타데이터 테이블은 루트별이기 때문이다. 같은 루트를 공유하는 임의의 두 트레이트는 직접 관계를 선언하지 않아도 서로 캐스트할 수 있다.TypeId 없음, 'static 요구 없음.이 속성들을 실험하는 포괄적인 네 가지 타입 / 여섯 가지 트레이트 매트릭스는 Appendix A: Trait-graph worked examples 에 있다.
타입은 여러 루트 슈퍼트레이트를 구현하여 둘 이상의 그래프에 참여할 수 있다. 모든 캐스트는 정확히 하나의 루트에 범위가 지정되므로, 서로 분리된 그래프 사이의 캐스트는 컴파일 타임 오류다. 슈퍼트레이트 체인이 두 루트 모두에 도달하는 트레이트는 어느 쪽에서도 캐스트 대상으로 사용할 수 있다.
pub trait SuperA: TraitMetadataTable<dyn SuperA> { } pub trait SuperB: TraitMetadataTable<dyn SuperB> { }
pub trait ATrait: SuperA { } pub trait BTrait: SuperB { } pub trait Shared: ATrait + BTrait { }
// COMPILE ERROR: ATrait and BTrait have no common root. // cast!(in dyn SuperA, some_a => dyn BTrait) // // OK: Shared is reachable from both SuperA and SuperB. // cast!(in dyn SuperA, some_a => dyn Shared) // cast!(in dyn SuperB, some_b => dyn Shared)
두 루트를 모두 구현하는 타입은 메타데이터 테이블도 두 개 — 루트별 하나씩 — 를 갖고, 캐스트는 in 절과 일치하는 테이블을 조회한다. 공유와 부분 구현을 포함한 예시는 Appendix A: Multiple roots 에 있다.
제네릭 루트는 다른 트레이트와 마찬가지로 모노모피제이션된다. dyn SuperTrait<u8>와 dyn SuperTrait<u16>는 구별되는 루트이며 각기 다른 그래프다. 구체 파라미터에 고정된 서브트레이트(Trait1: SuperTrait<u8>)는 일치하는 루트에만 참여하고, 같은 파라미터에 대해 제네릭인 서브트레이트(Trait2<T>: SuperTrait<T>)는 자신의 인스턴스화와 같은 루트를 공유하는 쪽에 참여한다. 자세한 내용은 Appendix A: Generic roots 를 보라.
핵심 규칙은 소거된 수명은 계속 소거된 채로 남는다. 구체 타입 C<'a, ...>가 &dyn SuperTrait로 강제 변환될 때, SuperTrait의 시그니처(메서드, 연관 타입, 슈퍼트레이트 바운드)에 나타나지 않는 C의 수명 파라미터는 트레이트 객체 뒤에 존재적으로 숨겨진다. 그 수명은 여전히 기반 값을 제한하지만, 트레이트 객체 타입은 그것을 참조할 방법이 없다. 이후 서브트레이트로 캐스트할 때는 그에 대해 새로운 바인딩을 발명해서는 안 된다.
이 규칙이 금지하는 불건전한 패턴 — 호출자가 'b를 선택하고, 다운캐스트한 뒤, 실제 수명은 'a였던 &'b T를 읽는 경우 — 는 다음과 같다.
pub trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { } pub trait Trait1<'a>: SuperTrait { fn f(&self) -> &'a u8; } struct S1<'a> { a: &'a u8 } impl<'a> SuperTrait for S1<'a> { } impl<'a> Trait1<'a> for S1<'a> { fn f(&self) -> &'a u8 { self.a } }
fn inner<'a, 'b>(s: &(dyn SuperTrait + 'a)) -> &'b u8 {
// Rejected: 'a was erased on the way into dyn SuperTrait,
// so Trait1<'b> cannot be reselected with a fresh 'b.
cast!(in dyn SuperTrait + 'a, s => dyn Trait1<'b> + 'a).unwrap().f()
}
형식적 진술 — 서브트레이트의 모든 수명은 루트의 수명들로 표현 가능해야 하고, 수명 간 관계는 소거 전후에 보존되어야 한다 — 은 Reference-level explanation: Lifetime Erasure 에 있다. dyn Sub<Assoc = &'a T>와 같은 연관 타입 바인딩을 통해서만 나타나는 수명까지 포함하여, 모든 바운드 수명이 여기에 참여한다.
'static is special in trait selection
트레이트 타입 파라미터는 불변이므로, SubTrait<'static>와 SubTrait<'a>는 진짜로 다른 트레이트 객체 타입이다. 캐스트는 이 차이를 존중한다.
SubTrait<'static>만 구현하는 값은 비-'static인 'a에 대해 SubTrait<'a>로 캐스트될 수 없고, 그 반대도 마찬가지다.impl<'a> SubTrait<'a> for S<'static>처럼 작성된 impl은 사실상 for<'a> SubTrait<'a>를 만족하므로 어떤 인스턴스화로도 캐스트된다.impl<'a> SubTrait<'static> for S<'a>처럼 작성된 impl은 S의 구체 수명과 무관하게 오직 SubTrait<'static>로만 캐스트된다.이 경우들의 전체 매트릭스는 Appendix A: Lifetime selection 에 전개되어 있다.
impl은 선택 술어가 되는 outlives 술어(where 'b: 'a)를 가질 수 있다. 캐스트는 이를 보존한다. where 'b: 'a로 보호된 impl은 호출자가 호출 지점에서 그 관계를 증명할 수 있을 때만 허용된다. 따라서 타입 시그니처는 같지만 impl 술어가 다른 두 구조체는 서로 다른 캐스트 동작을 보인다 — Appendix A: Multiple lifetimes 를 보라.
trait graph 레이아웃이 최종 확정되는 산출물이 바로 global crate 이다. 보통 바이너리, staticlib, 또는 cdylib가 이에 해당한다. 즉 완전히 모노모피제이션된 트레이트 그래프를 볼 수 있는 크레이트다. 각 산출물은 자신만의 레이아웃을 독립적으로 계산하고, 메타데이터 테이블에 고유한 정체성을 태그한다.
요약하면, 트레이트와 구조체 정의가 양쪽에서 문자 그대로 동일하더라도 캐스트는 결코 global-crate 경계를 넘지 않는다. 소스 객체와 호출 지점이 서로 다른 정체성을 갖고 있으면 Err(TraitCastError::ForeignTraitGraph)를 반환한다.
왜 이 제약이 핵심적인가: 공통 라이브러리 C에 의존하는 독립적으로 빌드된 두 cdylib A와 B는 각자 고립된 상태에서 자기 레이아웃을 계산한다. A가 ATrait에 부여한 인덱스는 B가 BTrait에 부여한 인덱스와 충돌할 수 있다. 로더가 B에서 만든 객체를 A에서 만든 캐스트에 넘기면, 정체성 검사가 없다면 잘못된 슬롯을 조용히 읽게 된다. 주소 비교는 이런 인덱스 우연한 일치와 무관하게 그러한 캐스트를 거부한다.
공유 스키마를 C에서 미리 계산할 수 없는 더 깊은 이유는 트레이트 그래프가 지연 모노모피제이션 되기 때문이다. dyn Trait2<DownstreamType>는 downstream 크레이트가 그것을 인스턴스화하기 전까지는 C의 관점에서 존재하지 않는다. 따라서 C에서의 어떤 사전 계산도 downstream 크레이트가 미래에 발명할 모든 인스턴스화를 포괄하는 정준 레이아웃을 고정할 수 없다. 동적 레지스트리는 런타임에 새 vtable을 코드 생성해야 하고, 이는 사실상 컴파일러의 일부를 배포하는 것과 같다. 따라서 이 RFC는 그 경로를 거부한다.
결과적으로, 다음의 경우에도 global-crate 경계를 넘는 캐스트는 거부된다.
C)에 정의되어 있는 경우이 실패 모드를 끝까지 재현하는 cdylib 예시는 Appendix B: Cross-crate cdylib example 에 있다.
이 절은 사용자에게 보이는 계약을 두 번에 나누어 정의한다. 먼저 정의, 핵심 타입, 그리고 루트별 레이아웃을 노출하는 intrinsic을 설명하고, 이어서 캐스트 의미론 자체 — TraitCast 트레이트, cast! 계열 매크로, 수명 소거 규칙, 캐스트가 관찰하는 메타데이터 테이블 구조, 그리고 캐스트 지점 코드 생성이 언제 최종 확정되는지 — 를 진단과 함께 설명한다. 이 계약을 실현하는 구현 장치 — 지연 코드 생성, call_id 체인, GenericArgKind::Outlives, 메타데이터 테이블을 조립하는 전역 단계 쿼리 — 는 Appendix C: Implementation sketch (non-normative) 에 있다. 아래의 의미론을 보존하기만 한다면, 적합한 구현은 그 세부 사항들에서 얼마든지 달라질 수 있다.
슈퍼트레이트: trait Subtrait where Self: Supertrait {} 형태만을 뜻한다. T: Supertrait 위의 blanket trait는 포함하지 않는다.
루트 슈퍼트레이트: 타입이 트레이트 그래프의 유효한 인스턴스로 간주되기 위해 반드시 구현해야 하는 최소/최상위 슈퍼트레이트. 이 RFC의 모든 예시에서 SuperTrait가 루트 슈퍼트레이트다.
Outlives class: 같은 서브트레이트를 구현하는 모든 타입에 대해 균일하지 않은 impl 선택 민감 region 관계를 인코딩하는, 서브트레이트별 유일한 클래스.
구체적으로, 같은 서브트레이트의 두 impl은 주어진 호출 지점에서 impl이 선택 가능한지를 결정하는 서로 다른 region 요구사항을 가질 수 있다. 이러한 각 요구사항 — 또는 요구사항이 전혀 없음 — 은 서로 다른 outlives class를 형성한다.
trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { } trait Sub<'a, 'b>: SuperTrait { }
// Two outlives classes for Sub<'a, 'b>:
// class C0: no predicate (always admissible)
// class C1: 'b: 'a (admissible only when the caller can prove it)
impl<'a, 'b> Sub<'a, 'b> for S0<'a, 'b> { } // class C0
impl<'a, 'b> Sub<'a, 'b> for S1<'a, 'b> where 'b: 'a { } // class C1
캐스트 대상 dyn Sub<'a, 'b>는 캐스트 지점에서 알려진 outlives 관계를 바탕으로 클래스를 선택한다. 그 클래스를 만족하는 impl을 가진 타입은 캐스트 가능하고, 그렇지 않은 타입은 Err로 떨어진다. 전체 레이아웃 규칙은 Metadata Table / Table Entries 에 있다.
위의 Cross-crate boundaries and cdylibs 에서 도입되었다. 타입 시스템 정보가 최대가 되는 지점을 나타내는 크레이트다. 어떤 downstream 또는 sibling 크레이트도 upstream 트레이트에 대한 새로운 트레이트나 새로운 모노모피제이션을 트레이트 그래프에 추가할 수 없다.
트레이트 그래프는 지연적 이다. 캐스트 대상으로 나타나는 트레이트만 포함된다.
한 번의 컴파일에서 정확히 하나의 크레이트가 global crate로 지정된다. 기본적으로 지정은 크레이트 타입에 의해 결정된다.
이 기본값은 컴파일 시점에 재정의 가능해야 한다. 그래야 비표준 산출물(예를 들어 dlopen으로 로드되고 정적으로 부트스트랩되는 것으로 알려진 dylib)이 opt in 또는 opt out 할 수 있다. 다중 산출물 빌드 드라이버(Cargo 등)는 기존의 crate-type 선택을 통해 이를 제어한다.
각 global crate는 고유 주소의 형태를 갖는 고유 식별자로 태그되며, 이 식별자는 해당 크레이트가 사용하는 트레이트 메타데이터 테이블과 인덱스를 식별하는 데 쓰인다. 이 주소들이 가지는 계약과, 코드 생성, LTO, 링크를 거치는 동안 global-crate별 유일성이 유지되도록 하는 backend 의무는 아래 Identity tokens 를 보라.
기본 정책은 의도적으로 보수적이다. 이론상 더 관대한 global-crate 선택이 가능할 수 있는 프로그램에서도 메타데이터 테이블과 인덱스가 링크 목적상 존재함을 보장한다. 예를 들어, 호스트 프로세스와 많은 코드를 공유하는 dlopen 로드 Rust 코드 생성 크레이트는 이론상 캐스팅과 관련하여 같이 컴파일될 수 있다. 그러나 이를 가능하게 하려면 이 RFC의 범위를 벗어나는 컴파일러 변경이 필요하므로, 이 RFC는 Rust 코드 생성 생태계에 대한 변경을 제안하지 않으며 외부 코드 생성 크레이트와의 호환성에도 영향을 주지 않는다. 플러그인 아키텍처는 ahead-of-time 최적화와 긴장 관계에 있으며, 이 RFC는 후자를 선호한다.
global-crate 상태를 노출하는 rustc 내부 표면(tcx.is_global_crate()와 -Z global_crate=yes|no 재정의)은 Appendix C §C.0 에 설명되어 있다.
TraitMetadataTable은 트레이트가 루트 슈퍼트레이트가 되도록 opt-in 하는 마커다. 사용자는 다음과 같이 선언한다.
pub trait Root: TraitMetadataTable<dyn Root> {}
이 선언으로부터 컴파일러는 Root를 구현하는 모든 구체 타입에 대해 루트별 메타데이터 테이블을 계산하기 시작한다. 사용자는 TraitMetadataTable을 직접 구현하지 않는다. blanket impl이 모든 Sized 타입을 덮기 때문에, impl Root for T만 작성하면 충분하다. 트레이트의 rustc 내부 형태(언어 아이템 마커, coinduction attribute, blanket impl, 순환 회피 논리)는 Appendix C §C.0 에 있다.
/// The table is computed only for the global crate. It is satisfied
/// for every type that implements the root supertrait; SuperTrait
/// must be a trait-object type (dyn Trait).
pub trait TraitMetadataTable<SuperTrait>: MetaSized
where
SuperTrait: MetaSized + Pointee<Metadata = DynMetadata<SuperTrait>>,
{
/// The returned slice is a static array of all trait vtables for
/// this concrete type. Its order is implementation-defined and
/// unstable, but constant for a given SuperTrait. Must not
/// dereference any part of self. (Lowering this to a "virtual
/// const" rather than a virtual function call is a desired future
/// optimization; this RFC does not require it.)
fn derived_metadata_table(&self) -> (&'static u8, NonNull<Option<NonNull<()>>>);
}
네 개의 컴파일러 intrinsic이 캐스트가 관찰하는 루트별 레이아웃을 노출한다. 사용자 코드는 TraitCast impl과 cast! 매크로를 통해서만 이에 도달하며, 이들은 unstable이고 공개 표면의 일부가 아니다(Stability 참조). rustc 내부 attribute(#[rustc_intrinsic], #[rustc_nounwind])는 여기서 생략한다. Appendix C §C.0 를 보라.
/// Retrieve the index of Trait's vtable in the slice returned via
/// TraitMetadataTable::derived_metadata_table. The index includes
/// the outlives-class offset, computed during the global phase from
/// lifetime relationships at the call site. The specific value is
/// implementation-defined and unstable; it is constant for a given
/// Trait and SuperTrait but not const fn because the global
/// computation is required. The &'static u8 is a per-global-crate
/// identity token, independent of the generic params.
pub unsafe fn trait_metadata_index<SuperTrait, Trait>() -> (&'static u8, usize)
where SuperTrait: MetaSized + Pointee<Metadata = DynMetadata<SuperTrait>> + TraitMetadataTable<SuperTrait>,
Trait: MetaSized + Pointee<Metadata = DynMetadata<Trait>> + TraitMetadataTable<SuperTrait>;
/// Retrieve the slice returned via
/// TraitMetadataTable::derived_metadata_table for the given
/// SuperTrait. Calling this intrinsic forces the caller to be
/// delayed until after global monomorphization. The value is
/// constant for a given ConcreteType and SuperTrait but not
/// const fn because the global computation is required.
pub unsafe fn trait_metadata_table<SuperTrait, ConcreteType>() -> (&'static u8, NonNull<Option<NonNull<()>>>)
where SuperTrait: MetaSized + Pointee<Metadata = DynMetadata<SuperTrait>> + TraitMetadataTable<SuperTrait>,
ConcreteType: Sized + TraitMetadataTable<SuperTrait>;
/// Return the length of the metadata table for the given
/// SuperTrait. Separate from the table itself so optimizations can
/// eliminate OoB checks.
pub unsafe fn trait_metadata_table_len<SuperTrait>() -> usize
where SuperTrait: MetaSized + Pointee<Metadata = DynMetadata<SuperTrait>> + TraitMetadataTable<SuperTrait>;
/// Return true iff casting to TargetTrait (within the graph rooted
/// at SuperTrait) is safe with respect to lifetime erasure. Checks
/// that every lifetime in TargetTrait's binder is expressible
/// through SuperTrait's binder and that the concrete outlives
/// relationships at the call site establish equivalence. Resolved
/// during the global phase when generic parameters may transitively
/// contain lifetimes; otherwise resolved earlier. Separated from the
/// table entries to facilitate lifetime binders.
pub unsafe fn trait_cast_is_lifetime_erasure_safe<SuperTrait, TargetTrait>() -> bool
where SuperTrait: MetaSized + Pointee<Metadata = DynMetadata<SuperTrait>> + TraitMetadataTable<SuperTrait>,
TargetTrait: MetaSized + Pointee<Metadata = DynMetadata<TargetTrait>> + TraitMetadataTable<SuperTrait>;
각 trait-cast intrinsic은 첫 번째 요소가 global crate에 대한 identity token 인 (&'static u8, …) 튜플을 반환한다. 같은 global crate 안에서 얻은 두 토큰은 주소 비교 시 같아야 하고, 독립적으로 컴파일된 서로 다른 global crate에서 얻은 두 토큰은 주소 비교 시 달라야 한다. 역참조한 u8의 값은 지정되지 않는다. 중요한 것은 오직 주소다.
캐스트 지점 코드는 이를 이용해 foreign-graph 캐스트를 거부한다. trait_metadata_table이 반환한 주소와 trait_metadata_index가 반환한 주소를 비교하고, 다르면 캐스트를 거부한다. 이것이 가이드의 Cross-crate boundaries and cdylibs 에서 설명한 ForeignTraitGraph 경로의 메커니즘이며, 양쪽의 트레이트와 구조체 정의가 문자 그대로 동일하더라도 global-crate 간 캐스트를 거부한다.
Backend obligation. 적합한 backend와 linker는 주소 비중요 상수를 병합할 수 있는 모든 단계 — LLVM의 unnamed_addr 식 병합, linker ICF, 그리고 그와 유사한 모든 컴파일 단위 간 중복 제거 — 를 가로질러 토큰 비동등성을 보존해야 한다. 만약 그런 단계가 토큰을 병합하면, 달라야 할 정체성 검사가 같게 비교되고, Err(ForeignTraitGraph)를 반환해야 하는 캐스트가 잘못된 테이블에 대해 성공할 수 있다. global-crate 간 거부의 건전성은 이 의무에 달려 있다.
backend나 linker가 이 보장을 제공할 수 없는 빌드는 이 기능에 대해 지원되는 구성에 해당하지 않는다. 비-LLVM backend에서의 stabilization은 backend가 이 의무를 존중하거나(또는 동등한 메커니즘을 제공하거나) 해야 한다. Unresolved questions / Non-LLVM backend enforcement of address\_significant 를 보라. rustc가 오늘날 이 의무를 만족시키기 위해 사용하는 구체적 메커니즘은 규범적이지 않으며, Appendix C §C.6 에 설명되어 있다.
주어진 캐스트가 Ok를 반환할지 Err를 반환할지는 구현 세부가 아니라 의미론적 계약의 일부다. 아래 API와 impl은 이 규칙을 실현하며, 그것을 어떻게 발전시킬 수 있는지에 대한 정책은 Stability / Evolution policy 에 있다.
checked_cast가 Ok를 반환하는 것은 다음이 모두 성립할 때 그리고 오직 그때뿐이다.
그 외의 경우 캐스트는 Err를 반환하며, 어떤 variant를 고를지는 아래의 트레이트 정의에 따른다. unchecked_cast는 (3)을 제거한 동일한 규칙을 사용하며, (3)은 호출자의 안전 의무가 된다.
use core::ptr::{Pointee, DynMetadata}; use core::marker::{MetaSized, PointeeSized};
/// In core.
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum TraitCastError<T> {
/// This object is from a different global crate than the one
/// that is performing the cast.
/// Useful if you'd like to provide a more informative error message.
/// Note: do not rely on this behavior. It is subject to change.
ForeignTraitGraph(T),
/// This object does not implement the specified trait, or the cast does not
/// satisfy lifetime erasure requirements.
UnsatisfiedObligation(T),
}
impl<T> TraitCastError<T> {
/// Recover the contained, un-casted, value. Does not panic — both variants
/// carry the original operand so a failed cast can be retried or returned
/// to the caller unchanged.
pub fn into_inner(self) -> T {
match self {
Self::ForeignTraitGraph(v) | Self::UnsatisfiedObligation(v) => v,
}
}
}
/// I is the root supertrait.
/// In a future extension, the root supertrait could be implied. Regardless of the specific root supertrait the result of
/// the cast is the same, since the output vtable will be the same after monomorphization
/// (or is essentially user-invisible).
pub trait TraitCast<I: MetaSized, U: MetaSized>: Sized
where I: Pointee<Metadata = DynMetadata<I>> + TraitMetadataTable<I>,
U: Pointee<Metadata = DynMetadata<U>> + TraitMetadataTable<I>,
{
type Target;
/// Attempt to cast self to U. All trait impl-obligations are enforced,
/// but lifetime-erasure soundness is not.
///
/// # Safety
/// The caller must ensure that the cast is lifetime-erasure safe.
/// Prefer checked_cast or cast unless you have verified erasure safety
/// through other means (e.g., lifetime binder implementations).
///
/// Returns Err(TraitCastError::UnsatisfiedObligation) if the cast is not
/// possible due to unfulfilled generic obligations.
/// Returns Err(TraitCastError::ForeignTraitGraph) if the cast is not
/// possible because the object is from a different global crate.
unsafe fn unchecked_cast(self) -> Result<Self::Target, TraitCastError<Self>>;
/// Attempt to cast self to U.
///
/// Returns Err(TraitCastError::ForeignTraitGraph) if the cast is not
/// possible because the object is from a different global crate.
/// Returns Err(TraitCastError::UnsatisfiedObligation) if the cast is not
/// possible due to lifetime erasure requirements or because of unfulfilled
/// generic obligations.
fn checked_cast(self) -> Result<Self::Target, TraitCastError<Self>> {
if !core::intrinsics::trait_cast_is_lifetime_erasure_safe::<I, U>() {
return Err(TraitCastError::UnsatisfiedObligation(self));
}
unsafe { self.unchecked_cast() }
}
/// Same as checked_cast, but strips TraitCastError::* from the return type.
fn cast(self) -> Result<Self::Target, Self> {
self.checked_cast().map_err(TraitCastError::into_inner)
}
}
impl<'r, T, U, I> TraitCast<I, U> for &'r T
where I: Pointee<Metadata = DynMetadata<I>> + TraitMetadataTable<I> + 'r,
T: MetaSized + TraitMetadataTable<I>,
U: MetaSized + Pointee<Metadata = DynMetadata<U>> + TraitMetadataTable<I> + 'r,
{
type Target = &'r U;
unsafe fn unchecked_cast(self) -> Result<&'r U, TraitCastError<Self>> {
unsafe {
let (obj_graph_id, table) = <T as TraitMetadataTable<I>>::derived_metadata_table(self);
let (crate_graph_id, idx) = crate::intrinsics::trait_metadata_index::<I, U>();
if crate_graph_id as *const u8 != obj_graph_id as *const u8 {
return Err(TraitCastError::ForeignTraitGraph(self));
}
let table_len = crate::intrinsics::trait_metadata_table_len::<I>();
let table: &[Option<NonNull<()>>] =
&*crate::ptr::from_raw_parts(table.as_ptr(), table_len);
let (p, _) = (self as *const T).to_raw_parts();
let Some(Some(vtable)) = table.get(idx) else {
return Err(TraitCastError::UnsatisfiedObligation(self));
};
Ok(&*crate::ptr::from_raw_parts(p, crate::mem::transmute(vtable)))
}
}
}
impl<'r, T, U, I> TraitCast<I, U> for &'r mut T
where I: Pointee<Metadata = DynMetadata<I>> + TraitMetadataTable<I> + 'r,
T: MetaSized + TraitMetadataTable<I>,
U: MetaSized + Pointee<Metadata = DynMetadata<U>> + TraitMetadataTable<I> + 'r,
{
type Target = &'r mut U;
// Body mirrors &'r T's, using *mut T, from_raw_parts_mut, and a
// final &mut * to rebuild the reference.
unsafe fn unchecked_cast(self) -> Result<&'r mut U, TraitCastError<Self>> { /* ... */ }
}
/// In alloc
impl<'a, T, U, I, A> TraitCast<I, U> for Box<T, A>
where I: Pointee<Metadata = DynMetadata<I>> + TraitMetadataTable<I>,
T: MetaSized + TraitMetadataTable<I> + 'a,
U: MetaSized + Pointee<Metadata = DynMetadata<U>> + TraitMetadataTable<I> + 'a,
A: Allocator,
{
type Target = Box<U, A>;
// Body mirrors &'r T's, using Box::into_raw_with_allocator and
// Box::from_raw_with_allocator (and re-wrapping on the Err paths so
// the caller gets back the original Box).
unsafe fn unchecked_cast(self) -> Result<Box<U, A>, TraitCastError<Self>> { /* ... */ }
}
/// In alloc
impl<'a, T, U, I, A> TraitCast<I, U> for Rc<T, A>
where I: MetaSized + Pointee<Metadata = DynMetadata<I>> + TraitMetadataTable<I>,
T: MetaSized + TraitMetadataTable<I> + 'a,
U: MetaSized + Pointee<Metadata = DynMetadata<U>> + TraitMetadataTable<I> + 'a,
A: Allocator,
{
type Target = Rc<U, A>;
// Body mirrors Box's, using Rc::into_raw_with_allocator and
// Rc::from_raw_in (and re-wrapping on the Err paths so the caller
// gets back the original Rc).
unsafe fn unchecked_cast(self) -> Result<Rc<U, A>, TraitCastError<Self>> { /* ... */ }
}
/// In alloc
impl<'a, T, U, I, A> TraitCast<I, U> for Arc<T, A>
where I: MetaSized + Pointee<Metadata = DynMetadata<I>> + TraitMetadataTable<I>,
T: MetaSized + TraitMetadataTable<I> + 'a,
U: MetaSized + Pointee<Metadata = DynMetadata<U>> + TraitMetadataTable<I> + 'a,
A: Allocator,
{
type Target = Arc<U, A>;
// Body mirrors Box's, using Arc::into_raw_with_allocator and
// Arc::from_raw_in.
unsafe fn unchecked_cast(self) -> Result<Arc<U, A>, TraitCastError<Self>> { /* ... */ }
}
위의 &'r T impl이 정준 본문이며, 다른 네 impl은 주석에서 언급한 포인터 재구성 helper만 다르다. 이 다섯 impl(&T, &mut T, Box<T, A>, Rc<T, A>, Arc<T, A>)이 제안되는 완전한 집합이다. Pin<P>, raw pointer(*const T / *mut T), NonNull<T>에 대한 impl은 여기서는 범위 밖이며, 추가는 Future possibilities 에서 논의한다.
/// In core; re-exported in std.
/// Attempt to cast $e to $u in the trait graph of $i.
/// Returns Err($e) if the cast is not possible.
#[macro_export]
macro_rules! cast {
(in $i:ty, $e:expr => $u:ty) => {{
core::trait_cast::TraitCast::<$i, $u>::cast($e)
}};
}
/// In core; re-exported in std.
/// Attempt to cast $e to $u in the trait graph of $i.
///
/// Returns Err(TraitCastError::ForeignTraitGraph) if the cast is not
/// possible because the object is from a different global crate.
/// Returns Err(TraitCastError::UnsatisfiedObligation) if the cast is not
/// possible due to lifetime erasure requirements or because of unfulfilled
/// generic obligations.
#[macro_export]
macro_rules! try_cast {
(in $i:ty, $e:expr => $u:ty) => {{
core::trait_cast::TraitCast::<$i, $u>::checked_cast($e)
}};
}
/// In core; re-exported in std.
/// Unsafely attempt to cast $e to $u in the trait graph of $i.
///
/// All trait impl-obligations are enforced, but lifetime-erasure soundness is
/// not.
///
/// # Safety
/// The caller must ensure that the cast is lifetime-erasure safe.
///
/// Returns Err(TraitCastError::UnsatisfiedObligation) if the cast is not
/// possible due to unfulfilled generic obligations.
/// Returns Err(TraitCastError::ForeignTraitGraph) if the cast is not
/// possible because the object is from a different global crate.
#[macro_export]
macro_rules! unchecked_cast {
(in $i:ty, $e:expr => $u:ty) => {{
core::trait_cast::TraitCast::<$i, $u>::unchecked_cast($e)
}};
}
TraitCast를 통한 다운캐스팅은 소거 이후에 수명을 만들어낼 수 없어야 한다. 비형식적으로 말하면, 타입의 수명 구조 일부를 소거한 뒤에는 다운캐스트하면서 더 "긴" 수명을 다시 도입할 수 없다.
이것이 허용할 불건전한 패턴은 다음과 같다.
C<'a, ...>로부터 생성된 vtable을 갖는 트레이트 객체 &dyn SuperTrait에서 시작한다.C의 수명 파라미터를 소거한다.dyn SubTrait<'b, ...>로 캐스트하고, 'b가 원래의 'a와 호환되지 않더라도 마치 기반의 C<'b, ...>가 존재하는 것처럼 다룬다.이를 막기 위해, TraitCast에 참여할 수 있는 트레이트 그래프와 소거된 파라미터를 추적하는 방식을 다음과 같이 제한한다.
루트 슈퍼트레이트 I와, I의 메타데이터 테이블에 나타날 수 있는 임의의 서브트레이트 J에 대해, J의 공개 인터페이스(메서드 시그니처, 연관 타입, 슈퍼트레이트 제약)에 나타날 수 있는 모든 수명 파라미터는 I의 수명 파라미터들의 관점에서 표현 가능해야 한다.
구체적으로, J의 region 파라미터들에서 I의 region 파라미터들로의 매핑이 존재해야 하고, 모든 합법적 인스턴스화에 대해 J가 사용하는 region들이 I가 사용하는 region보다 더 오래 살지 않아야 한다. 직관적으로는 루트 슈퍼트레이트의 수명들이 일종의 "closure"를 형성하여, 거기에서 도달 가능한 임의의 트레이트를 통해 흐르는 모든 수명을 제한해야 한다. 그래야 I까지 소거하더라도 서브트레이트 수명 건전성을 검사하는 데 필요한 정보를 잃지 않는다.
이는 예를 들어, 비제네릭 루트가 다음과 같이 되는 경우를 의미한다.
pub trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { }
pub trait Trait1<'a>: SuperTrait { ... }
이 경우는 다운캐스트 안전한 그래프에 참여할 수 없다. SuperTrait에는 Trait1<'a>의 'a를 제한할 수 있는 region 파라미터가 없기 때문이다.
구체 타입 C<…>를 루트 슈퍼트레이트 dyn I<…>로 unsizing 하여 트레이트 객체를 만들 때, I의 공개 인터페이스에 나타나지 않는 C의 타입/수명 파라미터는 객체 뒤에 존재적으로 숨겨진다. 이 소거 단계 이후 프로그램은 트레이트 그래프를 따라 다운캐스트함으로써 그러한 숨겨진 파라미터에 대해 새 인스턴스화를 "선택"할 수 없어야 한다.
참고: 이것은 unsizing 자체를 수정하지 않는다.
이 제약들을 함께 적용하면, 루트 슈퍼트레이트로 unsizing 한 뒤의 모든 성공적인 다운캐스트가 원래 구체 값에 존재하던 것보다 더 긴 수명을 만들어내거나, 그 값을 통해 도달 가능한 참조들의 수명을 연장할 수 없음을 보장한다.
trait_cast_is_lifetime_erasure_safe
trait_cast_is_lifetime_erasure_safe intrinsic은 TargetTrait로 캐스트하는 것이(SuperTrait를 루트로 하는 그래프 내부에서) 수명 소거 관점에서 안전한지를 검사하는 데 사용된다. 소스 트레이트는 중요하지 않다. 소스는 unsizing 중에 이미 루트로 소거되었기 때문이다. 따라서 유일한 질문은 루트→대상 binder 매핑이 수명 정체성을 보존하는지 여부다. 이 검사는 수명 binder를 다루기 쉽도록 메타데이터 테이블 엔트리와 분리되어 있다.
제네릭 캐스트 대상(예: dyn SubTrait<'a, T>에서 T가 전이적으로 수명을 포함할 수 있는 경우)은 이 계약의 나머지 부분에는 없는 질문을 제기한다. 캐스트가 선택하는 outlives-class 슬롯과 trait_cast_is_lifetime_erasure_safe의 응답은 모두 모노모피제이션 이후에야 완전히 알려지는 수명 관계에 의존하는데, 그 시점에는 보통 수명이 소거되기 때문이다. 계약은 다음과 같다.
ReErased로 남는다. 이 기능의 어떤 경로도 MIR에 region을 보존하거나 되살리지 않는다.ParamEnv에 의존하지 않는다. 슬롯을 선택하고 trait_cast_is_lifetime_erasure_safe에 답하는 outlives 증거는 호출 지점 자신의 outlives 그래프이며, 모노모피제이션을 통해 전달된다.구현 스케치 — borrowck region summary, GenericArgKind::Outlives arg kind, 이를 실현하는 call-chain composer — 는 Appendix C §C.3 에 있다.
메타데이터 테이블의 각 위치는 다음 두 가지의 쌍에 대응한다.
수명 관계는 impl 선택 술어이며, 같은 트레이트의 서로 다른 impl마다 다를 수 있기 때문에(즉 각 타입마다 다를 수 있기 때문에) 각 트레이트를 여러 엔트리로 확장해야 한다.
예를 들어:
trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { } trait Trait1<'a, 'b>: SuperTrait { }
struct S1<'a, 'b> { // ... } impl<'a, 'b> SuperTrait for S1<'a, 'b> { } impl<'a, 'b> Trait1<'a, 'b> for S1<'a, 'b> where 'b: 'a, { } struct S2<'a, 'b> { // ... } impl<'a, 'b> SuperTrait for S2<'a, 'b> { } impl<'a, 'b> Trait1<'a, 'b> for S2<'a, 'b> { }
// The SuperTrait metadata table layout will need to have three entries:
// 1. The vtable for SuperTrait
// 2. The vtable for Trait1<'a, 'b>
// 3. The vtable for Trait1<'a, 'b> where 'b: 'a
// For a given set of lifetimes, the tables for S1 and S2 would look like this: // // [ S1 Table ] [ S2 Table ] // +----------------------------------+ +----------------------------------+ // | 0: vtable for SuperTrait | | 0: vtable for SuperTrait | // +----------------------------------+ +----------------------------------+ // | 1: None (no base Trait1 impl) | | 1: vtable for Trait1<'a, 'b> | // +----------------------------------+ +----------------------------------+ // | 2: vtable for Trait1 (if 'b: 'a) | | 2: vtable for Trait1 (implied) | // +----------------------------------+ +----------------------------------+
여기서 보여준 세 엔트리 레이아웃은 pre-condensation 관점이다. 실제 레이아웃은 impl_universally_admissible(아래 fast-path 절 참조)을 적용하고, 허용 가능한 impl 집합이 동일한 outlives class들을 공유 슬롯으로 응축한다. 참여 impl들이 impl별 outlives 술어나 Self/trait-param 공유를 가지지 않는 트레이트의 경우, 모든 클래스가 하나의 슬롯으로 붕괴하므로 실제 프로그램에서의 흔한 경우는 (sub_trait, OutlivesClass) 쌍당 하나의 슬롯이 아니라 도달 가능한 서브트레이트당 하나의 슬롯이다.
즉 테이블 인덱스는 트레이트 "ID"와 outlives 관계 그래프 "sub-index"를 함께 인코딩한다.
레이아웃은 global crate에서만 실행되며 implementation-defined 이고 unstable이다. 우연한 의존을 막기 위해 슬롯 순서는 무작위로 섞일 수 있다. 계약은 관찰 가능한 세 단계로 표현된다.
impl_universally_admissible을 통과하면(아래 참조), 모든 클래스는 하나의 슬롯으로 붕괴한다. 서브트레이트 자신의 where 'a: 'b 스타일 술어에서 유도된 where-clause 기반 outlives class도 접어 넣어서, 제네릭 라이브러리 코드를 통해 유효한 outlives 증거를 가진 캐스트가 올바른 슬롯을 찾도록 한다.(root, concrete) 쌍에 대해, 각 슬롯은 vtable을 담거나 None이다. 주어진 루트에 공급되는 모든 구체 타입에서 테이블은 균일하므로, 그래프의 어떤 구체 타입은 만족하지만 다른 구체 타입은 만족하지 않는 슬롯이 하나라도 있으면 None 엔트리는 피할 수 없다. 런타임에서 트레이트 만족 여부는 널에 대한 단일 분기다.수명 소거 제약을 위반하는 트레이트라도 레이아웃에는 남아 있다. trait_cast_is_lifetime_erasure_safe가 그들에 대한 unsafe 캐스트를 막아 주며, 수명 binder 구현을 위한 unsafe 탈출구도 제공한다.
impl_universally_admissible fast path
impl_universally_admissible(impl_def_id: DefId) -> bool는 impl의 선택이 호출자의 outlives 문맥과 무관한지 — 즉 모든 dyn binder 구조에 대한 모든 outlives class 아래에서 그 impl이 허용 가능한지 — 를 결정한다. 주어진 서브트레이트의 모든 참여 impl이 이 검사를 통과하면, 레이아웃은 클래스별 전체 허용 가능성 분석을 건너뛰고 해당 서브트레이트의 모든 outlives class를 하나의 슬롯으로 붕괴시킨다.
기준은 다음과 같다.
'static)이 없어야 한다. 모든 trait-ref region은 ReEarlyParam 또는 ReBound여야 한다.longer 또는 shorter로 갖는 RegionOutlives where-clause가 없어야 한다.Self에도 나타나면 안 된다. SelfTy와 트레이트의 generic args 양쪽에 모두 나타나는 Self-anchored param은 Self unsizing에 의해 구체 값의 소거된 수명에 고정되지만, impl의 generic-arg 위치는 호출자의 outlives 문맥에 의존할 수 있다. 이 둘이 보편적으로 일치하려면 impl이 해당 파라미터를 'static으로 강제해야만 한다. 이 검사는 엄격한 경로를 택해 Self와 trait ref 사이에서 파라미터를 공유하는 모든 impl을 거부한다.inherent impl(트레이트 ref 없음)은 자명하게 허용 가능하다.
코드 크기에 대한 결과: 한 루트의 그래프 안에 있는 모든 서브트레이트의 모든 참여 impl에 대해 허용 가능성이 성립하면, 레이아웃은 도달 가능한 서브트레이트당 한 슬롯으로 붕괴하고, trait_metadata_index 호출 지점은 outlives class와 무관하게 모두 그 동일한 슬롯으로 해석된다. 따라서 사용자 함수는 outlives class별로 복제되지 않는다. impl별 outlives 술어나 Self/trait-param 공유가 없는 캐스트 대상 impl을 가진 프로그램은 구조적으로 허용 가능하며, 이것이 흔한 경우다.
캐스트 지점 코드는 메타데이터 테이블 레이아웃이 알려지기 전에는 완전히 코드 생성될 수 없고, 레이아웃은 global crate에서만 알려진다. 사용자에게 보이는 결과는 다음과 같다.
ForeignTraitGraph 거부 경로다(가이드의 Cross-crate boundaries and cdylibs 참조).Stability는 세 표면을 서로 독립적으로 다룬다. API, 선언적 캐스트 계약, 그리고 그 아래의 구현이다.
안정화 시 다음은 stable surface의 일부가 된다.
TraitCast 트레이트와 그 다섯 impl(&T, &mut T, Box<T, A>, Rc<T, A>, Arc<T, A>)cast!, try_cast!, unchecked_cast! 매크로(최종 경로는 Unresolved questions 에 따름)#[non_exhaustive]로 표시된 TraitCastError<T>. 기존 ForeignTraitGraph와 UnsatisfiedObligation variant는 stable이며, #[non_exhaustive]는 추후 UnsatisfiedObligation을 더 세밀한 variant들로 분해하더라도 이미 exhaustive match를 하는 사용자 코드를 깨뜨리지 않도록 여지를 남긴다.
모든 캐스트를 지배하는 선언적 Ok-iff 규칙은 TraitCast / Cast contract 아래에 적혀 있다. 이것은 이 기능의 stable semantic contract의 일부이며, 아래 정책이 그 규칙이 어떻게 진화할 수 있는지 지배한다.
Err → Ok 반전은 허용된다. rustc N에서 Err를 반환하던 캐스트가 rustc N+1에서는 Ok를 반환할 수 있다. 이는 이전에 거부되던 프로그램이 수용되도록 만드는 것과 같은 종류의 변화에서 비롯된다. 더 정밀한 허용 가능성 추론, NLL/Polonius 완화, outlives solver의 개선 등이 조항 (2)를 통해 캐스트 동작에 흘러들어온다. 제어 흐름 신호로서 캐스트의 실패 에 의존하는 크레이트는, borrow checker 정밀도에 의존하는 모든 동작에 대해 이미 받아들이고 있는 것과 같은 노출을 감수하게 된다.Ok → Err 반전은 파괴적 변경 이며 오직 건전성 수정의 일부로서만 허용된다. 다른 모든 건전성 동기 언어 변경과 같은 위상이다. 그러한 수정이 필요할 때는 표준 unsound-feature 절차(future-incompat lint, edition migration, 또는 심각도에 따른 직접적 파괴)를 따른다.이는 오늘날의 trait solver와 borrow checker가 관리되는 방식과 닮아 있다. 선언적 규칙은 안정적이고, 결정 절차는 단조롭게 개선될 자유가 있으며, 허용되는 유일한 파괴는 건전성을 위한 것이다.
다음은 implementation-defined이며, 하나의 stable release series 안에서도 언제든 바뀔 수 있다.
trait_metadata_index가 부여하는 슬롯 순서와 인덱스 값trait_metadata_table의 per-(root, concrete) 메타데이터 테이블 레이아웃, 순서, 내용Instance의 name mangling과, 지연 코드 생성 파이프라인의 다른 모든 내부 사항core::intrinsics::trait_metadata_* intrinsic은 영구적으로 unstable 상태를 유지한다. 사용자 코드는 stable한 TraitCast / cast! 표면을 통해서만 이 기능에 접근한다.
unused_cast_target lint는 슬롯은 존재하지만 도달 불가능한(프로그램 안의 어떤 구체 타입도 구현하지 않는) 캐스트 대상을 다룬다.
위의 선언적 캐스트 계약이 이 기능이 제공하는 유일한 stable contract다. 특정 인덱스 값, &'static u8 identity token들 사이의 주소 관계, 슬롯 인접성, 기타 메타데이터 테이블의 모든 관찰 가능한 속성은 계약 바깥이며 어떤 릴리스에서든 바뀔 수 있다. 프로그램은 여기에 의존해서는 안 된다.
아래의 모든 컴파일 타임 진단은 별도 언급이 없는 한 typeck 또는 trait solving 중에 방출된다.
cast! 표현식의 대상 트레이트가 루트 슈퍼트레이트를 (전이적) 슈퍼트레이트로 가지지 않을 때 방출된다.
error[E0XXX]: `Trait2` is not in the trait graph rooted at `SuperTrait`
--> src/main.rs:10:5
|
10| cast!(in dyn SuperTrait, &s => dyn Trait2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `Trait2` does not have `SuperTrait` as a (transitive) supertrait
= help: add `SuperTrait` as a supertrait bound on `Trait2`
마찬가지로, 소스 트레이트 객체 타입이 루트의 그래프 안에 없을 때도 방출된다.
TraitMetadataTable bound on root supertrait
트레이트가 cast! 표현식에서 루트 슈퍼트레이트로 사용되었지만, TraitMetadataTable<dyn Self>를 슈퍼트레이트 바운드로 갖지 않을 때 방출된다.
error[E0XXX]: `Root` cannot be used as a cast root: missing `TraitMetadataTable` bound
--> src/main.rs:5:1
|
5 | pub trait Root {}
| -------------- `TraitMetadataTable<dyn Root>` is not a supertrait of `Root`
|
= help: add a supertrait bound: `trait Root: TraitMetadataTable<dyn Root> {}`
TraitMetadataTable type argument must be a trait object
트레이트 선언이 TraitMetadataTable<T>를 슈퍼트레이트로 이름 붙였는데 T가 dyn Trait 타입이 아닐 때 방출된다. TraitMetadataTable 장치는 트레이트 객체에 대해서만 정의된다(그 blanket impl은 T: Pointee<Metadata = DynMetadata<T>>를 요구한다). 따라서 non-dyn 인자는 해당 바운드를 inhabit할 수 없게 만들며, 저자가 의도한 것일 가능성도 거의 없다.
error[E0XXX]: `TraitMetadataTable` type argument must be a trait object
--> src/main.rs:5:23
|
5 | pub trait ChildTrait: TraitMetadataTable<u32> {}
| ^^^^^^^^^^^^^^^^^^^^^^^
| |
| `u32` is not a `dyn Trait` type
|
= note: `TraitMetadataTable<T>` requires
`T: Pointee<Metadata = DynMetadata<T>>`, which holds only for
trait objects
= help: use `dyn Self` to declare `ChildTrait` as a cast root, or
`dyn R` for a cast-root supertrait `R` of `ChildTrait`
TraitMetadataTable type argument
트레이트 선언이 TraitMetadataTable<dyn X>를 슈퍼트레이트로 이름 붙였는데, dyn X가 dyn Self도 아니고(이 경우 이 트레이트 자체를 캐스트 루트로 선언하게 된다), 캐스트 루트인 전이적 슈퍼트레이트 R에 대한 dyn R도 아닐 때 방출된다. 이러한 바운드는 blanket impl에 의해 만족되기는 하지만, 트레이트를 어떤 도달 가능한 캐스트 그래프에도 넣지 않으므로 거의 항상 사용자 실수다.
error[E0XXX]: `TraitMetadataTable` type argument does not match a cast root
--> src/main.rs:7:25
|
5 | pub trait Root: TraitMetadataTable<dyn Root> {}
| ---- cast root
6 | pub trait Unrelated: TraitMetadataTable<dyn Unrelated> {}
| --------- unrelated cast root
7 | pub trait ChildTrait: Root + TraitMetadataTable<dyn Unrelated> {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| `dyn Unrelated` is not a (transitive)
| supertrait of `ChildTrait`
|
= note: on a trait `Tr`, a `TraitMetadataTable<dyn X>` supertrait
bound requires `X = Self` (declaring `Tr` as a cast root) or
`X = R` for some transitive supertrait `R` of `Tr` that is
itself a cast root
= help: subtraits inherit `TraitMetadataTable<dyn Root>` from their
root — the explicit bound is usually unnecessary
= help: if you meant to place `ChildTrait` in `Root`'s graph, write
`TraitMetadataTable<dyn Root>`; if you meant `ChildTrait` to
be its own root, write `TraitMetadataTable<dyn ChildTrait>`
두 진단 모두 어떤 cast! 표현식이 이 트레이트를 언급하는지와 무관하게, 트레이트 정의 시점에 방출된다.
서브트레이트가 루트 슈퍼트레이트의 수명 파라미터들로는 표현할 수 없는 수명 파라미터를 도입할 때 방출된다.
error[E0XXX]: trait graph rooted at `SuperTrait` is not downcast-safe
--> src/main.rs:8:1
|
4 | pub trait SuperTrait: TraitMetadataTable<dyn SuperTrait> {}
| ---------- root supertrait has no lifetime parameters
...
8 | pub trait Sub<'a>: SuperTrait { fn f(&self) -> &'a u8; }
| ^^ lifetime `'a` is not bounded by any lifetime on `SuperTrait`
|
= note: downcasting to `dyn Sub<'a>` could manufacture lifetimes
that were erased when unsizing to `dyn SuperTrait`
= help: add a lifetime parameter to the root: `trait SuperTrait<'a>: ...`
이 진단은 캐스트 지점에서만이 아니라, 루트 슈퍼트레이트가 알려진 즉시(트레이트 정의 시점에) eager하게 방출된다. 따라서 현재 크레이트에 아무 캐스트도 작성되지 않았더라도 라이브러리 작성자가 오류를 받게 된다.
캐스트 표현식의 대상 트레이트가 object-safe(dyn-compatible)하지 않을 때 방출된다.
error[E0XXX]: `NotObjectSafe` cannot be made into a trait object
--> src/main.rs:12:5
|
12| cast!(in dyn SuperTrait, &s => dyn NotObjectSafe)
| ^^^^^^^^^^^^^ `NotObjectSafe` is not dyn-compatible
이는 기존 object-safety 진단을 재사용한다.
다음은 typeck가 아니라 전역 코드 생성 단계(모노모피제이션 이후)에서 노출된다.
No global crate. 지연된 cast-intrinsic 요청은 포함하지만 global crate가 없는 컴파일은 ill-formed이다. 구현은 직접 감지 가능한 경우 명확한 진단을 내야 한다. 예를 들어 -Z global_crate=no로 컴파일된 최종 산출물이 그러하다. 드라이버가 "나중에 링크될 의도의 라이브러리"와 구분할 수 없는 경우(특히 독립 dylib)는 미해결 cast-intrinsic 심볼에 대한 일반적인 링크 실패로 저하된다. 이때 심볼 이름은 linker 메시지만으로도 의미가 자명하도록 선택되어야 한다.
Unused cast-target trait pruned (lint, off by default). 어떤 트레이트가 trait_metadata_index 인스턴스화에서 캐스트 대상으로 나타나지만 최종 바이너리의 어떤 구체 타입도 이를 만족하지 않으면, 그 트레이트의 인덱스는 도달 불가능으로 설정된다. 선택적 lint(unused_cast_target)는 이에 대해 경고할 수 있다.
warning: cast target `dyn Trait4` is unreachable in the trait graph of `dyn SuperTrait`
--> src/main.rs:15:5
|
15| cast!(in dyn SuperTrait, &s => dyn Trait4)
| ^^^^^^
|
= note: no type implementing `SuperTrait` also implements `Trait4`
= note: this cast will always return `Err` at runtime
= note: `#[warn(unused_cast_target)]` on by default
이 RFC를 수용하면 언어와 컴파일러는 새로운 표면들과 의무들의 집합에 커밋하게 된다. 이 절은 검토자가 동기와 비용을 비교할 수 있도록 그것들을 모아 놓는다. 각 항목은 이 기능이 대체하는 기존 생태계 크레이트들과 비교한 단점이 아니라, 현 상태에 비한 단점이다.
이 설계는 수명 소거, 모노모피제이션, 크레이트 간 링크를 가로지른다. 구체적으로 다음이 추가된다.
TraitMetadataTable)와 네 개의 컴파일러 intrinsicGenericArgKind variant(Outlives)Call / TailCall terminator를 통해 threading되고 MIR inliner 전반에서 보존되는 call_id 체인Instance별로 패치된 MIR body를 공급받을 수 있는 codegen_mir 쿼리borrowck_region_summary, vid_provenance)address_significant 플래그개별적으로는 이국적인 요소가 아니지만, 이들의 조합은 컴파일러 보장의 표면적을 크게 넓힌다. 특히 전역 단계 장치는 건전성에 핵심적이며(Identity tokens 참조), 그 부분의 회귀는 typeck 오류가 아니라 캐스트 지점에서의 Ok / Err 뒤바뀜으로 나타난다.
사용자가 자신의 트레이트 계층에서 캐스팅을 도입하려면 최소한 다음을 배워야 한다.
trait Root: TraitMetadataTable<dyn Root>)ForeignTraitGraph 실패의 존재와 의미이는 유의미한 교육 비용이다. 일부 규칙(특히 수명 소거)은 미묘해서 사용자는 진단에 부딪히며 배우게 될 가능성이 높다. 문서화, 진단 품질, worked example은 사후 작업이 아니라 안정화 노력의 일부가 된다.
수명 오류는 이미 사용자 혼란의 주요 원인 중 하나다. 이 기능은 수명 오류 또는 그에 준하는 형태로 나타나는 세 가지 새로운 실패 모드를 추가한다.
trait_cast_is_lifetime_erasure_safe가 false를 반환했기 때문에 캐스트가 런타임에 Err(UnsatisfiedObligation)을 반환할 수 있다.unused_cast_target lint와 전역 단계 진단은 정적 사례에는 도움이 되지만, 런타임에 드러나는 소거-안전성 실패는 새롭다. 즉 프로그램의 다른 곳에서의 borrow-checker 정밀도가, 겉보기에 "맞아 보이는" 캐스트가 Ok를 반환하는지 여부에 영향을 주는 경우다. 이 경로에 대해 유용한 진단을 만들려면 상당한 작업이 필요하다.
DelayedInstance가 전역 단계 입력이기 때문에 다시 컴파일될 수 있다.sccache 같은 도구는 크레이트 단위 작업을 키로 삼는다. 전역 단계는 여러 크레이트에 걸쳐 있으므로, 기존 캐시 가정과 좋지 않게 상호작용할 수 있다. 의존성 재사용을 위해 결정적 rmeta hash에 의존하는 빌드 시스템은 그 hash를 전역 단계 입력까지 포함하도록 확장해야 한다.impl<'a, 'b> Trait for S<'a, 'b> where 'b: 'a) 캐스트에 도달하는 사용자 함수가 outlives class별로 복제될 수 있다. 실제로는 레이아웃이 outlives 동등 클래스를 응축하고, impl_universally_admissible이 흔한 경우에는 클래스를 완전히 붕괴하므로, 중복은 수명이 대상 트레이트 수명으로 흐르는 CFG로 제한되고, 트레이트 캐스팅이 사용되지 않을 때는 사라진다. 그럼에도 최악의 경우는 실제이며, 경계를 정하는 것이 중요하다.(root, concrete) 쌍마다 하나의 [Option<NonNull<()>>; N], 여기서 N은 해당 루트의 도달 가능한 슬롯 수다. pruning이 N을 타이트하게 유지하지만, 어떤 슬롯이 그래프의 적어도 하나의 구체 타입에는 만족되고 다른 구체 타입에는 만족되지 않는 이상 None 엔트리는 피할 수 없다. 후속 작업으로 vtable을 기준 주소에서 오프셋한 Option<NonMaxU32>로 엔트리를 줄여 테이블 크기를 절반으로 줄일 수 있다.unnamed_addr 억제된 1바이트 할당 하나 추가. 무시 가능하다.
캐스트는 하나의 그래프 내부에서만 동작하므로, 서로 관련이 없더라도 트레이트들을 공유 루트 슈퍼트레이트 아래에 두도록 하는 실제 설계 유인이 생긴다. 더 좁은 계층을 선호하는 작성자는 여전히 여러 독립 루트를 정의할 수 있지만, 그 대가로 루트 사이에서 캐스트할 수 없게 된다. 두 루트 모두에서 대상으로 삼아야 하는 트레이트는 양쪽 모두를 슈퍼트레이트로 선언해야 한다(Appendix A.2 참조). 시간이 지나면 이 압력은 널리 쓰이는 라이브러리에서 몇몇 "god root"를 선호하는 경향으로 굳어질 수 있다. 이는 object-safe한 Error가 사실상 오류 타입의 루트가 된 것과 비슷하다. 바람직할 수도 있고 아닐 수도 있지만, 오늘날의 언어가 그런 방향으로 밀지는 않는다.
identity-token 계약(Identity tokens)은 코드 생성 backend가 할당별 address_significant 플래그를 존중하는 데 의존한다. LLVM은 이를 UnnamedAddr::No로 만족한다. Cranelift와 GCC는 현재 활성 주소 병합 패스가 없으므로 플래그를 기록만 하고 사용하지 않는다. 미래에 어느 backend든 ICF류 병합을 도입하면 이 플래그 또는 동등한 메커니즘을 존중해야 한다. 그런 장치가 없는 비-LLVM backend는 이 기능이 안정화되었을 때 건전하게 호스팅할 수 없다(Unresolved questions / Non-LLVM backend enforcement 참조).
마찬가지로, v0 심볼 mangler는 GenericArgKind::Outlives를 인코딩하도록 지정되어 있지만, legacy mangler에는 해당 인코딩이 없다. 증강된 Instance에 대해 legacy mangler로 fallback 하는 경우는 지원되지 않는다(Unresolved questions / Legacy symbol mangler 참조).
제안된 API 시그니처는 MetaSized와 Pointee<Metadata = DynMetadata<…>>를 참조하는데, 이 둘은 아직 진화 중이다. 이 기능의 안정화는 이들이 여기 적힌 슈퍼트레이트 바운드와 호환되는 형태에 있음을 전제한다. 둘 중 하나라도 변경되면(예: MetaSized가 다른 방식으로 분리되거나, DynMetadata가 파라미터를 추가하게 되면) 여기의 트레이트 시그니처를 다시 설계해야 한다.
설계상, 트레이트 캐스트는 global-crate 경계를 절대 넘지 않는다(Cross-crate boundaries and cdylibs). 이는 dlopen된 산출물 간에 하나의 트레이트 그래프를 공유하고 싶어하는 동적 플러그인 아키텍처를 배제한다. 이런 패턴은 오늘날 intercast 같은 생태계 크레이트를 통해, 성능과 'static을 희생하는 대가로 가능하다. 그런 사용 사례를 가진 사용자는 일급 해결책 없이 남게 된다. 이 RFC는 상수 시간 캐스팅과 엄격한 건전성을 위해 그들의 표현력을 희생한다. 의도된 트레이드오프이지만, 분명한 트레이드오프다.
dyn Trait 합성과 부정 추론. 어떤 impl이 존재할 수 있는지를 제한하는 미래 기능들(예: impl !Trait, specialization)은 이 기능의 허용 가능성 규칙과 상호작용해야 한다. 알려진 blocker는 없지만, 상호작용은 지정되어 있지 않다.intercast crate: dyn Trait에서 dyn Trait로 캐스팅. 트레이트 vtable을 저장하기 위해 전역 hashmap을 사용한다. 캐스팅은 상수 시간이 아니며 virtual dispatch가 필요하다.traitcast crate: AoT 시점의 트레이트 그래프 지식과 런타임 type/trait registry를 요구한다. 캐스팅은 상수 시간이 아니며 virtual dispatch가 필요하다.내부적으로 이 크레이트들은 모두 std::any::Any/TypeId를 사용한다. 한 트레이트 객체를 다른 트레이트 객체로 캐스트하기 위해 두 단계 과정을 따른다.
그러나 이 접근에는 몇 가지 단점이 있다.
std::any::Any 때문에 'static 수명을 강제한다.가능한 다른 접근도 있지만, 공개된 크레이트에서 구현된 것으로 보이지는 않는다. rustc_public을 사용해 트레이트 구현과 타입을 노출하는 것이다. 이 접근은 그 자체로는 지연 코드 생성을 허용하지 않는다. 먼저 트레이트 vtable을 추출하기 위한 완전한 컴파일을 하고, 그 다음 구축된 vtable 테이블을 사용할 수 있는 두 번째 컴파일이 필요하다. 추가 우회책 없이는 크레이트 간에도 동작하지 않는다.
cast! surface syntax
cast!(in $root:ty, $e:expr => $u:ty) 형태는 미학이 아니라 macro_rules! follow-set 규칙에 의해 제약된다. $e:expr 조각 뒤에는 문법상 다음 토큰으로 =>, ,, ;만 올 수 있다. 따라서 자연스러워 보이는 $e as dyn U / $e as dyn U in dyn Root 형태는 선언적 매크로로는 표현할 수 없다. 허용되는 구분자 중 =>만이 캐스트 화살표처럼 읽히고, 선행하는 in $root, 절은 루트를 $e:expr 뒤가 아니라 앞에 둘 수 있게 해 준다. 제약은 바로 그 follow-set이다.
고려한 대안:
e.cast::<Root, U>(). 가능하지만 루트를 turbofish 안에 숨기고, 포인터 타입의 메서드처럼 읽히므로 언어 수준 캐스트처럼 느껴지지 않는다. as 캐스트와의 시각적 유사성도 잃는다.$e as dyn U. 위와 같은 macro follow-set 제약에 막힌다. proc-macro나 내장 구문이 필요하다. 미래의 언어 수준 캐스트는 이를 재검토하고 소스의 트레이트 객체 타입에서 루트를 추론할 수 있겠지만, 그러려면 라이브러리 매크로가 아니라 컴파일러 내장 표면이 필요하다.$e :> dyn U 등). 같은 follow-set 문제에 걸리며, 더 넓은 정당화 없이 새 연산자 비슷한 토큰을 도입한다.in 키워드는 순전히 매크로 내부 마커 토큰으로 재사용될 뿐이다. 새로운 contextual keyword가 아니며, 매크로 matcher 밖의 문법에는 나타나지 않는다. 미래에 내장 캐스트 구문으로 옮겨가면 자유롭게 제거할 수 있다.
가이드에서 말했듯, 이 제안은 동적으로 로드된 트레이트 그래프를 지원하지 않는다.
SubTrait1 to SubTrait2
Lifetime Erasure 규칙은 SuperTrait에서 SubTrait1/SubTrait2로 가는 경로에 대해서만 정의되며, 본질적으로 모든 캐스트를 다운캐스트로 취급한다. 이렇게 해야 하는 이유는 테이블 엔트리 의무를 타입별로가 아니라 트레이트 객체별로만 검사할 수 있기 때문이다. 즉 한 번만, 루트 슈퍼트레이트를 기준으로 검사할 수 있다.
대안은 캐스트마다 값비싼 검사를 추가하는 것이다. 각 캐스트는 소스 트레이트와 대상 트레이트의 수명에 대한 컴파일러 생성 인코딩된 lifetime relationship graph를 비교해야 한다. 후자는 메타데이터 테이블 엔트리에 살아 있어야 한다. 최소한 extra memcmp가 필요하고, 완전 일반성에서는 rooted graph isomorphism 문제와 동등하다.
Lifetime Erasure or Downcast-Safety 의 규칙("루트 슈퍼트레이트에 의한 서브트레이트의 region closure")에 대한 대칭적 대안은, closure 의무를 루트가 아니라 unsize site 에 두는 것이다. 강제 변환 C<'a, ...> -> &dyn SuperTrait에서 컴파일러는 구체 타입의 수명 파라미터와 그 프로그램 지점에서 성립하는 outlives 관계를 알고 있다. 이 관계들을 증강된 Instance 데이터로 unsizing에 포착할 수 있다. 이는 서브트레이트 impl의 impl-selection outlives class에 사용하는 것과 같은 장치다(Metadata Table / Table Entries 와 Appendix C §C.3 참조). 그리고 그것을 vtable/테이블 선택에 접어 넣을 수 있다. 그렇게 하면 다운캐스트는 unsize site가 인증한 테이블 변형에 대해서만 성공한다.
사용자에게 보이는 효과는, 현재 비제네릭 루트와 region-제네릭 서브트레이트(예: Lifetime Erasure or Downcast-Safety 아래의 Trait1<'a>: SuperTrait 예시)를 금지하는 제약을 부분적으로 완화할 수 있다는 것이다. 그런 그래프도 각 unsize site가 서브트레이트의 region을 고정하기에 충분한 outlives 증거를 들고 있다면 허용 가능해진다.
이 RFC는 그 경로를 택하지 않는다. 세 가지 우려가 이 선택을 이끈다.
모든 캐스트 지점이 아니라, 참여하는 모든 unsize 지점에서 증강이 필요하다. 대상 dyn 트레이트가 TraitMetadataTable을 상속하는 unsize 강제 변환만 영향을 받으므로 표면은 "모든 unsizing"보다 좁다. 그럼에도 이는 이 RFC가 의존하는 캐스트 지점 전용 표면보다 엄격히 더 넓다. 캐스트 지점은 문법적으로 구분된다(cast!(in dyn Root, ...)). 반면 해당하는 unsize 지점은 그렇지 않으며, 참여 그래프 안의 모든 &C<'a,...> -> &dyn Root 강제 변환이 증강된 Instance 처리를 필요로 한다. 이는 아래 Generic cast targets and lifetime-sensitive monomorphization 에서 설명하는 region-sensitive 모노모피제이션 표면을 넓힌다.
Vtable 정체성이 구체 정체성과 갈라진다. 이 대안에서는 테이블 키가 (root, concrete)가 아니라 (root, concrete, outlives-evidence-at-unsize)가 된다. 같은 구체 타입의 두 &dyn SuperTrait 값이라도 서로 다른 unsize 지점에서 생성되면 서로 다른 vtable과 서로 다른 캐스트 동작을 갖는다. 이는 현재 모델에서는 결코 관찰되지 않는 방식으로 관찰 가능하다.
원거리 작용. 캐스트 성공 여부는 캐스트 지점의 어떤 것이 아니라 unsize site가 무엇을 증명했는지에 달려 있다. 캐스트 자체에서 사용 가능한 outlives 증거가 테이블 엔트리를 선택한다는 locality 속성이 있어야 진단을 다루기 쉽다. 이를 잃으면 "다른 모듈 어딘가에서 값이 더 강한 outlives 바운드 아래에서 unsize되었더라면 이 캐스트는 성공했을 것" 같은 오류가 생기고, 이를 유용하게 표면화하기 어렵다.
건전성은 여전히 보존 가능하다. 기존 불변식("소거된 수명은 계속 소거된 채로 남는다")은 unsize-site 증강이 전혀 발생하지 않는 특수 경우가 되고, 테이블 선택은 unsize site가 인증하지 않은 어떤 outlives class도 거부한다. 그러나 비용 구조 — 증강 지점 집합이 두 배가 되고, 캐스트 지점 locality가 약해지며, 같은 구체 타입에 대해서도 unsize 지점별 vtable 분기가 도입되는 것 — 는 허용 가능한 트레이트 그래프가 조금 더 늘어나는 이득을 정당화하지 못한다. region-제네릭 서브트레이트가 필요한 프로그램은 region-제네릭 루트를 선언하면 되며, 이 RFC는 이를 예측 가능한 비용으로 이미 수용한다.
dynamic_cast핵심 차이점:
개념적으로, C++도 이 두 기능이 필요하지 않았다면 이 제안과 유사하게 캐스팅을 구현할 수 있었을 것이다.
대체로 같은 아이디어다. Java의 배열 캐스팅은 Rust에 dyn [Trait]가 없기 때문에, 최소한 fat pointer가 일반화되기 전까지는 여기서 범위 밖이다.
Java는 일반 virtual dispatch를 위해 각 구체 클래스에 하나의 vtable을 할당하고, 그 클래스가 구현하는 모든 인터페이스에 대해 독립적인 per-interface dispatch 구조("itable")를 둔다. itable은 개념적으로 각 인터페이스에 대한 조밀한 메서드 테이블이며, JVM은 클래스 메타데이터에 저장된 간접 참조를 통해 이를 객체 헤더에 설치하여, 그래프 순회나 RTTI 조회 없이 상수 시간 인터페이스 호출 해석을 가능하게 한다. 클래스 로딩 중 JVM은 이러한 itable을 전역적으로 계산한다. 전체 인터페이스 상속 그래프를 순회하고, 상속된 인터페이스 메서드를 정준 순서로 평탄화한 뒤, 각 구체 클래스마다 각 인터페이스 슬롯에 대응하는 구현 메서드 엔트리를 기록한다. 실패한 인터페이스 캐스트도 이 동일한 전역 메타데이터를 조회하여 처리된다. 검사된 캐스트 연산은 런타임에 구조적 탐색을 수행하는 대신, 사전 계산된 인터페이스 구현 집합에 대한 멤버십 테스트를 수행한다. 순효과는 Java가 전역 계산과 클래스별 추가 메타데이터 비용을 치르는 대신, 안정적 상수 시간 인터페이스 디스패치와 상수 시간 검사된 인터페이스 캐스팅을 달성한다는 것이다. 이는 이 제안의 전역 계산 트레이트 메타데이터 테이블 및 인덱스와 정신적으로 넓게 유사하다.
Go의 v, ok := x.(I)는 가장 가까운 표면 유사체다. 인터페이스 값 x를 다른 인터페이스 타입 I에 대해 런타임 검사하고, x의 구체 동적 타입이 I를 만족하면 새로운 인터페이스 값을 돌려준다. 비교를 위해 참고할 만한 몇 가지 차이가 있다. Go의 인터페이스 만족은 구조적이며 이름 기반이다. 구체 타입은 선언 지점 없이, 메서드 집합이 이름과 시그니처로 I의 메서드를 모두 덮을 때 I를 만족한다. 따라서 런타임은 컴파일러가 방출한 테이블을 읽는 대신 메서드 집합을 순회해 "T가 I를 구현하는가"를 판단한다. 결과는 (concrete type, interface type)을 키로 하는 전역 락 보호 itab 해시 테이블에 캐시되므로 반복된 assertion은 저렴하지만, cold-path 첫 assertion은 메서드 집합 순회 비용을 치른다. Go 런타임은 모든 타입 메타데이터를 소유하고 itab을 지연 구축하기 때문에, assertion은 플러그인/공유 라이브러리 경계를 넘어서도 깔끔하게 조합된다. 대략 이 RFC가 ForeignTraitGraph 경로로 거부하는 시나리오다. 그 대가로 previously-unseen assertion의 빠른 경로에 필수 런타임, 변경 가능한 전역 상태가 필요하고, 대상으로 삼을 수 있는 인터페이스 집합에 대한 컴파일 타임 경계가 없으며, 이 제안이 보존해야 하는 종류의 수명 관계를 표현할 메커니즘도 없다.
제안된 매크로 이름 cast!, try_cast!, unchecked_cast!(core에서 export되고 std에서 re-export됨)은 짧아서 사용자 코드 식별자와 충돌할 수 있다. 안정화 시 매크로에 trait_ 접두사(trait_cast!, try_trait_cast!, unchecked_trait_cast!)를 붙여야 하는지, 아니면 core::cast::cast! 같은 전용 경로 아래에 두어야 하는지 다시 검토해야 한다. 이 RFC는 최종 이름을 미리 확정하지 않는다.
Display and Error impls for TraitCastError<T>
TraitCastError<T>는 Debug, Clone, Copy만 derive한다. 안정화 시 core::fmt::Display와 core::error::Error를 구현해야 하는지, 그렇다면 각 variant에 어떤 formatter 출력을 제공하는 것이 적절한지(특히 ForeignTraitGraph 대 UnsatisfiedObligation)를 결정해야 한다.
NonNull<T> impls of TraitCast
이 RFC는 정확히 &T, &mut T, Box<T, A>, Rc<T, A>, Arc<T, A>에 대해서만 TraitCast를 제안한다. Pin<P>, *const T, *mut T, NonNull<T>에 대한 impl은 제안하지 않는다. 특히 Pin<&T>는 자연스러운 후보이다. raw pointer impl은 obj_graph_id 비교에 대한 명확한 안전 계약이 필요하다(포인터가 역참조 가능하지 않을 수 있기 때문이다). 안정화 시 최종 집합을 결정해야 한다.
address_significant
global-crate-id 할당은 코드 생성 backend가 unnamed_addr류 병합을 억제하는 데 의존한다. 그렇지 않으면 global-crate별 유일성 계약이 LTO나 linker ICF에 의해 깨질 수 있다. LLVM은 set_unnamed_address(UnnamedAddr::No)를 통해 address-significance 플래그를 직접 존중한다. Cranelift와 GCC에 대해서는 이 RFC가 메커니즘을 정하지 않으므로, 그 backend를 통해 빌드된 바이너리에는 병합에 대한 기능적 보호 장치가 없다. 안정화 전에 이를 해결해야 한다. 즉 각 backend가 address-significant 할당에 대한 병합을 억제하도록 요구하거나, 모든 backend가 존중하는 shared upstream marker를 도입해야 한다.
GenericArgKind::Outlives
v0 mangler만이 GenericArgKind::Outlives를 인코딩하도록 지정되어 있다(Appendix C §C.3.3 참조). 증강된 Instance에 대해 컴파일이 legacy mangler로 fallback 하면 결과 심볼 인코딩은 지정되지 않는다. 해결책은 둘 중 하나다. 증강된 Instance의 legacy mangling을 명시적으로 거부하고 그 경로에서 v0를 강제하든가, 아니면 legacy mangler를 확장해 Outlives arg를 인코딩하든가.
VidProvenance::BoundedByUniversal semantics
BoundedByUniversal은 NLL 제약 그래프가, dyn 타입이 공변이기 때문에 unsizing coercion에서 전방 간선('universal: vid)만 기록하지만, 그 coercion을 통과하는 실질적인 구체 수명은 universal 자체인 경우를 다룬다. 중첩된 unsizing, higher-ranked subtyping, re-borrow 패턴과의 상호작용은 다른 variant에 비해 덜 명세되어 있다. 안정화에는 실제 사용자 코드를 대상으로 이 variant를 실험하는 테스트 매트릭스와, 이 variant가 보존하는 불변식을 설명하는 문서가 필요하다.
이 RFC는 여러 global crate가 런타임에 공존할 수 있도록 허용하며(가이드의 cdylib 논의 참조), crate-type에 따른 기본 유도를 재정의하기 위해 -Z global_crate=yes|no를 노출한다. 안정화 전에는 Cargo 및 다른 빌드 시스템이 global-crate 역할을 어떻게 표면화할지 결정해야 한다. 계속 crate-type만으로 유도할 것인지, manifest key를 도입할 것인지, 또는 휴리스틱이 모호한 경우 진단을 표면화할 것인지 말이다.
dyn upcasting
Rust는 이미 각 트레이트 객체에 내장된 supertrait-vtable 포인터를 통해 native dyn upcasting을 지원한다. 이 RFC의 trait-cast 장치는 추가적이며, 두 메커니즘은 공존한다. 장기적 질문은 둘을 통합해 &dyn Sub as &dyn Super도 메타데이터 테이블을 거치도록 해야 하는지 여부다. 그러면 각 vtable 안의 embedded supertrait-vtable 포인터를 제거하는 대신, 작은 런타임 lookup 비용이 생긴다. 추측적 스케치는 Future possibilities > Dyn upcasting 을 보라.
Native dyn upcasting은 이미 stable하다. 이는 각 vtable 안에 트레이트 계층을 따라 도달 가능한 모든 supertrait의 vtable 포인터를 내장함으로써 구현된다. &dyn Sub에서 &dyn Super로의 업캐스트는 그 내장 포인터를 상수 시간에 로드하는 것이다.
이 RFC가 도입하는 루트별 메타데이터 테이블 장치는 원칙적으로 upcasting을 대체할 수 있다. 업캐스트는 구조적으로, 대상이 그래프에서 소스보다 위에 놓인 다운캐스트와 동일하기 때문이다. 업캐스트를 per-supertrait 메타데이터 테이블을 통해 처리하면, 각 vtable에서 embedded supertrait-vtable 포인터를 제거할 수 있고, 그 대가로 각 업캐스트마다 작은 런타임 lookup(본질적으로 다운캐스트와 같은 두 번의 로드)을 수행하게 된다. 대신 트레이트 그래프의 깊이와 fan-in에 비례하는 vtable 크기 감소를 얻는다.
이는 추측적인 미래 방향이다. 이미 stable한 upcasting 경로와의 하위 호환성에 대한 주의가 필요하고, vtable 크기와 per-upcast 비용의 tradeoff도 workload에 따라 달라진다. 여기서는 어떤 약속도 하지 않는다.
현재 제안은 특정 Metadata 타입을 갖는 Pointee를 요구하므로, 구체 타입은 배제된다.
하지만 제안된 lifetime erasure 규칙은 구체 타입으로의 안전한 다운캐스트 경로를 열어 줄 수 있다.
일반적으로 우리는 두 가지에 대해 전역 방문을 수행하고 있다.
그리고 그 방문 결과로 추가 코드와 데이터를 생성한다. 핵심 능력은 전역 모노모피제이션 이후까지 지연하면서도 typeck 등이 로컬하게 동작하도록 하는 것이다.
이 RFC가 이를 위해 도입하는 메커니즘 — 지연 코드 생성, 최종 산출물마다 한 번 실행되는 전역 단계 쿼리, 크레이트 간 DelayedInstance 교환 — 은 트레이트 캐스팅에만 특화된 것이 아니다. 그럴듯하게는 다른 기능도 소비할 수 있는 일반적 "global phase" 능력으로 분리해낼 수 있다.
(Ty, TraitRef) 쌍의 vtable을 병합하기이를 일반 기능으로 제공하려면 global phase가 어떤 계약 위에서 동작하는지 안정화해야 한다. 특히 global phase에서 어떤 쿼리가 허용되는지, 증강된 Instance가 어떻게 교환되는지, backend enforcement(address_significant 이야기)가 기능들 사이에서 어떻게 조합되는지가 중요하다. 이는 추측적이다. 현재 RFC는 그런 일반화를 제안하지 않고, 단지 추가되는 빌딩 블록들이 그럴듯한 출발점이라는 점만 지적한다.
아래 예시들은 가이드 수준 설명에 대한 적합성 오라클이다. 제안을 이해하는 데 필수 읽을거리는 아니며, 각각 하나의 속성을 분리해서 보여준다.
pub trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { }
// These types and traits can be spread out over multiple crates. struct S0; struct S1; struct S2; struct S3; pub trait Trait1: SuperTrait { } pub trait Trait2: SuperTrait { } pub trait Trait3: Trait1 + Trait2 { } pub trait Trait4: SuperTrait { } pub trait Trait5: Trait4 { } pub trait Trait6: Trait3 + Trait5 { }
/// A trait that is not part of the trait graph. /// It can't be cast from or to any trait in the graph. pub trait IrrelevantTrait { }
impl SuperTrait for S0 { } impl Trait1 for S0 { }
impl SuperTrait for S1 { } impl Trait2 for S1 { }
impl SuperTrait for S2 { } impl Trait1 for S2 { } impl Trait2 for S2 { } impl Trait3 for S2 { }
impl SuperTrait for S3 { } impl Trait1 for S3 { } impl Trait2 for S3 { } impl Trait3 for S3 { } impl Trait4 for S3 { } impl Trait5 for S3 { } impl Trait6 for S3 { }
#[test] fn s0() { let s = S0; assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait1).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait1) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait2).map(|r| r as *const _).ok(), None ); } #[test] fn s1() { let s = S1; assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait1).map(|r| r as *const _).ok(), None ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait2).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait2) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait3).map(|r| r as *const _).ok(), None ); } #[test] fn s2() { let s = S2; assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait1).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait1) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait2).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait2) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait3).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait3) ); let s1 = cast!(in dyn SuperTrait, &s => dyn Trait1).unwrap(); let s2 = cast!(in dyn SuperTrait, &s => dyn Trait2).unwrap(); assert_eq!( cast!(in dyn SuperTrait, s1 => dyn Trait3).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait3) ); assert_eq!( cast!(in dyn SuperTrait, s2 => dyn Trait3).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait3) ); } #[test] fn s3() { let s = S3; assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait1).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait1) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait2).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait2) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait3).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait3) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait4).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait4) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait5).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait5) ); assert_eq!( cast!(in dyn SuperTrait, &s => dyn Trait6).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait6) );
let s3 = cast!(in dyn SuperTrait, &s => dyn Trait3).unwrap();
assert_eq!(
cast!(in dyn SuperTrait, s3 => dyn Trait4).map(|r| r as *const _).ok(),
Some(&s as *const dyn Trait4)
);
}
pub trait SuperTrait1: TraitMetadataTable<dyn SuperTrait1> { } pub trait SuperTrait2: TraitMetadataTable<dyn SuperTrait2> { }
pub trait Trait1: SuperTrait1 { } pub trait Trait2: SuperTrait2 { } pub trait Trait3: Trait1 + Trait2 { }
pub struct S1; pub struct S2; pub struct S3;
impl SuperTrait1 for S1 { } impl SuperTrait2 for S2 { } impl SuperTrait1 for S3 { } impl SuperTrait2 for S3 { } impl Trait1 for S1 { } impl Trait2 for S2 { } impl Trait1 for S3 { } impl Trait2 for S3 { } impl Trait3 for S3 { }
// S3 will have two trait vtable tables: one for SuperTrait1 and one for SuperTrait2. // S1 and S2 will have only one trait vtable table.
#[test] fn s3_multiple_supertraits() { let s = S3; assert_eq!( cast!(in dyn SuperTrait1, &s => dyn Trait1).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait1) ); assert_eq!( cast!(in dyn SuperTrait2, &s => dyn Trait2).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait2) ); assert_eq!( cast!(in dyn SuperTrait1, &s => dyn Trait3).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait3) ); assert_eq!( cast!(in dyn SuperTrait2, &s => dyn Trait3).map(|r| r as *const _).ok(), Some(&s as *const dyn Trait3) );
// So far, so obvious. But what about this?
let s1 = cast!(in dyn SuperTrait1, &s => dyn Trait1).unwrap();
let s2 = cast!(in dyn SuperTrait2, &s => dyn Trait2).unwrap();
// COMPILE ERROR: Trait1 and Trait2 do not share a common supertrait, so
// the following have unsatisfiable constraints:
// cast!(in dyn SuperTrait1, s1 => dyn Trait2)
// cast!(in dyn SuperTrait2, s2 => dyn Trait1)
// But Trait3 has a shared supertrait with both Trait1 and Trait2, so:
assert_eq!(
cast!(in dyn SuperTrait1, s1 => dyn Trait3).map(|r| r as *const _).ok(),
Some(&s as *const dyn Trait3)
);
assert_eq!(
cast!(in dyn SuperTrait2, s2 => dyn Trait3).map(|r| r as *const _).ok(),
Some(&s as *const dyn Trait3)
);
}
pub trait SuperTrait<T>: TraitMetadataTable<dyn SuperTrait<T>> { }
pub trait Trait1: SuperTrait<u8> { } pub trait Trait2<T>: SuperTrait<T> { } pub trait Trait3: Trait1 + Trait2<u16> { }
// Same as the multiple-supertrait example, but with a generic supertrait. // Trait3 has two supertraits: SuperTrait<u8> and SuperTrait<u16>.
/// This will have one super trait, after monomorphization. pub trait Trait4: Trait1 + Trait2<u8> { }
trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { } trait SubTrait<'a>: SuperTrait { }
struct S0<'a>(PhantomData<fn(&'a ()) -> &'a()>); impl<'a> SuperTrait for S0<'a> { } impl<'a> SubTrait<'a> for S0<'a> { }
struct S1<'a>(PhantomData<fn(&'a ()) -> &'a()>);
impl<'a> SuperTrait for S1<'a> { }
impl<'a> SubTrait<'a> for S1<'static> { }
// Technically, S1<'static> implements for<'a> SubTrait<'a>, i.e.
// for all lifetimes.
struct S2<'a>(PhantomData<fn(&'a ()) -> &'a()>);
impl<'a> SuperTrait for S2<'a> { }
impl<'a> SubTrait<'static> for S2<'a> { }
// Note: S1<'_> does not implement for<'a> SubTrait<'a> (!= SubTrait<'static>).
// Trait generics are invariant, so 'static can't be "relaxed" to any lifetime
// like, e.g., &'static u8 can.
macro_rules! cast_helper { ($b:lifetime, $e:expr) => ( cast!(in dyn SuperTrait, $e as &(dyn SuperTrait + $b) => dyn SubTrait<$b>).ok() ) }
#[test]
fn static_s0() {
const S: S0<'static> = S0(/.../);
assert!(cast_helper!('static, &S).is_some());
}
#[test]
fn non_static_s0() {
let s = S0(/.../);
fn inner<'a>(s: &'a S0<'a>) {
assert!(cast_helper!('a, s).is_some());
assert!(cast_helper!('static, s).is_none());
}
inner(&s);
}
#[test]
fn static_s1() {
const S: S1<'static> = S1(/.../);
fn inner<'a>(s: &'static S1<'static>, : &'a ()) {
assert!(cast_helper!('a, s).is_some());
assert!(cast_helper!('static, s).is_some());
}
inner(&S, &());
assert!(cast!(in dyn SuperTrait, &S => dyn for<'out> SubTrait<'out>).is_ok());
}
#[test]
fn non_static_s1() {
let s = S1(/.../);
fn inner<'a>(s: &'a S1<'a>) {
// S1<'a> does not implement SubTrait<'_> for any lifetime other
// than 'static.
assert!(cast_helper!('a, s).is_none());
assert!(cast_helper!('static, s).is_none());
}
inner(&s);
}
#[test]
fn non_static_s2() {
let s = S2(/.../);
fn inner<'a>(s: &'a S2<'>) {
assert!(cast_helper!('a, s).is_none());
// S2<'a> implements SubTrait<'static> for any lifetime 'a.
assert!(cast_helper!('static, s).is_some()); // !
}
inner(&s);
}
모든 바운드 수명은 검사에 참여하며, 트레이트 정의에 문법적으로 나타나는 것만이 아니다.
trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { } trait SubTrait: SuperTrait { type Assoc; } /// Note: all lifetimes are considered, including those reached through /// associated-type bindings: type T3<'a> = dyn SubTrait<Assoc = &'a u8>;
여러 수명이 있을 때 캐스트는 소거와 무관하게 관계('b: 'a 등)를 보존해야 한다.
trait SuperTrait<'a, 'b>: TraitMetadataTable<dyn SuperTrait<'a, 'b>> { } trait SubTrait<'a, 'b>: SuperTrait<'a, 'b> { }
#[derive(Default)] struct S0<'a, 'b> { _m0: PhantomData<&'a ()>, _m1: PhantomData<&'b ()>, } #[derive(Default)] struct S1<'a, 'b> { _m0: PhantomData<&'a ()>, _m1: PhantomData<&'b ()>, } impl<'a, 'b> SuperTrait<'a, 'b> for S0<'a, 'b> { } impl<'a, 'b> SuperTrait<'a, 'b> for S1<'a, 'b> { } impl<'a, 'b> SubTrait<'a, 'b> for S0<'a, 'b> { } impl<'a, 'b> SubTrait<'a, 'b> for S1<'a, 'b> where 'b: 'a, { }
macro_rules! cast_helper { ($a:lifetime, $b:lifetime, $e:expr) => ( cast!(in dyn SuperTrait<', '>, $e as &dyn SuperTrait<', '> => dyn SubTrait<$a, $b>).ok() ) }
#[test]
fn unrelated_lifetimes() {
fn inner<'a, 'b>(_: &'a (), : &'b ()) {
let s = S0::<'a, 'b>::default();
assert!(cast_helper!('a, 'b, &s).is_some());
let s = S1::<'a, 'b>::default();
assert!(cast_helper!('a, 'b, &s).is_none());
}
inner(&(), &());
}
#[test]
fn related_lifetimes() {
fn inner<'a, 'b>(: &'a (), _: &'b ())
where 'b: 'a,
{
let s0 = S0::<'a, 'b>::default();
assert!(cast_helper!('a, 'b, &s0).is_some());
assert!(cast_helper!('a, 'a, &s0).is_some()); // via variance of S0
let s1 = S1::<'a, 'b>::default();
assert!(cast_helper!('a, 'b, &s1).is_some()); // S1's 'b: 'a impl predicate is now satisfied.
assert!(cast_helper!('a, 'a, &s1).is_some()); // via variance of S1
}
inner(&(), &());
}
토폴로지는 A cdylib + B cdylib + C shared dylib이다. A와 B는 인터페이스 역할을 하고, C는 둘 다 의존하는 shared library다. 핵심 문제는 서로 다른 global crate에서 독립적으로 계산된 (SuperTrait, Struct, Trait) 인덱스에서 비롯된다. 더 긴 의존 체인도 같은 방식으로 동작하므로, 이것이 최소 형태다.
#![crate_type = "dylib"] // C.rs pub trait SuperTrait: TraitMetadataTable<dyn SuperTrait> { }
#[repr(C)] pub struct FfiObject(Box<dyn SuperTrait>); impl FfiObject { pub fn new(inner: impl SuperTrait) -> Self { Self(Box::new(inner)) } } impl core::ops::Deref for FfiObject { type Target = dyn SuperTrait; fn deref(&self) -> &Self::Target { &self.0 } } impl core::ops::DerefMut for FfiObject { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } }
// B.rs #![crate_type = "cdylib"] extern crate C; use C::*;
trait BTrait: SuperTrait { fn thing_done(&self) -> bool; fn do_b_thing(&mut self) -> Result<(), Box<str>>; }
struct InternalB { thing_done: bool } impl SuperTrait for InternalB { } impl BTrait for InternalB { fn thing_done(&self) -> bool { self.thing_done } fn do_b_thing(&mut self) -> Result<(), Box<str>> { self.thing_done = true; Ok(()) } }
#[no_mangle] unsafe extern "C" fn init_obj(obj: *mut MaybeUninit<FfiObject>) { unsafe { obj.as_mut_unchecked().write(FfiObject::new(InternalB { thing_done: false })); } } #[no_mangle] unsafe extern "C" fn uninit_obj(obj: *mut FfiObject) { let Some(obj) = (unsafe { obj.as_mut() }) else { return; }; unsafe { core::ptr::drop_in_place(obj); } } #[no_mangle] unsafe extern "C" fn do_thing(obj: *mut FfiObject) -> core::ffi::c_int { let Some(obj) = (unsafe { obj.as_mut() }) else { return 0; }; let Ok(obj) = cast!(in dyn SuperTrait, &mut **obj => dyn BTrait) else { return 0; }; obj.do_b_thing().is_ok() as _ } #[no_mangle] unsafe extern "C" fn thing_done(obj: *mut FfiObject) -> core::ffi::c_int { let Some(obj) = (unsafe { obj.as_mut() }) else { return 0; }; let Ok(obj) = cast!(in dyn SuperTrait, &mut **obj => dyn BTrait) else { return 0; }; obj.thing_done() as _ }
// A.rs — symmetrically the same as B.rs, with BTrait/InternalB/do_b_thing
// replaced by ATrait/InternalA/do_a_thing.
이를 로드하는 바이너리(Rust로 제시하지만 C++여도 무방하다)는 두 cdylib를 모두 dlopen한다. 핵심 관찰은 마지막 블록이다.
// user.rs (dlopen/ffi scaffolding elided) fn main() { let a = dlopen_load("libA.so"); let b = dlopen_load("libB.so");
let mut a_obj = a.new_obj(); // libA-built: trait graph is A's.
let mut b_obj = b.new_obj(); // libB-built: trait graph is B's.
// Both return 0: the cast inside do_thing returns
// Err(TraitCastError::ForeignTraitGraph) because the global-crate
// identities do not match — regardless of any index coincidence
// between A's ATrait and B's BTrait.
assert_eq!(unsafe { (a.do_thing)(&mut b_obj) }, 0);
assert_eq!(unsafe { (b.do_thing)(&mut a_obj) }, 0);
}
모든 트레이트가 C에 정의되어 있더라도, C를 global crate로 강제하는 것은 일반적으로 가능하지 않다. 트레이트 그래프는 dyn SuperTrait<u8>, dyn Trait2<u16>, dyn Trait2<Downstream> 같은 지연 모노모피제이션된 트레이트 객체 노드들 위에 형성되며, 캐스트 가능 여부는 구체 인스턴스화에 의존한다. dyn Trait2<Downstream>는 Downstream이 A에서 모노모피제이션되기 전까지는 C의 관점에서 존재하지 않는다. C가 미래의 모든 인스턴스화에 대한 인덱스를 미리 할당하려는 어떤 방식도 무한하며, C의 컴파일 시점에는 알 수 없다. 동적 레지스트리도 배제된다. 트레이트 그래프가 지연적이기 때문이다(캐스트 대상으로 나타나는 트레이트만 포함됨). 따라서 레지스트리는 런타임에 외부 타입에 대한 vtable을 코드 생성해야 하며, 이는 사실상 Rust 컴파일러의 일부를 배포하는 것이다. 컴파일러 인프라의 큰 전환이 없는 한, 이러한 단점 없는 해결책은 손에 닿지 않는다.
이 부록은 컴파일러가 참조 절에서 정의된 계약을 어떻게 실현할 수 있는지 스케치한다. 이는 규범적이지 않다. 적합한 구현은, 위에서 지정한 의미론을 보존하기만 한다면, 여기 설명된 어떤 메커니즘과도 달라도 된다. 아래의 구체 타입, 쿼리, 알고리즘 선택은 프로토타입에서 따온 것이며, 실현 가능성을 보여주기 위해 포함되었다.
Definitions / Global crate 의 선언적 계약은 컴파일 타임 불리언과 재정의 메커니즘을 이름 붙이지만, 그것을 구체적으로 지정하지는 않는다. rustc는 이를 다음처럼 실현한다.
tcx.is_global_crate() -> bool, TyCtxt에 노출된 컴파일 타임 불리언(rustc_middle::ty::context에 정의). 기본 provider는 CrateType을 키로 삼는다. Executable, StaticLib, Cdylib는 true, Dylib, Rlib, ProcMacro, Sdylib는 false를 반환한다.-Z global_crate=yes|no, rustc_session::options의 unstable_opts.global_crate: Option<bool>로 뒷받침되는 unstable session option. 설정되면, tcx.is_global_crate()는 CrateType 기반 기본값을 우회하고 명시된 값을 무조건 반환한다.적합한 구현은 동등한 임의의 메커니즘으로 global-crate 계약을 만족시킬 수 있다.
TraitMetadataTable internalsTraitMetadataTable 의 사용자 표면은 네 개의 intrinsic 자유 함수를 가진 단순 트레이트 선언이다. rustc는 이를 language-item 및 attribute marker와 blanket impl로 실현한다.
#[lang_item = "trait_metadata_table"]TraitMetadataTable<dyn Self>를 상속하는 루트 슈퍼트레이트로 인해 생기는 순환(예: trait Foo: TraitMetadataTable<dyn Foo>)을 coinductive하게 해결하기 위한 #[rustc_coinductive]trait_metadata_* / trait_cast_is_lifetime_erasure_safe intrinsic 선언 각각에 붙는 #[rustc_nounwind] #[rustc_intrinsic]blanket impl은 모든 Sized 타입을 덮어, 이 트레이트를 사실상 #[rustc_deny_explicit_impl]로 만든다. 사용자는 impl TraitMetadataTable을 직접 쓰지 않는다. 슈퍼트레이트 바운드 trait Root: TraitMetadataTable<dyn Root>는, 사용자가 impl Root for T를 쓴 모든 구체 T에 대해 blanket impl에 의해 만족된다.
impl<SuperTrait, T: Sized> TraitMetadataTable<SuperTrait> for T
where
SuperTrait: MetaSized
+ Pointee<Metadata = DynMetadata<SuperTrait>>
+ TraitMetadataTable<SuperTrait>,
{
fn derived_metadata_table(&self) -> (&'static u8, NonNull<Option<NonNull<()>>>) {
// SAFETY: intrinsic requires unsafe but is not actually unsafe to call here.
unsafe { core::intrinsics::trait_metadata_table::<SuperTrait, T>() }
}
}
실제로 T가 루트 슈퍼트레이트를 구현한다는 제약은 이 impl의 where-clause가 아니라, 슈퍼트레이트 관계 자체에 의해 강제된다(사용자는 반드시 impl Root for T를 써야 한다).
이 impl은 trait solver의 순환을 피하기 위해 의도적으로 Unsize<SuperTrait> 바운드를 생략한다. T: Unsize<dyn Root>를 증명하려면 T: Root가 필요하고, 이는 다시 슈퍼트레이트 T: TraitMetadataTable<dyn Root>를 요구하므로 Unsize를 통해 순환하게 된다. intrinsic 위의 SuperTrait: TraitMetadataTable<SuperTrait> 바운드는 SuperTrait = dyn Root일 때 object candidate(vtable dispatch)를 통해 만족된다. TraitMetadataTable<dyn Root>가 Root의 슈퍼트레이트이기 때문이다.
계산은 global crate에서만, 서로를 공급하는 세 개의 cached query로 실행된다. 세 쿼리 모두 Ty<'tcx>를 키로 삼아, 무효화가 트레이트 객체 단위의 도달 가능한 트레이트 집합을 추적하도록 한다. 그래프와 레이아웃은 arena-cached된다.
trait_cast_graph(root: Ty<'tcx>) -> &'tcx TraitGraph<'tcx>trait_cast_layout(root: Ty<'tcx>) -> &'tcx TableLayout<'tcx>trait_cast_table((root, concrete)) -> &'tcx [Option<AllocId>]trait_cast_graph(root) 는 수집된 지연 코드 생성 요청을 서브트레이트 → outlives-class 매핑과, 이 루트에 대한 메타데이터 테이블을 요청한 구체 타입 집합으로 분할한다. super_trait가 root와 일치하는 요청만 고려하며, 각 인덱스 요청은 outlives class로 축약되어 대상에 대한 per-sub-trait 정보에 삽입된다. 요청 스캔 이후, 그래프는 서브트레이트 자신의 where 'a: 'b 스타일 술어가 암시하는 where-clause 기반 outlives class들로 보강된다. 그래야 제네릭 라이브러리 코드를 통해 유효한 outlives 증거를 가진 캐스트가 올바른 슬롯을 찾을 수 있다. 이 쿼리는 루트의 도달 가능한 지연 코드 생성 요청이 바뀔 때마다 다시 실행된다.
trait_cast_layout(root) 는 그래프의 모든 (sub_trait, OutlivesClass) 쌍에 테이블 슬롯 인덱스를 부여한다. 각 서브트레이트에 대해, 먼저 그래프 안의 각 구체 타입에 대해 참여 impl을 해석한다. 모든 해석된 impl이 impl_universally_admissible fast path를 통과하면, 해당 서브트레이트의 모든 outlives class는 하나의 슬롯으로 붕괴한다(참조의 impl_universally_admissible fast path 절 참조). 그렇지 않으면 레이아웃은 행이 outlives class이고 열이 참여 구체 타입인 BitMatrix<u32, u32>를 구성한다. 비트 (c, t)는 t의 impl이 클래스 c 아래에서 허용 가능할 때 세트된다. 동일한 행을 갖는 클래스는 하나의 슬롯을 공유한다. 출력 TableLayout은 평탄한 (sub_trait, OutlivesClass) -> slot 맵과 per-slot 메타데이터(서브트레이트, 대표 클래스, binder-variable 수)를 저장한다. OutlivesClass는 모노모피제이션된 Instance의 outlives 엔트리 중 정렬·중복 제거된 interned subslice를 빌려온다.
pub struct OutlivesClass<'tcx> {
/// instance.outlives_entries()[1..] — the semantic pairs,
/// skipping the sentinel at position 0.
pub entries: &'tcx [GenericArg<'tcx>],
}
trait_cast_table((root, concrete)) 는 구체 구조체마다 하나의 테이블을 채운다. 레이아웃의 서브트레이트들을 순회하면서, 구체 타입의 impl을 한 번 해석한 뒤, 그 서브트레이트에 속한 각 슬롯에 대해 캐시된 outlives reachability matrix를 사용해 슬롯의 대표 클래스에 대한 허용 가능성을 검사한다. impl이 허용 가능한 슬롯은 대응 vtable의 AllocId를 받고, 구체 타입이 만족하지 않는 슬롯은 None으로 남는다. 형제 쿼리는 결과 배열을 불변 .rodata 할당으로 방출한다.
Pruning은 암묵적이다. 요청하는 outlives class가 없는 서브트레이트는 건너뛰어지고 슬롯을 전혀 받지 않으므로, 도달 불가능한 캐스트 대상은 레이아웃에 아예 나타나지 않는다(예약된 sentinel 인덱스 없음).
trait_metadata_index / trait_metadata_table / trait_metadata_table_len / trait_cast_is_lifetime_erasure_safe에 대한 직접 호출은, 최종 코드 생성이 global crate까지 지연되어야 하는 Instance로 해석된다. 구체 슬롯 인덱스, vtable 테이블, 길이는 모두 전역적으로 계산된 trait-cast 레이아웃 및 population(trait_cast_layout, trait_cast_table, trait_cast_table_alloc)의 산물이며, upstream 크레이트는 이를 단독으로 알 수 없다. 이 intrinsic을 upstream에서 eager하게 방출하면 오래된 인덱스를 구워 넣게 된다.
대신 collector는 이러한 호출자와 그 intrinsic callee를 모두 DelayedInstance<'tcx>로 기록한다.
pub struct DelayedInstance<'tcx> { pub instance: Instance<'tcx>, pub callee_substitutions: &'tcx [( &'tcx List<(DefId, u32, GenericArgsRef<'tcx>)>, Instance<'tcx>, )], pub intrinsic_callees: &'tcx [Instance<'tcx>], }
instance는 코드 생성을 지연할 호출자다. callee_substitutions는 call_id 체인(§C.3.1)과, global phase가 그 호출 지점에서 본문에 splice해야 할 증강된 callee Instance를 짝지어 놓는다. intrinsic_callees는 global-phase condensation 파이프라인이 사용하는 증강된 intrinsic leaf들의 별도 목록이다.
upstream 크레이트는 이 DelayedInstance들을 크레이트 메타데이터에 기록하고, intrinsic 자체는 코드 생성하지 않는다. global crate는 모든 upstream 크레이트의 delayed_codegen_requests(CrateNum)를 소비해 전역 단계 작업(레이아웃, 테이블 population, MIR patching, 최종 코드 생성)을 구동한다. upstream rmeta는 필요 시 global crate가 디코드하는 per-crate LazyArray<DelayedInstance>를 담고 있다.
모노 수집은, 제네릭 파라미터가 수명을 담을 수 있는 trait-cast intrinsic을 전이적으로 포함하는 함수들에 대해 outlives 민감하게 된다. MIR region은 평소와 같이 계속 ReErased다. 기존 소거 파이프라인에서 함수별 예외는 없다. 대신 outlives 정보는 두 개의 새로운 borrowck 쪽 쿼리를 통해 전달된다.
borrowck_result(LocalDefId) -> &'tcx mir::BorrowckResult<'tcx> — 공유되는 핵심 계산. mir_borrowck와 borrowck_region_summary가 둘 다 여기서 투영한다.borrowck_region_summary(DefId) -> &'tcx mir::BorrowckRegionSummary — mono collector가 소비하는 크레이트 간 표면(separate_provide_extern)BorrowckRegionSummary는 다음을 담는다.
call_id 카운터에서 키를 얻는 call_site_mappings: UnordMap<u32, CallSiteRegionMapping> — (DefId, u32, GenericArgsRef<'tcx>) 체인(§C.3.1)outlives_graph: ProjectedOutlivesGraph — 호출 지점 매핑에 관여하는 region 위의 projected SCC graphvid_provenance: UnordMap<u32, VidProvenance>와 vid_to_param_pos: Vec<(u32, u32)> / vid_to_resolved_param: Vec<(u32, u32)> — universal-region / param-position 대응을 제공하며, STATIC_PARAM_POS = u32::MAX가 ReStatic를 나타낸다.서로를 outlive하는 region('a: 'b와 'b: 'a)은 응축된 SCC 위의 Hamiltonian-cycle 쌍으로 인코딩된다. ty::Instance는 region-erased이므로, 등가 클래스를 하나의 대표로 붕괴하면 mangling과 query-cache 정체성을 잃기 때문이다.
각 MIR body에서, trait_metadata_index intrinsic으로부터 정규화되었지만 아직 소거되지 않은 고유한 (SuperTrait, Trait) 쌍과, trait_metadata_table intrinsic으로부터의 유사한 고유 (SuperTrait, Struct) 쌍을 수집한다.
trait_metadata_index / trait_metadata_table / trait_metadata_table_len / trait_cast_is_lifetime_erasure_safe에 대한 직접 호출은 모두 모노모피제이션 요청으로 취급되며, 크레이트의 지연 코드 생성 요청 목록에 추가된다. upstream 크레이트는 이 intrinsic을 절대 코드 생성하지 않고, 메타데이터에 요구사항으로만 기록한다.
위에서 생긴 직접 참조들의 linkage와 visibility가 downstream에서 링크 가능하도록 보장한다.
call_id Chain
수명 소거 이후에는, 같은 MIR body 안의 두 Call terminator가 동일한 callee DefId와 동일한 소거된 GenericArgsRef로 해석되면 terminator의 func operand만으로는 서로 구별할 수 없다. 그럼에도 각 호출 지점은 호출자 안에서 서로 다른 outlives 문맥 아래에 있을 수 있으며, 호출 지점별 outlives 계산(§C.3.5)은 각 지점마다 서로 다른 증강 callee Instance를 산출해야 한다. 따라서 호출 지점은 모든 MIR 패스를 통과하고 inlining을 거쳐도 보존되는, 소거와 무관한 안정적 정체성이 필요하다. 그 마커가 바로 모든 Call / TailCall terminator의 call_id 체인이다.
TerminatorKind::Call과 TerminatorKind::TailCall 모두 interned chain field를 얻는다.
TerminatorKind::Call { // ...existing fields... #[type_foldable(identity)] #[type_visitable(ignore)] call_id: &'tcx List<(DefId, u32, ty::GenericArgsRef<'tcx>)>, }
각 튜플 엔트리는 inlining 경로의 한 링크를 기록한다. DefId는 MIR building 동안 이 호출이 원래 생성된 함수 body를 가리키고, u32는 해당 body 안의 Call / TailCall terminator들 사이에서 유일한 body-local 카운터이며, GenericArgsRef<'tcx>는 그 소스 body의 고유한 generic-parameter 공간 안에서 표현된 callee의 edge-local generic-arg template를 저장한다. #[type_foldable(identity)] / #[type_visitable(ignore)] attribute는 핵심적이다. 이 체인은 타입이 아니라 구조적 식별자이므로, body에 대한 generic substitution이 그것을 건드려서는 안 된다. 내장된 DefId와 template GenericArgsRef는 바깥 호출자 인수에 대해 단계적으로 해석되기 때문이다.
u32 카운터는 MIR build 시점에 할당된다. body는 next_call_id cursor를 갖고 있어서, 나중에 추가된 synthetic call(drop elaboration, shim 등)도 충돌하지 않는 새로운 id를 할당할 수 있다. 체인은 TyCtxt 위에서 interned되며, interned된 &'tcx List<…>에 대한 포인터 동등성이 downstream에서 사용하는 주된 정체성이다. 따라서 패스는 체인 공유를 보존하고, 다시 써야 할 때만 재-intern한다.
MIR inliner가 callee body를 caller에 splice할 때, caller terminator의 체인을 포착한 뒤, inlined callee를 순회하면서 각 inlined terminator의 체인 앞에 caller의 체인을 prepend 하고 결과를 재-intern한다.
chain.extend(self.caller_call_chain.iter()); chain.extend(call_id.iter()); *call_id = self.tcx.mk_call_chain(&chain);
따라서 체인은 바깥 호출자에서 안쪽 호출 지점으로 단조롭게 성장하며, call_id[0]은 항상 가장 바깥 소스 body를 식별한다.
모노모피제이션 시점 patching은 복제된 body 안의 특정 Call / TailCall을, 패치 전 interned list에 대한 포인터 동등성 으로 찾아내고, terminator의 func operand를 증강된 callee의 FnDef를 참조하도록 다시 쓴다. inliner와 interner가 체인 공유를 보존하므로, 지연 코드 생성 요청 안의 주어진 (call_site → callee) 치환은 고유한 terminator 하나를 정확히 가리킨다.
두 downstream 소비자가 이 체인에 의존한다.
CallSiteRegionMapping 조회에서 &call_id[0]의 u32를 원래 body의 borrowck_region_summary에 대한 키로 사용한다. 따라서 같은 body 안의 두 call terminator라도 call_id가 다르면, 눈에 보이는 generic arg가 모두 같더라도 서로 다른 증강 callee instance를 산출한다.Instance의 arg를 투영한다. call_id가 있기 때문에 collector는 이 작업을 호출 지점별로 키잉할 수 있고, 소거 후 동일해 보이는 사이트들을 합쳐 버리지 않는다.call_id 자체는 v0 symbol mangling에 참여하지 않는다. 증강된 callee의 mangled name에 들어가는 것은 체인이 아니라, 그 체인을 사용해 계산된 GenericArgKind::Outlives arg다.
GenericArgKind::Outlives
수명 소거 이후에는, 서로 다른 outlives 문맥을 갖는 trait_metadata_index에 대한 두 호출 지점이 동일한 ty::Instance 값을 생성한다(DefId도 같고, 소거된 GenericArgsRef도 같음). 그런데 Instance는 심볼 이름, query cache, mono-item deduplication의 고유 키로 쓰이므로, 이 둘은 구별되어야 한다.
이를 위해 rustc_type_ir의 GenericArgKind에 outlives-predicate 데이터를 interned handle로 감싸는 네 번째 variant를 추가한다.
pub struct OutlivesArgData { pub longer: usize, pub shorter: usize, }
pub enum GenericArgKind<I: Interner> { Lifetime(I::Region), Type(I::Ty), Const(I::Const), Outlives(I::OutlivesArg), }
각 Outlives arg는 dyn 타입의 existential binder에 대한 canonical bound-variable(BoundVar) 인덱스를 사용해 하나의 outlives predicate를 인코딩한다. 인덱스 usize::MAX는 'static을 나타낸다. 예를 들어 dyn SubTrait<'^0, '^1>에서 '^1: '^0이면 arg의 OutlivesArgData는 { longer: 1, shorter: 0 }이다. dyn SubTrait<'^0, '^1>에서 '^0: 'static이면 { longer: 0, shorter: usize::MAX }다. dyn SubTrait<Target = &'^0 ()>에서 '^0: 'static이면 역시 { longer: 0, shorter: usize::MAX }다. 인덱스는 generic args 목록의 위치가 아니라 binder variable을 가리킨다. 참고로 여기서 모든 수명은 실제로 ReErased이며, dyn SubTrait<'static, _>는 불가능하다.
이 인덱스들은 dyn 타입의 existential binder에 있는 위치를 가리키므로, canonical variable ordering이 결정적이고 수명 소거와 무관하여 안정적이다. Outlives arg는 (longer, shorter) 순으로 정렬되어야 한다.
Outlives arg는 함수의 선언된 generic parameter 뒤와 closure generic parameter 뒤에 덧붙여진다.
Interning. I::OutlivesArg는 CtxtInterners의 새로운 outlives_arg 필드를 통해 intern되며 tcx.mk_outlives_arg(longer, shorter)로 구성된다. interning은 각 OutlivesArg를 두 개의 usize가 아니라 단일 포인터로 표현할 수 있게 하고, 포인터 단위의 hashing/equality를 보존하며, GenericArg의 기존 크기도 유지한다.
Pointer tagging. GenericArg는 interned pointer의 하위 2비트를 태그 판별자로 사용한다. 기존 태그는 0b00(Type), 0b01(Region), 0b10(Const)이며, 비어 있던 0b11이 이제 Outlives가 차지한다.
이를 통해 다음이 얻어진다.
symbol_name 결과symbol_name, items_of_instance, size_estimate 등의 서로 다른 query cache entry. 이는 올바르다. 서로 다른 outlives 문맥은 서로 다른 코드 생성(서로 다른 index constant)을 요구하기 때문이다.MonoItem deduplication — 서로 다른 outlives 문맥은 서로 다른 mono item이다.
동기. 같은 DefId와 같은 post-erasure GenericArgsRef로 해석되는 두 intrinsic 호출 지점도, 그 지점에서의 outlives 문맥(dyn 타입 binder variable들 사이의 관계)이 다를 수 있다. 전역 단계는 심볼 mangling, query caching, mono-item deduplication이 이들을 구별하도록 구조적으로 다른 mono item으로 코드 생성해야 한다. 이를 위해 outlives 정보를 Instance::args 자체로 전달한다. augmented Instance는 base Instance의 args 뒤에 Outlives generic arg를 덧붙여, 구조적 equality와 hashing 아래에서 args 포인터가 다른 새로운 Instance를 만든다.
OUTLIVES_SENTINEL. outlives 엔트리를 단순히 덧붙이는 것만으로는 충분하지 않다. outlives 관계가 0개인 사이트는 base와 구별되지 않기 때문이다. 따라서 augmentation은 항상 실제 caller-supplied outlives 쌍 앞에 sentinel을 prepend한다.
pub const OUTLIVES_SENTINEL: (usize, usize) = (usize::MAX, usize::MAX);
생성자 Instance::with_outlives(self, tcx, outlives)가 유일하게 지원되는 경로다. 이 함수는 outlives에 sentinel이 포함되지 않았음을 debug_assert!한 다음, self.args, interned sentinel Outlives arg 하나, 그리고 caller가 제공한 각 쌍에 대한 interned Outlives arg 하나를 차례로 연결한 새 GenericArgs를 만든다. base Instance는 0개의 Outlives arg를 가진다 — sentinel조차 없다. tail이 OUTLIVES_SENTINEL로 시작하는 모든 Instance는, 실제 outlives 관계가 하나도 없더라도 augmentation된 것이다.
Instance 위 helper. with_outlives와 함께 다음 helper들이 정의된다.
outlives_entries(self) -> &'tcx [GenericArg<'tcx>] — sentinel을 포함하는 Outlives 엔트리의 tail slice. base instance에서는 &[]를 반환한다.outlives_indices_iter(self) -> impl Iterator<Item = (usize, usize)> — sentinel을 건너뛴 의미론적 (longer, shorter) 쌍을 산출한다. tail에 non-Outlives 엔트리가 나타나면 bug!() 한다.has_outlives_entries(self) -> bool — Instance가 augmentation되었는지 여부(적어도 sentinel 포함)strip_outlives(self, tcx) -> Instance<'tcx> — 첫 번째 Outlives 엔트리에서 args를 잘라 base Instance를 재구성한다.두 좌표계. Outlives arg가 담고 있는 (longer, shorter) 인덱스는 모두 같은 공간에 사는 것이 아니다. MIR-backed user-wrapper callee의 경우에는 callee 자신의 GenericArgs의 walk-order 위치를 뜻하지만, MIR-less intrinsic leaf의 경우에는 dyn 타입의 existential binder를 인덱싱한다. 공간과 그 사이의 변환 규칙은 §C.3.4에 전부 명세되어 있다.
v0 symbol mangling. v0는 Outlives kind를 위한 새 <generic-arg> 생성 규칙을 얻는다. 태그 바이트 Oo, 그 뒤에 longer 인덱스, _ 구분자, shorter 인덱스, 마지막에 E가 온다. impl-path 출력은 generic-arg 목록을 내보낼지 결정할 때 Outlives 엔트리를 명시적으로 검사해야 한다. Outlives arg는 TypeFlags의 "has non-region param" 비트를 세우지 않기 때문이다. legacy(pre-v0) mangler에는 Outlives 전용 처리가 없다. 증강된 Instance는 오직 v0-mangled call path에만 도달하는 것이 기대되며, 증강된 arg를 가진 사이트가 legacy path로 가게 되면 stable한 인코딩이 없다.
Phase-2 cleanup. base Instance는 Phase-1 순회 도중 mono collection에 들어올 수 있다(예를 들어 check_a가 처음에는 비증강 상태로 기록되었다가 나중에 main이 이를 증강하는 경우). Phase-2 augmentation이 실행된 뒤, collector는 대체된 base mono item을 제거해 증강된 변형만 코드 생성에 남긴다. 각 대체된 base에 대해 collector는 그 사용 기록을 증강된 대체물로 옮기고, per-crate delayed_codegen_requests 소비자가 오직 증강된 대체물만 보도록 delayed_codegen 집합에서 base를 걸러낸다.
코드 생성 도달. 증강된 Instance는 다른 어떤 Instance와 마찬가지로 codegen_mir를 통해 흐른다. 패치된 body를 가진 경우, partitioning의 global phase가 그 인스턴스를 최종 mono-item 집합에 넣기 직전에 tcx.feed_codegen_mir(instance, body)를 통해 패치된 body를 공급한다.
모든 증강된 Instance는 (usize, usize) 인덱스 쌍인 Outlives(longer, shorter) generic arg의 tail을 담고 있다. 그 쌍의 형태 는 동일하지만, 인덱스의 의미 는 그 arg가 붙은 callee의 종류에 따라 달라진다. 구별되는 세 개의 인덱스 공간이 있고, 'static에 대한 usize::MAX sentinel은 세 공간 모두에서 공유된다(BorrowckRegionSummary::vid_to_param_pos의 STATIC_PARAM_POS: u32 = u32::MAX와 같은 관례).
공간 1 — user-wrapper walk order. trait-cast intrinsic을 전이적으로 호출하는 MIR-backed user function의 증강된 Instance에 대해서는, 인덱스가 callee 자신의 GenericArgs 안에서의 walk-order 위치를 가리킨다. "Walk order"는 type-visitor DFS walk가 만나는 모든 region에서 카운터를 증가시키며 생성하는 번호다. 즉 ReVar, ReBound, ReErased, ReEarlyParam, ReLateParam, ReStatic이 모두 하나의 위치를 소비하므로, 같은 타입 구조에 대해 region 표현이 달라도 번호는 안정적이다. walk_pos → RegionVid 매핑은 borrowck가 기록한 각 CallSiteRegionMapping에 들어 있다.
공간 2 — intrinsic dyn-binder. MIR-less leaf intrinsic trait_metadata_index::<SuperTrait, TargetTrait>의 native consumer space는 대상(서브트레이트) dyn 타입의 existential binder-variable 공간이다. transport-to-native 재작성은 intrinsic이 소비하기 전에 각 walk position을 자신의 binder variable로 매핑한다. 나머지 두 table-dependent intrinsic인 trait_metadata_table과 trait_metadata_table_len은 augmentation을 통과시키지만 구체 타입 인수만 조회하며, Outlives tail을 읽지 않는다. 따라서 공간 2는 사실상 trait_metadata_index에서만 사용된다.
공간 3 — 결합된 root+target+'static. trait_cast_is_lifetime_erasure_safe::<SuperTrait, TargetTrait>에 대해 transport/origin 공간은 슬롯을 두 개의 연결된 블록으로 배열한다.
// transport / origin walk-position space for the erasure-safe intrinsic:
[0 .. n_root) // root supertrait's walk positions
[n_root .. n_root + n_target) // target trait's walk positions (offset by n_root)
usize::MAX // sentinel for 'static (shared across spaces)
여기서 n_root와 n_target은 각각 super-trait와 target-trait 타입의 region-slot 수다. 대상 술어의 walk-position t_wp는 transport 위치 n_root + t_wp로 번역되고, 루트 술어의 walk-position r_wp는 그대로 r_wp를 유지한다. post-remap("native") 공간은 두 세그먼트의 binder variable을 연속적으로 포장한다(루트 bvs, 그 뒤에 대상 bvs, 'static은 여전히 usize::MAX). 하나의 결합 공간이 필요한 이유는, 소거 안전성 검사가 양쪽 binder를 한꺼번에 이름 붙이는 쌍을 비교하기 때문이다(예: 루트 수명과 상호 outlive 관계인 대상 수명). 어느 한 binder만으로는 충분하지 않다.
공간 사이의 운반. 두 resolver가 공간 1의 인덱스를 공간 2 또는 공간 3으로 운반한다.
trait_metadata_index resolver는 augmented_outlives_for_call과 compose_all_through_chain을 호출해 엔트리를 origin walk-position 공간으로 운반한 다음, 공간 2로 재매핑한다.trait_cast_is_lifetime_erasure_safe resolver는 같은 두 helper를 호출한 뒤, 운반된 엔트리(공간 3 형태의 origin/transport 좌표로 유지됨)를 tcx.is_lifetime_erasure_safe에 전달한다.호출 체인 가장자리의 user wrapper들 사이에서는 transport가 처음부터 끝까지 walk-order 공간에 머문다. batched composer는 compose_all_through_chain(tcx, caller, call_id, n_positions) -> Vec<Option<usize>>이고, query augmented_outlives_for_call이 이를 감싼다. call_id 체인 엔트리는 (DefId, u32, GenericArgsRef<'tcx>) 삼중항이며 — 세 번째 필드는 edge-local template다 — composer는 바깥 caller Instance로부터 이 template를 단계적으로 구체화한다.
체인 구성 장치는 세 가지 지원 타입을 가진다.
InputSlot { arg_ordinal: u32, offset_within_arg: u32 } — walk-order 위치를 어떤 인수가 그 수명을 담았는지, 그 인수 내부 어디였는지로 분해한다(따라서 argument-template composition이 projected lifetime에 대해서도 정확해진다). body의 parameter signature 위 DFS builder가 walk position마다 하나의 InputSlot을 할당한다.VidProvenance — 네 variant를 갖는 enum: Static, Input(InputSlot), BoundedByUniversal(InputSlot), LocalOnly. 각 borrowck region vid는 BorrowckRegionSummary 위에 VidProvenance를 갖고, 호출자 입력 공간에서 어디서 왔는지 기록한다. BoundedByUniversal은 NLL 제약 그래프 안에서 공변성만 갖는 unsizing-edge "lifetime GCD" 바운드를 기록한다.compose_all_through_chain(tcx, caller, call_id) -> Vec<Option<usize>> — batched composer. call_id 체인을 따라가며 각 링크의 edge-local GenericArgsRef<'tcx> template를 바깥 caller Instance에 대해 concretize하고, 여전히 살아 있는 각 위치를 borrowck_region_summary(body_def_id).call_site_mappings[local_id]와 VidProvenance를 통해 해석한다. Input / BoundedByUniversal provenance는 build_template_input_slot_map을 통해 바깥 caller InputSlot으로 매핑되고, Static과 LocalOnly provenance는 그 위치를 버린다(None 기록). 엔트리는 가장 바깥 링크까지 살아남아 Some(origin_walk_pos)가 되거나, 중간 어디서든 소멸해 None이 된다.MIR-less intrinsic 소비자는 자신의 native binder-variable 공간에서 엔트리를 소비하기 전에, OutlivesClass 계약에 따라 transport된 엔트리를 그 공간으로 재매핑해야 한다. 호출자 outlives 환경에서 실제로 엔트리를 생산하는 GenericArgKind::Outlives 계산은 §C.3.5에 명세되어 있다.
GenericArgKind::Outlives Computation
주어진 (caller, call_id, callee) 삼중항에 대해 실제 Outlives tail을 생산하는 query는 augmented_outlives_for_call이다. 이 쿼리는 Instance::with_outlives로 바로 덧붙일 수 있도록 sentinel이 제거된 &'tcx [GenericArg<'tcx>]를 반환한다. sentinel 자체는 이 query가 아니라 나중에 with_outlives가 prepend한다. 세 정보 조각은 모두 같은 네 단계 파이프라인에 들어간다. callee의 민감도를 얻고, call_id 체인을 따라 walk position을 구성하고, caller의 outlives oracle을 만들고, 마지막으로 augment_callee를 실행한다. 다만 MIR-backed와 MIR-less intrinsic 분기는 서로 다른 입력으로 그 파이프라인에 들어간다.
1단계 — callee의 민감도 조회. 첫 동작은 tcx.cast_relevant_lifetimes(callee)(§C.4.3)이다. trait-cast intrinsic을 전이적으로 호출하는 MIR-backed callee는 CastRelevantLifetimes 값을 반환한다. callee가 민감한 각 dyn 타입마다 하나의 LifetimeBVToParamMapping이 있고, 각 매핑은 (bv_idx, Option<callee_walk_pos>) 엔트리를 나열한다. dyn 타입의 binder variable마다 하나씩이며, None은 'static에 고정된 binder variable을 뜻한다. 모든 위치는 callee walk-order 공간 으로 표현된다. MIR-less intrinsic leaf(trait_metadata_index, trait_cast_is_lifetime_erasure_safe, trait_metadata_table, trait_metadata_table_len)는 body가 없으므로 민감도 맵에 존재하지 않고, 아래의 fallback 분기로 간다.
2단계 — call_id 체인 구성. MIR-backed callee의 경우, 쿼리는 compose_all_through_chain(tcx, caller, call_id, max_walk_pos)를 호출해 민감도가 언급하는 모든 callee walk-order 위치를 가장 바깥 소스 body의 입력 공간에 있는 origin walk-order 위치로 번역한다. composer는 체인을 가장 안쪽 링크에서 가장 바깥 링크 순으로 순회하며, 각 링크의 edge-local GenericArgsRef<'tcx> template를 instantiate_mir_and_normalize_erasing_regions로 concretize하고, 각 still-live 위치를 borrowck_region_summary(body_def_id).call_site_mappings[local_id]와 VidProvenance를 통해 해석한다. Input / BoundedByUniversal provenance는 build_template_input_slot_map을 통해 바깥 caller InputSlot으로 매핑되고, Static과 LocalOnly provenance는 해당 위치를 제거한다(None). 엔트리는 가장 바깥 링크까지 살아남아 Some(origin_walk_pos)가 되거나, 중간 어느 지점에서든 소멸해 None이 된다. 링크에 call_site_mapping이 없지만 모노모피제이션 이후에도 region이 존재하는 경우 — 예를 들어 그 edge가 U = dyn Trait<'lt>이고 U가 caller type parameter인 경우 — composer는 template 자체를 통해 위치를 전달하는 fallback을 사용한다.
3단계 — caller의 outlives 환경 구성. 이 단계는 caller의 outlives 관계에 대해 "region a가 region b를 outlive하는가?"에 답하는 oracle인 CallerOutlivesEnv를 만든다. CallerOutlivesEnv는 미리 계산된 Floyd–Warshall reachability BitMatrix(outlives_reachability((entries, dim)) 공유 query가 반환)와, caller-space key를 matrix index로 재매핑하는 선택적 key_to_idx: FxHashMap<usize, usize>를 감싼다.
'static 관례. matrix index dim - 1은 CallerOutlivesEnv 전반에서 'static에 예약된다. §C.3.4의 사용자 가시적 usize::MAX sentinel은 CallerOutlivesEnv::resolve에 의해 dim - 1로 접히고, successor가 dim - 1인 모든 reachability edge는 4단계에서 (bv, usize::MAX) 쌍을 방출하므로, 'static은 변경 없이 통과한다.
두 생성자가 두 caller 체제를 다룬다.
caller.has_outlives_entries()가 참이면, caller는 이미 자기 Instance tail에 outlives 증거를 갖고 있다. CallerOutlivesEnv::from_outlives_entries는 caller.outlives_indices_iter()를 읽고, dim을 max_idx + 2(추가 'static 슬롯 하나 포함)로 잡은 뒤, 그 쌍들을 직접 outlives_reachability에 넣는다. 이 경우 caller-space key 자체가 matrix index이므로 key_to_idx는 None이다.caller_env_for_call_id는 borrowck_region_summary(origin_def_id)에서 call-site CallSiteRegionMapping을 조회한다. 그 매핑이 없으면(원점이 제네릭이고 intrinsic arg가 type param이라 수명이 모노모피제이션 이후에야 물질화되는 경우) 비어 있는 1차원 env를 반환한다. 그렇지 않으면 CallerOutlivesEnv::from_region_summary_walk_pos를 구성한다. summary.outlives_graph.scc_successors의 SCC마다 하나의 matrix slot, 그리고 'static용 하나를 두고, 응축된 SCC edge로 seed한 뒤 outlives_reachability를 통해 캐시한다. key_to_idx는 각 call-site walk position을 그 region-vid의 SCC index로 번역한다.4단계 — augment_callee 실행. augment_callee는 callee 민감도, caller env, composed mapping을 소비해 최종 증강 Instance를 callee_instance.with_outlives(tcx, &outlives_pairs)로 생산한다. 세 단계로 동작한다.
LifetimeBVToParamMapping에 대해, 각 (bv_idx, Some(callee_walk_pos)) 엔트리마다 composed_mapping[callee_walk_pos]를 조회한다. 그것이 Some(caller_key)이면 (bv_idx, caller_key)를 기록한다. composed_mapping이 None인 경우(아래 설명하는 fallback-identity path)에는 callee walk position 자체를 caller key로 사용한다. nodes를 bv_idx로 정렬하고 중복 제거한다. 각 binder variable은 많아야 하나의 caller-space key만 기여한다.CallerOutlivesEnv::resolve를 통해 matrix index로 해석한다. key를 해석하지 못하는 노드(예: call-site mapping에 존재하지 않는 walk position)는 조용히 버린다. idx_to_bvs: matrix_idx → SmallVec<[bv_idx; 4]>를 구성한다.(bv_i, idx_i)에 대해, caller env의 reachability matrix에 따라 idx_i가 outlive하는 모든 matrix index를 순회한다. successor가 'static 슬롯이면 (bv_i, usize::MAX)를 방출한다. 그 외 successor idx_j마다, idx_to_bvs[idx_j] 안의 모든 bv_j에 대해 bv_i != bv_j이면 (bv_i, bv_j)를 방출한다. 행 순회는 reflexive hit를 보존하므로, 같은 caller key로 alias되는 두 binder variable은 상호 outlive하는 Hamiltonian 쌍을 올바르게 만들어 낸다. 마지막으로 sort와 dedup을 수행한다.이는 naive한 쌍대 probe의 O(N²)가 아니라 O(N · dim)이다. 보통 dim은 ≤ 10이지만, callee가 많은 dyn 타입에 걸쳐 많은 binder variable에 민감할 수 있으므로 이것이 중요하다.
MIR-less intrinsic fallback. 1단계 조회가 None을 반환하면, 쿼리는 intrinsic symbol로 callee를 분류하고, body가 없는 대신 합성된 민감도로 augment_callee를 실행한다. outlives 정보는 여전히 어디선가 와야 하기 때문이다.
caller.outlives_entries().len() > 1). caller를 base로 strip한 뒤, base에 대해 items_of_instance를 다시 실행하여 direct sensitivity를 복구하고, 이를 CastRelevantLifetimes::from_direct_mappings로 감싼다. caller env는 증강된 caller 자신의 Outlives 엔트리(from_outlives_entries)로부터 구성한다. direct sensitivity는 이미 caller 자신의 공간에 있으므로 composition이 필요 없고, composed_mapping은 None이며 augment_callee는 identity mode로 실행된다.caller.outlives_entries().len() <= 1: base instance이거나, 실제 쌍이 없는 sentinel-only augmentation). borrowck_region_summary(origin_def_id).call_site_mappings[origin_local_id]를 가져와 input_identity_sensitivity_for_call_site를 합성한다. 이는 provenance가 Input, BoundedByUniversal, LocalOnly인 call-site walk position 각각을 자기 자신에 매핑하는 하나의 LifetimeBVToParamMapping이며, Static provenance 위치는 버린다. caller env는 같은 매핑에 대해 from_region_summary_walk_pos로 만들고, augment_callee를 identity mode로 실행한다. 1단계를 통과하지 못한 다른(비-intrinsic) callee는 — sensitivity-map 불변식이 성립한다면 도달 불가능해야 하므로 — &[]를 반환한다.MIR-less fallback 경로에서 방출된 엔트리는 origin call site의 walk-position 또는 SCC 공간에 있다 — §C.3.4의 공간 1 이지, 아직 intrinsic의 native binder-variable 공간은 아니다. 공간 2(또는 erasure-safety intrinsic의 경우 공간 3)로의 재매핑은 intrinsic resolver(resolve_table_callee, resolve_erasure_safe_callee)가 intrinsic body를 조회하기 전에 수행한다.
Sentinel 처리. with_outlives는 항상 OUTLIVES_SENTINEL을 prepend해서 zero-pair augmentation을 base Instance와 구별한다. query는 tail이 sentinel 뒤까지 비어 있지 않으면 augmented.outlives_entries()에서 &all[1..]를, sentinel만 있으면 &[]를 반환한다. sentinel까지 포함한 전체 tail이 필요한 호출자는 반환된 Instance에서 직접 augmented.outlives_entries()를 읽는다. query의 반환값은 Phase-2 patcher가 소비하는 형태이며, 이는 각 증강 호출 지점에서 Instance::with_outlives로 다시 threading된다.
codegen_mir코드 생성용 "MIR body 가져오기" query. 모노모피제이션 collector가 이를 사용해, outlives 민감한 instance들에 대해 augmented callee 참조로 패치된 body를 코드 생성에 넘긴다.
/// Returns the MIR body to use for codegen of the given Instance.
/// Defaults to instance_mir, but may be overridden by the
/// monomorphization collector for outlives-sensitive instances
/// that need patched MIR with augmented callee references.
query codegen_mir(key: ty::Instance<'tcx>) -> &'tcx mir::Body<'tcx> {
desc { "getting codegen MIR for {}", key }
feedable
}
기본 provider는 tcx.instance_mir(instance.def)로 떨어진다. outlives 민감 instance의 경우, partitioning의 global phase가 그 instance를 최종 mono-item 집합에 넣기 직전에 tcx.feed_codegen_mir(instance, body)를 통해 패치된 body를 공급하며, 이 값이 기본 provider보다 우선한다. Instance는 항상 로컬에서 해석되므로 separate_provide_extern은 필요 없다.
코드 생성 backend는 tcx.instance_mir(instance.def) 대신 tcx.codegen_mir(instance)를 호출한다.
delayed_codegen_requests§C.2에서 설명한 크레이트별 지연 코드 생성 요청 목록이다.
/// Tracks which MIR bodies contain calls to trait casting intrinsics,
/// signaling that their codegen must be delayed until the global crate.
/// For the local crate, proxies into collect_and_partition_mono_items.
/// For upstream crates, decoded from metadata.
query delayed_codegen_requests(key: CrateNum) -> &'tcx [mir::mono::DelayedInstance<'tcx>] {
separate_provide_extern
desc { "tracking MIR bodies for delayed codegen" }
}
값 타입은 &'tcx [Instance<'tcx>]가 아니라 &'tcx [DelayedInstance<'tcx>]이다. 각 엔트리는 global phase가 필요로 하는 augmented-callee substitution map과 intrinsic-callee list를 담는다(구조체 레이아웃은 위 §C.2 참조). 이 query는 feedable이 아니다. 로컬 provider는 collect_local_mono_items(())에서 투영한다.
providers.queries.delayed_codegen_requests = |tcx, _key: LocalCrate| { tcx.collect_local_mono_items(()).delayed_codegen };
collect_and_partition_mono_items가 아니라 collect_local_mono_items를 고른 것은 핵심적이다. 이는 사이클(collect_and_partition_mono_items → gather_trait_cast_requests → delayed_codegen_requests → collect_and_partition_mono_items)을 끊는다.
extern provider는 rmeta의 per-crate LazyArray<DelayedInstance<'static>>를 새 arena slice로 디코드한다.
codegen_mir과 delayed_codegen_requests 외에도, 이 RFC는 다음 global-phase query를 도입한다. 각 query는 컴파일 세션마다 한 번 선언되며, §C.3에서 설명한 "gather → classify → layout → populate → emit" 파이프라인의 한 단계를 구동한다.
gather_trait_cast_requests(()) -> &'tcx TraitCastRequests<'tcx> (arena-cached) — 모든 크레이트의 delayed_codegen_requests를 모아 분류된 버킷으로 집계한다.trait_cast_graph(root: Ty<'tcx>) -> &'tcx TraitGraph<'tcx> (arena-cached) — 모노모피제이션된 노드 위의 루트 슈퍼트레이트별 도달 가능한 트레이트 그래프trait_cast_layout(root: Ty<'tcx>) -> &'tcx TableLayout<'tcx> (arena-cached) — root를 루트로 하는 메타데이터 테이블에 대한 outlives-class condensation 및 per-slot 할당trait_cast_table(key: (Ty<'tcx>, Ty<'tcx>)) -> &'tcx [Option<AllocId>] — per-(root, concrete) 슬롯 벡터에 허용 가능한 슬롯의 vtable AllocId와 나머지 슬롯의 None을 채운다trait_cast_table_alloc(key: (Ty<'tcx>, Ty<'tcx>)) -> AllocId — 메타데이터 테이블을 backing하는 불변 per-(root, concrete) static을 방출한다global_crate_id_alloc(()) -> AllocId — global-crate 식별자로 쓰이는 1바이트 static을 방출한다(Identity tokens 및 Appendix C §C.6 참조)impl_universally_admissible(impl_def_id: DefId) -> bool — 레이아웃 condensation이 소비하는 fast-path 허용 가능성 검사outlives_reachability(key: (&'tcx [GenericArg<'tcx>], usize)) -> &'tcx BitMatrix<usize, usize> (arena-cached) — 레이아웃, population, erasure-safety 검사가 공유하는 dim 차원 인덱스 공간 위의 Floyd–Warshall reflexive-transitive closureis_lifetime_erasure_safe(key: (Ty<'tcx>, Ty<'tcx>, &'tcx [Option<usize>], &'tcx [GenericArg<'tcx>])) -> bool — walk-position 공간에서의 (super_trait, target_trait, origin_positions, call_site_outlives) 튜플에 대한 사이트별 erasure-safety 결과augmented_outlives_for_call((Instance<'tcx>, &'tcx List<(DefId, u32, GenericArgsRef<'tcx>)>, Instance<'tcx>)) -> &'tcx [GenericArg<'tcx>] — call_id 체인을 caller의 outlives 환경을 통해 구성하고, Instance::with_outlives에 바로 넣을 수 있는 sentinel-stripped Outlives tail을 반환하는 호출 지점별 outlives-entry 도출cast_relevant_lifetimes(Instance<'tcx>) -> Option<&'tcx CastRelevantLifetimes<'tcx>> — 크레이트 수준 맵에 대한 per-Instance thin lookup. 민감하지 않은 Instance에는 Nonecrate_cast_relevant_lifetimes(CrateNum) -> &'tcx UnordMap<Instance<'tcx>, CastRelevantLifetimes<'tcx>> (separate_provide_extern) — 크레이트 수준 SCC-batch 민감도 맵. per-Instance query는 여기서 투영한다.이와 함께 §C.3에 설명한 두 borrowck 쪽 query도 참조하라.
borrowck_result(LocalDefId) -> &'tcx mir::BorrowckResult<'tcx>.borrowck_region_summary(DefId) -> &'tcx mir::BorrowckRegionSummary (separate_provide_extern).
코드 생성 크레이트의 변경은 최소다. instance_mir 대신 새로운 codegen_mir query를 사용하면 된다. ty::Instance의 유일성과 hashing은 보존된다.
Identity tokens 에서 설명한 계약은 각 global crate가 고유 주소를 가진 &'static u8을 방출하고, 주소 중요성이 없는 상수를 병합할 수 있는 모든 단계를 가로질러 그 주소를 구별되게 유지할 것을 요구한다. rustc는 이를 할당의 address_significant 플래그와 backend별 lowering으로 만족한다. 적합한 구현은, 어떤 backend나 linker 패스도 유일성 보장을 무력화할 수 없기만 하다면, 다른 수단(예: 병합 불가능 섹션의 per-crate sentinel symbol)으로 계약을 만족해도 된다.
구체적으로, 토큰은 global_crate_id_alloc query(§C.4.3 참조)가 global crate마다 한 번 생성하는 AllocId다. 할당은 1바이트 불변 값이며(내용은 지정되지 않음), 네 개의 trait-cast intrinsic은 모두 이 동일한 AllocId를 첫 번째 튜플 요소로 반환하고, &'static u8로 승격한다.
모든 할당은 address_significant: bool 필드를 가지며, 기본값은 false이고 global-crate-id 할당에 대해서만 true로 설정된다. 이 플래그는 allocation interning에 참여하므로, 다른 것은 동일해도 하나가 address-significant이면 두 할당은 구별된다. 코드 생성 backend는 GlobalAlloc::Memory를 lowering할 때 이 플래그를 관찰한다.
UnnamedAddr::No로 방출하여, GlobalOpt, LTO, linker ICF의 unnamed_addr 기반 병합을 억제한다. 이것이 없으면 LLVM은 private zero-byte constant를 기본적으로 병합 가능하게 취급하므로, compilation unit 간 중복된 zero-byte global이 합쳐져 계약을 위반할 수 있다.