표준 라이브러리 유지 관리하기
누군가 제게
r+를 주기 전에 알았더라면 좋았을 모든 것
이 문서는 Rust 표준 라이브러리를 개발하고 유지 관리하는 데 필요한 맥락의 일부를 담아내려는 노력입니다. 이 문서의 목표는 라이브러리 팀 구성원들이 표준 라이브러리 작업에 적용하는 절차와 경험을 공유하여 다른 구성원들도 도움을 받을 수 있도록 하는 것입니다. 아마 더 넓은 Rust 커뮤니티의 구성원들에게도 흥미로울 만한 소소한 정보가 많이 쌓일 것입니다.
이 문서는 모범 사례나 좋은 스타일을 논의하려 하지 않습니다. 이에 대해서는 API 가이드라인을 참고하십시오.
기여하기
오래되었거나, 충분히 명시되지 않았거나, 누락되었거나, 그냥 틀린 내용을 발견하시면 언제든지 rust-lang/rust-forge 저장소에 PR을 열어 주십시오!
용어
- 라이브러리 팀(Libs). 바로 저희입니다! 표준 라이브러리의 개발과 유지 관리(그 외 여러 업무 포함)를 담당하는 팀입니다.
- 풀 리퀘스트(PR).
rust-lang/rust에 대한 일반적인 GitHub 풀 리퀘스트입니다. - 의견 요청(RFC).
rust-lang/rfcs에 작성되며 새로운 기능을 도입하는 공식 문서입니다. - 추적 이슈.
C-tracking-issue태그가 붙은 GitHub의 일반 이슈입니다. - 최종 의견 수렴 기간(FCP). 관련 팀이 RFC와 PR을 리뷰할 기회를 갖도록
rfcbot이 조율합니다.
확신이 서지 않을 때…
표준 라이브러리를 유지 관리하는 일은 벅찬 책임처럼 느껴질 수 있습니다! triagebot을 통한 자동 리뷰어 배정으로 인해 여러분은 수많은 새로운 맥락에 놓이게 될 것입니다.
언제든지 GitHub에서 @rust-lang/libs 팀에 핑을 보내십시오. 저희 모두 도와드리기 위해 여기 있습니다!
자신이 PR을 리뷰하기에 가장 적합한 사람이 아니라고 생각되면 triagebot을 사용해 다른 사람에게 배정하십시오.
여러분의 의견을 기다리는 리뷰 찾기
https://rfcbot.rs/ 를 정기적으로 확인하는 것을 잊지 마십시오. 자신의 닉네임이 나타난 곳 중 아무 곳이나 클릭하면 https://rfcbot.rs/fcp/SimonSapin 과 같이 여러분의 의견을 기다리는 리뷰만 보여 주는 페이지로 이동합니다.
PR 리뷰하기
라이브러리 팀 구성원으로서 여러분은 리뷰가 필요한 PR에 배정되고, Rust 프로젝트의 이슈에 대해 의견을 요청받게 될 것입니다.
RFC는 언제 필요합니까?
새로운 불안정 기능은 병합되기 전에 RFC가 필요하지 않습니다. 기능이 작고 설계 공간이 단순하다면, 해당 기능을 안정화하려면 보통 FCP만 거치면 됩니다. 그러나 때로는 안정화하기 전에 RFC를 요청할 수도 있습니다.
unsafe가 있습니까?
표준 라이브러리의 unsafe 코드 블록에는 그것이 왜 괜찮은지 설명하는 주석이 필요합니다. 이를 검사하는 tidy 린트가 있습니다. 또한 unsafe 코드는 실제로도 괜찮아야 합니다.
무엇이 건전하고 무엇이 그렇지 않은지에 관한 규칙은 미묘할 수 있습니다. 현재의 견해는 Unsafe Code Guidelines WG를 참고하시고, 조금이라도 의심스럽다면 @rust-lang/libs, @rust-lang/lang 및/또는 해당 WG의 누군가에게 핑하는 것을 고려하십시오. 저희는 unsafe 코드의 건전성에 대해 토론하는 것을 매우 좋아하며, 더 많은 사람이 살펴볼수록 더 좋습니다!
그 #[inline]은 올바릅니까?
인라이닝은 잠재적인 실행 속도, 컴파일 시간, 코드 크기 사이의 트레이드오프입니다. hashbrown 크레이트에 대한 이 PR에서 이에 관한 논의를 볼 수 있습니다. 해당 스레드에서 인용합니다:
#[inline]은 단순한 인라인 힌트와는 매우 다릅니다. 앞서 말씀드렸듯이, C++에는#[inline]이 하는 일에 해당하는 것이 없습니다. 디버그 모드에서 rustc는 기본적으로#[inline]을 무시하며, 마치 그것을 작성하지 않은 것처럼 취급합니다. 릴리스 모드에서 컴파일러는 기본적으로#[inline]함수를 그것을 참조하는 모든 코드 생성 단위 각각에 코드 생성하며, 그런 다음inlinehint도 추가합니다. 즉, CGU가 16개 있고 모두 어떤 항목을 참조한다면, 각각의 CGU 모두에 해당 항목의 전체 구현이 인라인된다는 뜻입니다.
다음의 경우 #[inline]을 추가할 수 있습니다:
- 공개되어 있고, 작고, 제네릭이 아닌 함수.
다음의 경우에는 #[inline]이 필요하지 않습니다:
- 스코프 내에 제네릭이 있는 메서드.
- 기본 구현이 없는 트레이트의 메서드.
#[inline]은 언제든지 나중에 도입할 수 있으므로, 의심스럽다면 그냥 제거해도 됩니다.
#[inline(always)]는 어떻습니까?
#[inline(always)]가 필요한 경우는 거의 없습니다. 제한된 수의 위치에서 사용되는 비공개 헬퍼 메서드나 사소한 연산자에는 유용할 수 있습니다. 이 속성을 사용하려면 마이크로 벤치마크로 그 타당성을 입증해야 합니다.
호환성이 깨질 가능성이 있습니까?
호환성을 깨는 변경은 가능한 한 피해야 합니다. RFC 1105는 무엇이 호환성을 깨는 변경에 해당하는지에 대한 기초가 되는 재단을 마련합니다. 호환성 깨짐을 허용할 수 있는지는 실제 영향에 따라 판단하며, 그 영향은 crater 실행으로 대략 추정할 수 있습니다.
영향의 정도에 따라 호환성 깨짐을 완화하는 여러 전략이 있습니다.
가치도 크고 영향도 큰 변경의 경우:
- 컴파일러 린트를 사용해 깨지는 동작을 단계적으로 없애도록 시도합니다.
영향이 그리 크지 않은 경우:
- 깨지는 크레이트의 메인테이너를 논의에 참여시키고, 이를 고치는 PR을 제출합니다.
동작이 변경됩니까?
호환성을 깨는 변경은 컴파일 실패에만 국한되지 않습니다. stable 함수의 동작을 바꾸는 변경은 일반적으로 받아들여질 수 없습니다. 예시로 home_dir 이슈를 참고하십시오.
stable 트레이트에 대한 새 impl이 있습니까?
표준 라이브러리에 대한 많은 PR은 이미 stable인 트레이트에 새 impl을 추가하는데, 이는 온갖 기상천외한 방식으로 사용자 코드를 깨뜨릴 수 있습니다. 다음 절에서는 새 트레이트 impl로 인한 호환성 깨짐 가운데 표준 라이브러리에 가해진 변경만 보아서는 분명하지 않을 수 있는 몇 가지 예를 소개합니다.
두 번째 제네릭 impl이 도입되면 타입 추론이 깨집니다
Rust는 타입 추론 과정에서 제네릭 트레이트에 대한 impl이 하나뿐이라는 사실을 활용합니다. 두 번째 impl이 추가되어 해당 제네릭의 타입이 모호해지면 이 추론이 깨집니다. 다음과 같은 코드가 있다고 합시다.
#![allow(unused)]
fn main() {
// in `std`
impl From<&str> for Arc<str> { .. }
}
#![allow(unused)]
fn main() {
// in an external `lib`
let b = Arc::from("a");
}
여기에 다음을 추가하면
impl From<&str> for Arc<str> { .. }
+ impl From<&str> for Arc<String> { .. }
다음 코드는
#![allow(unused)]
fn main() {
let b = Arc::from("a");
}
더 이상 컴파일되지 않습니다. 이전에는 Box<T>의 T를 알아내는 데 타입 추론에 의존했기 때문입니다.
이런 종류의 호환성 깨짐은 괜찮을 수도 있지만, crater 실행으로 그 범위를 추정해야 합니다.
새 impl이 도입되면 deref 강제 변환이 깨집니다
Rust는 인자가 타입 검사를 직접 통과하지 못하면 deref 강제 변환을 사용해 유효한 트레이트 impl을 찾습니다. 이는 impl이 하나뿐일 때만 일어나는 것으로 보이므로, 새 impl을 도입하면 deref 강제 변환에 의존하는 사용자 코드가 깨질 수 있습니다. 다음과 같은 코드가 있다고 합시다.
#![allow(unused)]
fn main() {
// in `std`
impl Add<&str> for String { .. }
impl Deref for String { type Target = str; .. }
}
#![allow(unused)]
fn main() {
// in an external `lib`
let a = String::from("a");
let b = String::from("b");
let c = a + &b;
}
여기에 다음을 추가하면
impl Add<&str> for String { .. }
+ impl Add<char> for String { .. }
다음 코드는
#![allow(unused)]
fn main() {
let c = a + &b;
}
더 이상 컴파일되지 않습니다. &String을 &str로 강제 변환하기 위해 deref를 사용하려고 시도하지 않기 때문입니다.
이런 종류의 호환성 깨짐은 괜찮을 수도 있지만, crater 실행으로 그 범위를 추정해야 합니다.
기존 기능을 사용해 구현할 수 있습니까?
String 같은 타입은 Vec<u8>를 기반으로 구현되며, deref 강제 변환을 통해 str의 메서드를 사용할 수 있습니다. Vec<T>는 deref 강제 변환을 통해 [T]의 메서드를 사용할 수 있습니다. 가능하다면 String 같은 래핑 타입의 메서드는 내부 저장소나 deref 대상에 이미 존재하는 메서드에 위임해야 합니다.
#[fundamental] 항목이 관련되어 있습니까?
#[fundamental] 타입은 일관성(coherence) 규칙이 다르기 때문에 이러한 타입에는 포괄(blanket) 트레이트 impl을 추가할 수 없습니다. 자세한 내용은 RFC 1023을 참고하십시오. 여기에는 다음이 포함됩니다:
&T&mut TBox<T>Pin<T>
특수화(specialization)가 관련되어 있습니까?
특수화는 현재 불안정(unstable) 기능입니다. 진행 상황은 여기에서 확인할 수 있습니다.
저희는 특수화에 지나치게 의존하지 않으려 하며, 그 사용을 특정 구현의 최적화로 제한합니다. 이러한 특수화 최적화는 공개 메서드 자체를 특수화하는 대신, 비공개 트레이트를 사용하여 올바른 구현을 찾습니다. 외부 호출자에 대한 메서드 디스패치 방식을 바꾸는 특수화 사용은 모두 신중하게 검토해야 합니다.
표준 라이브러리에서 특수화를 사용하는 방법의 예로, &[T]로부터 Rc<[T]>를 생성하는 경우를 살펴봅시다:
#![allow(unused)]
fn main() {
impl<T: Clone> From<&[T]> for Rc<[T]> {
#[inline]
fn from(v: &[T]) -> Rc<[T]> {
unsafe { Self::from_iter_exact(v.iter().cloned(), v.len()) }
}
}
}
T: Copy인 경우에는 최적화된 구현이 있으면 좋을 것입니다:
#![allow(unused)]
fn main() {
impl<T: Copy> From<&[T]> for Rc<[T]> {
#[inline]
fn from(v: &[T]) -> Rc<[T]> {
unsafe { Self::copy_from_slice(v) }
}
}
}
안타깝게도 이 두 impl은 서로 겹치기 때문에 일반적으로는 둘 다 가질 수 없습니다. 바로 이런 경우에 비공개 특수화를 사용하여 내부적으로 올바른 구현을 선택할 수 있습니다. 이 경우에는 구현을 전환하는 RcFromSlice라는 트레이트를 사용합니다:
#![allow(unused)]
fn main() {
impl<T: Clone> From<&[T]> for Rc<[T]> {
#[inline]
fn from(v: &[T]) -> Rc<[T]> {
<Self as RcFromSlice<T>>::from_slice(v)
}
}
/// Specialization trait used for `From<&[T]>`.
trait RcFromSlice<T> {
fn from_slice(slice: &[T]) -> Self;
}
impl<T: Clone> RcFromSlice<T> for Rc<[T]> {
#[inline]
default fn from_slice(v: &[T]) -> Self {
unsafe { Self::from_iter_exact(v.iter().cloned(), v.len()) }
}
}
impl<T: Copy> RcFromSlice<T> for Rc<[T]> {
#[inline]
fn from_slice(v: &[T]) -> Self {
unsafe { Self::copy_from_slice(v) }
}
}
}
min_specialization 기능을 사용하는 특수화만 사용해야 합니다. 완전한 specialization 기능은 건전하지 않은(unsound) 것으로 알려져 있습니다.
공개 열거형이 있습니까?
새로운 변형(variant)이 추가될 가능성이 조금이라도 있다면, 호환성을 깨뜨리지 않고 추가할 수 있도록 공개 열거형에 #[non_exhaustive] 속성을 지정해야 합니다.
이 변경이 드롭 순서를 바꿉니까?
컬렉션 내부 구조를 변경하면 항목이 드롭되는 순서에 영향을 줄 수 있습니다. 이는 과거에도 허용된 적이 있지만, 반드시 명시해야 합니다.
수동으로 작성한 Drop 구현이 있습니까?
Drop을 수동으로 구현하는 제네릭 Type<T>는 T에 #[may_dangle] 속성이 적절한지 검토해야 합니다. #[may_dangle]이 무엇인지에 대한 자세한 내용은 Nomicon에서 확인할 수 있습니다.
제네릭 Type<T>에 T의 drop까지 수반할 수 있는 수동 drop 구현이 있다면, dropck가 이를 알아야 합니다. Type<T>의 T 소유권이 ManuallyDrop<T>, *mut T, MaybeUninit<T>처럼 스스로 T를 drop하지 않는 타입을 통해 표현된다면, Type<T>에는 T가 drop될 수 있음을 dropck에 알리기 위한 PhantomData<T> 필드도 필요합니다. 내부 포인터 타입인 Unique<T>를 사용하는 표준 라이브러리의 타입에는 PhantomData<T> 마커 필드가 필요하지 않습니다. 이 부분은 Unique<T>가 대신 처리해 줍니다.
이것이 잘못될 수 있는 실제 사례로, 대략 다음과 같은 형태의 OptionCell<T>를 생각해 보십시오:
#![allow(unused)]
fn main() {
struct OptionCell<T> {
is_init: bool,
value: MaybeUninit<T>,
}
impl<T> Drop for OptionCell<T> {
fn drop(&mut self) {
if self.is_init {
// Safety: `value` is guaranteed to be fully initialized when `is_init` is true.
// Safety: The cell is being dropped, so it can't be accessed again.
unsafe { self.value.assume_init_drop() };
}
}
}
}
PhantomData<T> 마커 필드가 없던 이 OptionCell<T>에 #[may_dangle] 속성을 추가하자, OptionCell<T>보다 엄밀하게 더 오래 살지 않는 T에 대해 건전성 구멍이 생겼고, 그 결과 이러한 T는 자신의 Drop 구현 안에서 drop된 이후에도 접근될 수 있었습니다. #[may_dangle]를 올바르게 적용하려면 PhantomData<T> 필드도 필요했습니다:
struct OptionCell<T> {
is_init: bool,
value: MaybeUninit<T>,
+ _marker: PhantomData<T>,
}
- impl<T> Drop for OptionCell<T> {
+ unsafe impl<#[may_dangle] T> Drop for OptionCell<T> {
mem은 어떻게 가정을 깨뜨릴 수 있습니까?
mem::replace와 mem::swap
&mut 참조 뒤에 있는 모든 Sized 값은 mem::replace나 mem::swap을 사용해 새 값으로 교체될 수 있으므로, 코드는 도달 가능한 가변 참조의 내부가 교체로 인해 바뀌지 않는다고 가정해서는 안 됩니다.
mem::forget
Rust는 값이 누수될 때(mem::forget으로 누수시킬 수 있습니다) 소멸자가 실행된다는 것을 보장하지 않으므로, 코드는 안전성을 유지하기 위해 소멸자에 의존하는 것을 피해야 합니다. 기억하십시오, 누구나 똥을 쌉니다.
값이 누수될 때 소멸자를 실행하지 않아도 괜찮습니다. 그 저장 공간이 해제되거나 다른 용도로 재사용되지 않기 때문입니다. 저장 공간이 초기화된 상태에서 해제되거나 다른 용도로 재사용된다면 소멸자를 먼저 실행해야 합니다. 메모리가 고정(pin)되어 있을 수 있기 때문입니다. 그렇기는 하지만, 고정이 전혀 관여하지 않음을 보장할 수 있다면 해제 시 소멸자 실행을 건너뛰는 예외가 여전히 있을 수 있습니다.
성능에는 어떤 영향이 있습니까?
핫 코드를 변경하면 좋든 나쁘든 사용하는 쪽의 성능에 영향을 줄 수 있습니다. 적절한 벤치마크를 통해 성능 특성이 어떻게 변하는지 파악할 수 있어야 합니다. rustc 자체에 영향을 주는 변경이라면 rust-timer 실행을 할 수도 있습니다.
커밋 로그가 깔끔합니까?
PR에는 병합 커밋이 포함되어서는 안 됩니다. PR이 기본 브랜치에 비해 오래된 상태가 되면 리베이스해야 합니다.
PR 병합
rust-lang/rust에 대한 PR은 GitHub UI를 사용하거나 원격 브랜치를 푸시하는 방식으로 수동 병합하지 않습니다. 모든 것은 bors를 거칩니다.
rollup을 사용해야 할 때
라이브러리 PR은 대개 롤업해도 괜찮으며, 특히 새로운 unstable 항목 추가만 있거나 문서만 수정하는 경우에는 더욱 그렇습니다.
언제 롤업해야 하는지에 대한 자세한 내용은 롤업 가이드라인을 참고하십시오. 롤업의 취지는 여러 PR을 개별적으로 병합하는 대신 한데 모아 한 번에 병합하는 것입니다. 이렇게 하면 병합 속도를 높일 수 있지만, 충돌할 가능성이 높은 PR이나 롤업 안에서 성능 특성이 가려질 수 있는 PR에는 적절하지 않을 수 있습니다.
새로운 공개 항목이 있을 때
새로운 기능이라면 해당 기능의 추적 이슈를 열어야 합니다. 추적 이슈에 무엇을 담아야 하는지 감을 잡으려면 이전 추적 이슈 몇 개를 살펴보십시오. #[unstable] 속성의 issue 필드를 추적 이슈 번호로 갱신해야 합니다.
불안정 기능은 준비가 된 것으로 보이면 평소처럼 bors를 통해 병합할 수 있습니다.
새로운 트레이트 impl이 있을 때
stable 트레이트에 대한 트레이트 impl을 불안정하게 만들 방법은 없으므로, 이미 안정화된 트레이트에 새 impl을 추가하는 모든 PR은 병합 전에 FCP를 거쳐야 합니다. 다만 트레이트 자체가 불안정하다면 impl도 불안정해야 합니다.
기능을 안정화할 때
기능은 #[unstable] 속성을 #[stable] 속성으로 교체하는 PR을 통해 안정화할 수 있습니다. 기능을 안정화하려면 먼저 승인된 RFC가 있어야 합니다. 또한 병합 전에 FCP를 거쳐야 합니다.
#[stable] 속성에 사용할 올바른 버전은 Forge에서 확인할 수 있습니다.
const 함수를 안정화할 때
const 함수는 #[rustc_const_unstable] 속성을 #[rustc_const_stable] 속성으로 교체하는 PR을 통해 안정화할 수 있습니다. 해당 const 특성을 앞으로 보장할 것인지에 대해 Constant Evaluation WG에 핑을 보내 의견을 구해야 합니다. const 안정화되는 대상이 새로 노출되는 인트린식이라면 @rust-lang/lang도 FCP에 포함해야 합니다.
함수가 #[allow_internal_unstable] 속성을 통해 내부적으로 다른 불안정 const 함수에 의존하는지 확인하고, 내부의 불안정 호출을 제거한다면 함수를 어떻게 구현할 수 있을지 검토하십시오. #[allow_internal_unstable]에 대한 자세한 내용은 Stability attributes 페이지를 참고하십시오.
unsafe와 const가 함께 관련된 경우, 예를 들어 “unconst“인 연산의 경우에는 그 사용에 대한 const 안전성 논거도 문서화해야 합니다. 즉, const fn에는 반드시 지켜야 하는 추가적인 결정성 제약(예: 런타임과 컴파일 타임의 결과가 일치해야 하며 함수의 출력은 입력에만 의존해야 함 등)이 있으며, unsafe를 사용할 때는 이러한 제약이 지켜진다는 근거를 제시해야 합니다.
기능을 지원 중단할 때
지원 중단된 항목으로 인해 문서가 어수선해지는 것을 줄이기 위해, 해당 항목은 모듈이나 impl 블록의 맨 아래로 옮겨 문서 페이지의 맨 아래에 표시되도록 해야 합니다. 그런 다음 문서 내용은 항목의 사용 방법보다는 해당 항목이 지원 중단된 이유에 초점을 맞추도록 줄여야 합니다.