Any 특성은 컴파일 시간 타입 정보가 없는 오류 처리 및 디버깅 시나리오를 위해 동적 타이핑 기능을 제공하기 위해 Rust 개발 초기 단계에서 도입되었습니다. 이 설계는 **C++**의 typeid나 Java의 instanceof와 유사한 개념을 반영하지만, Rust의 소유권 모델은 고유한 제약을 부과합니다. 'static 요구 사항은 타입 제거된 참조가 자신이 설명하는 데이터보다 오래 살아남지 않도록 보장할 필요성에서 발생하였으며, 가비지 컬렉션 없이 메모리 해제 후 사용 오류를 방지합니다.
'static 제약 없이, Any로 제거된 타입은 제한된 수명을 가진 스택 로컬 데이터에 대한 참조를 포함할 수 있습니다. Any 특성 객체가 그 스택 프레임보다 오래 살아남으면, 다운캐스팅과 역참조는 할당 해제된 메모리에 접근하게 됩니다. Any는 가상 테이블(vtables) 및 타입 제거를 통해 작동하기 때문에, 컴파일러는 다운캐스팅 시점에서 수명을 검증할 수 없으며; 'static 제약은 타입이 모든 데이터를 소유하거나 정적 참조만 포함하는지를 보장하는 보수적 보증 역할을 합니다. 이는 제거 경계를 넘어 메모리 안전성을 보장합니다.
Any 특성 정의 trait Any: 'static은 Rust의 특성 제약 시스템을 활용하여 이 제약을 컴파일 타임에 강제합니다. 비정적 참조가 없는 타입만 Any를 구현할 수 있으며, 이는 모든 &dyn Any 또는 **Box<dyn Any>**가 프로그램 전체 기간 동안 유효하도록 보장합니다. 이로 인해 downcast_ref() 및 downcast_mut()를 통해 안전한 다운캐스팅이 가능하며, 기본 데이터가 범위 종료로 인해 무효화되지 않도록 보장됩니다.
우리는 게임 엔진을 위한 플러그인 시스템을 구축하고 있었으며, 스크립트가 이벤트 핸들러를 등록하고 엔진에 임의의 데이터를 반환할 수 있었습니다. 엔진은 이러한 반환 값을 이종 큐에 저장해야 했으며, 이를 통해 다양한 하위 시스템이 후에 처리할 수 있도록 해야 했습니다. 이를 위해서는 타입 제거가 필요했으며, 여러 타입을 단일 컬렉션에 저장해야 했습니다. 그러나 일부 스크립트 바인딩은 스크립트 실행 컨텍스트 내의 임시 로컬 변수에 대한 참조를 반환하려고 했으며, 이는 스크립트 프레임이 완료되면 dangling 되었을 것입니다.
해결책 1: 수명 매개변수를 가진 사용자 정의 특성
한 가지 접근 방식은 수명 매개변수를 위한 연관 타입을 가진 사용자 정의 특성 PluginResult를 만드는 것이었습니다. 이렇게 하면 엔진이 특성 객체를 통해 수명을 추적할 수 있었습니다. 이것은 빌린 데이터를 허용함으로써 유연성을 약속했지만, 플러그인 API 표면 전반에 걸쳐 복잡한 수명 주석이 필요했습니다. 이 복잡성은 모든 플러그인 저자가 고급 Rust 수명 메커니즘을 이해해야 하므로 너무 가파른 학습 곡선을 만들어, 서드파티 코드에서 미세한 수명 버그의 위험을 증가시켰습니다.
해결책 2: 안전하지 않은 수명 변환
또 다른 해결책은 데이터를 저장할 때 수명을 변환하는 데 unsafe 코드를 사용하는 것이었습니다. 본질적으로 엔진이 원본 범위가 종료되기 전에 모든 참조를 제거할 것이라고 약속했습니다. 이렇게 하면 원하는 API의 사용 편의성이 보장되었지만, 메모리 안전성의 부담이 전적으로 엔진 개발자에게 넘어갔습니다. 참조의 출처를 추적하는 데 실수가 생기면 exploitable use-after-free 취약점이 발생하여 Rust의 안전 보장을 위반하며, 코드베이스 감사가 어려워질 것입니다.
우리는 모든 플러그인 반환 값이 'static 제약을 가진 Any를 구현하도록 요구하기로 결정했습니다. 이는 스크립트 저자가 소유 데이터를 반환하거나 Arc로 래핑된 공유 상태를 반환하도록 강제했습니다. 이러한 결정은 제로 복사 참조의 이론적인 성능 이점을 일부 희생한 대신, 엔진의 이벤트 큐가 안전하게 데이터를 저장하고 비동기적으로 처리할 수 있는 보장을 제공했습니다. 그 결과, 공용 인터페이스에 unsafe 코드가 없는 견고한 플러그인 API가 만들어졌지만, 이전에 임시 대출에 의존했던 타입에 대해 직렬화 계층을 추가해야 했습니다.
왜 Any는 단순히 특성 객체를 생성하는 데 사용되는 참조의 수명이 아닌 'static을 요구합니까?
Any 특성은 컴파일 시간에 타입 정보를 제거하여 가상 테이블을 생성하며, 이 과정에서 모든 수명 데이터를 잃어버립니다. &dyn Any를 생성할 때 컴파일러는 특성 객체에 원래 수명 'a를 인코딩할 수 있는 방법이 없으며, 나중에 다운캐스팅 기계가 검증할 수 없습니다. 'static을 요구하는 것은 원래 타입이 dangling 포인터를 포함하지 않도록 보장하는 유일한 방법입니다. 만약 Any가 더 짧은 수명을 수용했다면, 가상 테이블 포인터 자체가 수명 메타데이터를 갖고 있어야 하며, 이는 Rust가 의존적 타입이나 런타임 대출 검사를 구현해야 함을 의미하며, 언어의 영구 비용 없는 추상화 모델을 근본적으로 변경하게 됩니다.
**원래 타입이 비정적 참조를 포함할 때 **Box<dyn Any>는 'static 제약과 어떻게 상호작용합니까?
예를 들어 struct Wrapper<'a>(&'a str)와 같은 타입은 Any를 구현할 수 없습니다. 왜냐하면 'static 특성 제약을 충족하지 않기 때문입니다. 따라서 Wrapper<'a> 인스턴스에서 **Box<dyn Any>**를 생성할 수 없습니다. 후보자들은 종종 값에 박스를 씌우는 것이 그 수명을 연장한다고 잘못 생각하지만, Box는 힙에 대한 할당 소유만 담당할 뿐, 해당 할당 내 필드에서 참조하는 데이터에 대한 소유는 하지 않습니다. 만약 참조된 데이터가 스택 로컬이라면, 외부 구조체를 힙으로 이동하는 것이 참조의 수명을 연장하지 않기 때문에, 컴파일러는 **Box<dyn Any>**로의 변환을 정확히 거부합니다. 이는 힙에 할당된 박스가 참조된 데이터를 포함하는 스택 프레임보다 오래 살아남는 상황을 방지합니다.
수명 추적을 수동으로 수행하여 unsafe 코드를 사용하여 'static 요구 사항을 완화하는 사용자 정의 Any 특성을 안전하게 구현할 수 있습니까?
기술적으로는 수명을 변환하고 사용자 정의 가상 테이블을 만드는 데 unsafe를 사용할 수 있지만, 그러한 구현은 Rust의 특성 시스템과 대출 검사기가 다운캐스팅 지점에서 수명 불변성을 검증할 수 없기 때문에 안전하지 않습니다. 원본 범위가 여전히 존재하는지 모든 접근 시 확인하는 런타임에서 수명을 추적하는 병렬 타입 시스템을 구현해야 합니다. 이러한 접근 방식은 본질적으로 가비지 컬렉터 또는 참조 카운팅 시스템을 재구현하는 것이며, Rust의 컴파일 타임 보장을 잃게 됩니다. 더욱이, 어떤 unsafe 구현도 Any 불변성이 기대되는 표준 라이브러리 구성 요소와 비정상적으로 상호작용하여, std::any::Any 특성 객체와 혼합할 때 정의되지 않은 동작을 초래할 수 있습니다.