Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

표준 라이브러리 유지 관리

누군가 제게 r+를 부여하기 전에 알았더라면 좋았을 모든 것

이 문서는 Rust 표준 라이브러리를 개발하고 유지 관리하는 데 필요한 맥락을 일부나마 담으려는 노력입니다. 이 문서의 목표는 라이브러리 팀 구성원들이 표준 라이브러리 작업에서 얻은 과정과 경험을 공유하여 다른 구성원들도 도움을 받을 수 있도록 하는 것입니다. 이 문서는 Rust 커뮤니티 전반의 구성원들에게도 흥미로울 수 있는 여러 잡학 지식을 아마 축적하게 될 것입니다.

이 문서는 모범 사례나 좋은 스타일을 논의하려는 시도는 하지 않습니다. 그에 대해서는 API Guidelines를 참고하십시오.

기여하기

오래되었거나, 명세가 부족하거나, 누락되었거나, 그냥 틀린 부분을 발견하시면 rust-lang/rust-forge 저장소에 자유롭게 PR을 열어 주십시오!

용어

  • 라이브러리 팀(Libs). 바로 저희입니다! 표준 라이브러리의 개발과 유지관리(를 비롯한 여러 일)를 담당하는 팀입니다. 바로 저희입니다! 표준 라이브러리의 개발과 유지 관리(및 그 외 여러 업무)를 담당하는 팀입니다.
  • 풀 리퀘스트(PR). rust-lang/rust에 대한 일반적인 GitHub 풀 리퀘스트입니다.
  • 코멘트 요청(RFC). rust-lang/rfcs에 작성되는, 새로운 기능을 도입하는 공식 문서입니다.
  • 추적 이슈. C-tracking-issue 태그가 붙은 일반적인 GitHub 이슈입니다.
  • 최종 의견 수렴 기간(FCP)입니다. rfcbot이 조율하며, 관련 팀들에게 RFC와 PR을 검토할 기회를 부여합니다.

확신이 서지 않으신다면…

표준 라이브러리를 유지 관리하는 것은 부담스러운 책임처럼 느껴질 수 있습니다! triagebot를 통한 자동 리뷰어 배정으로 인해, 여러분은 수많은 새로운 맥락 속에 놓이게 될 것입니다.

언제든 GitHub에서 @rust-lang/libs 팀에 핑을 보내십시오. 저희 모두가 도와드리기 위해 여기 있습니다!

자신이 PR을 검토하기에 가장 적합한 사람이 아니라고 생각되신다면 triagebot을 사용해 다른 사람에게 배정하십시오.

여러분의 입력을 기다리는 리뷰 찾기

https://rfcbot.rs/ 를 정기적으로 확인하는 것을 잊지 마십시오. 닉네임이 등장하는 부분을 클릭하면 https://rfcbot.rs/fcp/SimonSapin 과 같은 페이지로 이동하며, 그곳에는 여러분의 입력을 기다리는 리뷰만 표시됩니다.

PR 검토하기

라이브러리 팀의 구성원으로서 여러분은 리뷰가 필요한 풀 리퀘스트에 배정되거나, Rust 프로젝트의 이슈에 대해 의견을 요청받게 됩니다.

RFC는 언제 필요합니까?

새로운 불안정 기능은 병합되기 전에 RFC가 필요하지 않습니다. 기능이 작고 설계 공간이 단순하다면, 안정화는 대개 해당 기능이 FCP를 거치기만 하면 됩니다. 하지만 때로는 안정화 전에 RFC를 요청할 수도 있습니다.

unsafe가 있습니까?

표준 라이브러리 내 unsafe 코드 블록에는 왜 안전한지 설명하는 주석이 필요합니다. 이를 검사하는 tidy 린트가 있습니다. unsafe 코드는 실제로도 문제가 없어야 합니다.

무엇이 sound하고 무엇이 그렇지 않은지에 관한 규칙은 미묘할 수 있습니다. 현재의 견해를 확인하려면 Unsafe Code Guidelines WG를 참고하십시오. 그리고 어떤 의심이라도 든다면 @rust-lang/libs, @rust-lang/lang, 및/또는 WG의 누군가에게 핑을 보내는 것을 고려하십시오. 저희는 unsafe 코드의 건전성에 대해 논의하는 것을 좋아하며, 더 많은 눈이 살펴볼수록 좋습니다!

#[inline]은 올바릅니까?

인라이닝은 잠재적인 실행 속도, 컴파일 시간, 코드 크기 사이의 트레이드오프입니다. 이에 관해서는 hashbrown 크레이트에 대한 이 PR에서 논의가 있었습니다. 그 스레드에서 발췌하면:

#[inline]은 단순한 인라인 힌트와는 매우 다릅니다. 앞서 언급했듯이, C++에는 #[inline]이 하는 일에 대응하는 것이 없습니다. 디버그 모드에서 rustc는 기본적으로 #[inline]을 무시하며, 여러분이 그것을 작성하지 않은 것처럼 취급합니다. 릴리스 모드에서 컴파일러는 기본적으로 #[inline] 함수를 참조하는 모든 코드생성 유닛마다 코드생성을 하며, 여기에 inlinehint도 추가합니다. 이는 만약 16개의 CGU가 있고 이들이 모두 어떤 항목을 참조한다면, 각각 모두에 그 항목의 전체 구현이 인라인된다는 의미입니다.

#[inline]을 추가할 수 있는 경우:

  • 공개되고, 작고, 제네릭이 아닌 함수에 대해.

#[inline]이 필요하지 않아야 하는 경우:

  • 스코프 내에 제네릭이 있는 메서드에 대해.
  • 기본 구현이 없는 트레이트의 메서드에 대해.

#[inline]은 언제든 나중에 도입할 수 있으므로, 의심스럽다면 그냥 제거하면 됩니다.

#[inline(always)]는 어떻습니까?

#[inline(always)]가 필요한 경우는 거의 없어야 합니다. 제한된 소수의 위치에서 사용되는 비공개 헬퍼 메서드나 사소한 연산자에는 유익할 수 있습니다. 마이크로 벤치마크가 해당 속성을 정당화해야 합니다.

잠재적인 breakage가 있습니까?

가능하다면 호환성 저해 변경은 피해야 합니다. RFC 1105는 호환성 저해 변경을 구성하는 요소에 대한 기초를 마련합니다. 호환성 저해는 실제 영향에 따라 허용 가능한지 여부가 판단될 수 있으며, 이는 crater 실행으로 근사할 수 있습니다.

영향의 정도에 따라 호환성 저해를 완화하는 전략들이 존재합니다.

가치가 높고 영향도 역시 높은 변경의 경우:

  • 컴파일러 린트를 사용하여 문제가 있는 동작을 단계적으로 제거하는 것입니다.

영향이 그다지 크지 않다면:

  • 문제가 되는 크레이트의 메인테이너에게 연락하고 이를 수정하는 풀 리퀘스트를 제출하는 것입니다.

동작이 변경되었습니까?

호환성 저해 변경은 컴파일 실패에만 국한되지 않습니다. stable 함수의 동작 변경은 일반적으로 받아들여질 수 없습니다. 예시로 home_dir 이슈를 참고하십시오.

stable 트레이트에 대한 새로운 impl이 있습니까?

표준 라이브러리에 대한 많은 풀 리퀘스트는 이미 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는 인자가 직접 타입 검사를 통과하지 못할 경우 유효한 트레이트 impl을 찾기 위해 deref 강제 변환을 사용합니다. 이는 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>을 기반으로 구현되며, 역참조 강제 변환을 통해 str의 메서드를 사용할 수 있습니다. Vec<T>는 역참조 강제 변환을 통해 [T]의 메서드를 사용할 수 있습니다. 가능한 경우, String과 같은 래핑 타입의 메서드는 기저 저장소나 역참조 대상에 이미 존재하는 메서드에 위임해야 합니다.

#[fundamental] 항목이 관련되어 있습니까?

#[fundamental] 타입은 코히런스 규칙이 다르기 때문에 블랭킷 트레이트 구현을 추가할 수 없습니다. 자세한 내용은 RFC 1023을 참조하십시오. 여기에는 다음이 포함됩니다:

  • &T
  • &mut T
  • Box<T>
  • Pin<T>

특수화가 관련되어 있습니까?

특수화는 현재 불안정합니다. 진행 상황은 여기에서 추적할 수 있습니다.

저희는 특수화에 지나치게 의존하지 않도록 노력하며, 그 사용을 특정 구현의 최적화로 제한합니다. 이러한 특수화된 최적화는 공개 메서드 자체를 특수화하는 대신, 비공개 트레이트를 사용해 올바른 구현을 찾습니다. 외부 호출자에 대한 메서드 디스패치 방식을 바꾸는 특수화의 사용은 신중히 검토되어야 합니다.

표준 라이브러리에서 특수화(specialization)를 사용하는 방법의 예로, &[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) }
    }
}
}

안타깝게도 이 두 구현은 서로 겹치기 때문에 일반적으로는 동시에 가질 수 없었습니다. 이때 비공개 특수화를 사용해 내부적으로 올바른 구현을 선택할 수 있습니다. 이 경우, 구현을 전환하는 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) 것으로 알려져 있습니다.

공개 열거형이 있습니까?

새로운 배리언트가 도입될 가능성이 있다면, 공개 열거형은 #[non_exhaustive] 속성을 가져야 하며, 이를 통해 호환성을 깨지 않고 배리언트를 추가할 수 있습니다.

이 변경이 드롭 순서에 영향을 줍니까?

컬렉션 내부 구조의 변경은 항목이 드롭되는 순서에 영향을 줄 수 있습니다. 이는 과거에도 용인된 바 있지만, 명시해 두어야 합니다.

수동으로 구현된 Drop이 있습니까?

Drop을 수동으로 구현하는 제네릭 Type<T>T#[may_dangle] 속성이 적절한지 검토해야 합니다. 노미콘(Nomicon)#[may_dangle]이 무엇인지에 대한 자세한 내용이 나와 있습니다.

만약 제네릭 Type<T>T를 드롭하는 것도 포함할 수 있는 수동 드롭 구현을 가지고 있다면, dropck는 이를 알아야 합니다. Type<T>T를 소유하는 방식이 ManuallyDrop<T>, *mut T, MaybeUninit<T>처럼 T 자체를 드롭하지 않는 타입으로 표현되어 있다면, Type<T>T가 드롭될 수 있음을 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에 대해 건전성 구멍을 열어, 자신의 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::replacemem::swap

&mut 참조 뒤에 있는 모든 Sized 값은 mem::replacemem::swap을 사용해 새 값으로 교체될 수 있으므로, 코드는 도달 가능한 어떤 가변 참조도 교체를 통해 내부가 바뀌지 않는다고 가정해서는 안 됩니다.

mem::forget

Rust는 값이 누출된 경우(이는 mem::forget으로 할 수 있습니다) 소멸자가 실행될 것을 보장하지 않으므로, 코드는 안전성 유지를 위해 소멸자에 의존하는 것을 피해야 합니다. 기억하십시오, 모두가 실수를 합니다.

값이 누수될 때 소멸자를 실행하지 않아도 괜찮은 이유는 그 저장소가 해제되거나 재사용되지 않기 때문입니다. 저장소가 초기화되어 있고 해제되거나 재사용되고 있다면 소멸자가 먼저 실행되어야 합니다. 왜냐하면 메모리가 고정(pinned)되어 있을 수 있기 때문입니다. 그렇긴 하지만, 고정이 절대 관여하지 않는다는 것을 보장할 수 있다면 해제 시 소멸자를 건너뛰는 예외가 여전히 있을 수 있습니다.

성능에는 어떤 영향이 있습니까?

핫 코드에 대한 변경은 사용자 측 성능에 좋든 나쁘든 영향을 미칠 수 있습니다. 적절한 벤치마크는 성능 특성이 어떻게 변하는지에 대한 아이디어를 제공해야 합니다. rustc 자체에 영향을 미치는 변경의 경우, rust-timer 실행도 할 수 있습니다.

커밋 로그가 깔끔합니까?

PR에는 병합 커밋이 있어서는 안 됩니다. 기본 브랜치와 어긋나 오래되면 리베이스가 필요합니다.

PR 병합하기

rust-lang/rust로의 PR은 GitHub UI를 사용하거나 원격 브랜치를 푸시하여 수동으로 병합되지 않습니다. 모든 것은 bors를 거칩니다.

rollup은 언제 하는가

라이브러리 PR의 경우, 특히 새로운 불안정 추가일 뿐이거나 문서만 다루는 경우라면 롤업해도 대체로 괜찮습니다.

언제 롤업해야 하는지에 대한 자세한 내용은 rollup guidelines를 참고하십시오. 그 취지는 여러 PR을 개별적으로 병합하기보다는 함께 모아서 한 번에 병합하려는 것입니다. 이렇게 하면 병합을 더 빠르게 할 수 있지만, 충돌 가능성이 높거나 롤업 시 가려질 수 있는 성능 특성을 지닌 일부 풀 리퀘스트에는 적합하지 않을 수 있습니다.

새로운 공개 아이템이 있을 때

기능이 새로운 것이라면 이를 위한 추적 이슈를 열어야 합니다. 그곳에 무엇을 넣어야 할지 감을 잡으려면 이전 추적 이슈들을 살펴보십시오. #[unstable] 속성의 issue 필드는 추적 이슈 번호로 갱신되어야 합니다.

불안정 기능은 준비가 되면 bors를 통해 평소대로 병합될 수 있습니다.

새로운 트레이트 구현이 있는 경우

stable 트레이트에 대한 트레이트 impl을 unstable로 만들 방법은 없으므로, 이미 stable인 트레이트에 새로운 impl을 추가하는 모든 풀 리퀘스트는 병합 전 반드시 FCP를 거쳐야 합니다. 다만 트레이트 자체가 unstable이라면, impl도 unstable이어야 합니다.

기능이 안정화되는 경우

기능은 #[unstable] 속성을 #[stable] 속성으로 교체하는 PR을 통해 안정화될 수 있습니다. 안정화 전에 해당 기능은 승인된 RFC를 갖추어야 합니다. 이들 또한 병합 전 FCP를 거쳐야 합니다.

Forge를 확인하면 #[stable] 속성에 사용할 올바른 버전을 알 수 있습니다.

const 함수가 안정화되는 경우

const 함수는 #[rustc_const_unstable] 속성을 #[rustc_const_stable] 속성으로 교체하는 PR을 통해 안정화될 수 있습니다. Constant Evaluation WG에 해당 const성이 우리가 확정하고자 하는 것인지에 대한 의견을 요청해야 합니다. const로 안정화되는 것이 노출되는 intrinsic이라면 @rust-lang/lang 역시 FCP에 포함되어야 합니다.

해당 함수가 #[allow_internal_unstable] 속성을 통해 내부적으로 다른 불안정 const 함수에 의존하는지 확인하고, 내부의 불안정한 호출이 제거된다면 그 함수를 어떻게 구현할 수 있을지 고려하십시오. #[allow_internal_unstable]에 대한 자세한 내용은 Stability attributes 페이지를 참고하십시오.

unsafeconst가 관련된 경우, 예를 들어 “unconst“한 연산의 경우, 해당 사용에 대한 const 안전성 논거 또한 문서화되어야 합니다. 즉, const fn은 추가적인 결정성(예: 런타임/컴파일타임 결과가 일치해야 하고 함수의 출력이 오직 입력에만 의존해야 하는 등)에 대한 제약을 유지해야 하며, unsafe가 사용될 때는 이 점이 논증되어야 합니다.

기능이 폐기되는 경우

폐기된 항목으로 인해 문서에 잡음이 생기는 것을 줄이기 위해, 이러한 항목은 문서 페이지의 하단에 렌더링되도록 모듈이나 impl 블록의 맨 아래로 옮겨야 합니다. 그런 다음 문서는 어떻게 사용하는지가 아니라 왜 해당 항목이 폐기되었는지에 초점을 맞추도록 간추려야 합니다.