Unsafe Rust 의 슈퍼파워 탐구

Beginner

This tutorial is from open-source community. Access the source code

소개

Unsafe Rust에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 컴파일 시간에 강제되는 메모리 안전 보장을 우회하고 추가적인 능력을 부여하는 기능인 unsafe Rust 를 살펴보고, 이를 사용하는 데 따른 위험과 책임을 이해할 것입니다.

Unsafe Rust

지금까지 논의한 모든 코드는 컴파일 시간에 Rust 의 메모리 안전 보장이 적용되었습니다. 그러나 Rust 에는 이러한 메모리 안전 보장을 적용하지 않는 두 번째 언어가 숨겨져 있습니다. 이를 unsafe Rust라고 하며, 일반 Rust 와 동일하게 작동하지만 추가적인 능력을 제공합니다.

Unsafe Rust 가 존재하는 이유는 본질적으로 정적 분석이 보수적이기 때문입니다. 컴파일러가 코드가 보장을 준수하는지 여부를 결정하려고 할 때, 일부 유효한 프로그램을 거부하는 것이 일부 유효하지 않은 프로그램을 허용하는 것보다 낫습니다. 코드가 괜찮을 있지만, Rust 컴파일러가 확신할 수 있을 만큼 충분한 정보를 가지고 있지 않다면 코드를 거부합니다. 이러한 경우, unsafe 코드를 사용하여 컴파일러에게 "저를 믿으세요, 제가 무엇을 하는지 알고 있습니다."라고 말할 수 있습니다. 하지만 unsafe Rust 를 사용할 때는 위험을 감수해야 합니다. unsafe 코드를 잘못 사용하면 널 포인터 역참조와 같은 메모리 안전성 문제로 인해 문제가 발생할 수 있습니다.

Rust 에 unsafe alter ego 가 있는 또 다른 이유는 기본 컴퓨터 하드웨어가 본질적으로 unsafe 하기 때문입니다. Rust 가 unsafe 작업을 허용하지 않으면 특정 작업을 수행할 수 없습니다. Rust 는 운영 체제와 직접 상호 작용하거나 자체 운영 체제를 작성하는 것과 같은 로우 레벨 시스템 프로그래밍을 허용해야 합니다. 로우 레벨 시스템 프로그래밍 작업은 언어의 목표 중 하나입니다. unsafe Rust 로 무엇을 할 수 있는지, 그리고 어떻게 할 수 있는지 살펴보겠습니다.

Unsafe Superpowers

Unsafe Rust 로 전환하려면 unsafe 키워드를 사용한 다음 unsafe 코드를 포함하는 새 블록을 시작합니다. 안전한 Rust 에서는 할 수 없는 다섯 가지 작업을 unsafe Rust 에서 수행할 수 있으며, 이를 unsafe superpowers라고 부릅니다. 이러한 superpowers 에는 다음 기능이 포함됩니다.

  1. Raw 포인터 역참조
  2. Unsafe 함수 또는 메서드 호출
  3. 가변 static 변수 접근 또는 수정
  4. Unsafe trait 구현
  5. union의 필드 접근

unsafe가 borrow checker 를 끄거나 Rust 의 다른 안전 검사를 비활성화하지 않는다는 점을 이해하는 것이 중요합니다. unsafe 코드에서 참조를 사용하면 여전히 검사됩니다. unsafe 키워드는 메모리 안전성을 위해 컴파일러가 검사하지 않는 이 다섯 가지 기능에 대한 접근 권한만 제공합니다. unsafe 블록 내에서도 어느 정도의 안전성을 확보할 수 있습니다.

또한 unsafe는 블록 내부의 코드가 반드시 위험하거나 메모리 안전성 문제가 발생할 것이라는 의미는 아닙니다. 프로그래머로서 unsafe 블록 내부의 코드가 유효한 방식으로 메모리에 접근하도록 보장하는 것이 의도입니다.

사람은 실수를 할 수 있으며, 실수가 발생할 수 있지만, 이 다섯 가지 unsafe 작업을 unsafe로 주석 처리된 블록 내부에 두도록 요구함으로써 메모리 안전성과 관련된 모든 오류가 unsafe 블록 내에 있어야 함을 알 수 있습니다. unsafe 블록을 작게 유지하십시오. 메모리 버그를 조사할 때 나중에 감사하게 될 것입니다.

unsafe 코드를 최대한 격리하기 위해, 이러한 코드를 안전한 추상화 내에 캡슐화하고 안전한 API 를 제공하는 것이 가장 좋습니다. 이는 이 챕터 뒷부분에서 unsafe 함수와 메서드를 살펴볼 때 논의할 것입니다. 표준 라이브러리의 일부는 감사된 unsafe 코드에 대한 안전한 추상화로 구현됩니다. unsafe 코드를 안전한 추상화로 래핑하면 unsafe 사용이 사용자가 unsafe 코드로 구현된 기능을 사용하려는 모든 곳으로 유출되는 것을 방지할 수 있습니다. 안전한 추상화를 사용하는 것은 안전하기 때문입니다.

다섯 가지 unsafe superpowers 각각을 차례로 살펴보겠습니다. 또한 unsafe 코드에 대한 안전한 인터페이스를 제공하는 몇 가지 추상화도 살펴보겠습니다.

Raw 포인터 역참조

"Dangling References"에서 컴파일러가 참조가 항상 유효하도록 보장한다고 언급했습니다. Unsafe Rust 에는 참조와 유사한 raw 포인터라는 두 가지 새로운 유형이 있습니다. 참조와 마찬가지로 raw 포인터는 불변 또는 가변일 수 있으며 각각 *const T*mut T로 작성됩니다. 별표는 역참조 연산자가 아닙니다. 이는 유형 이름의 일부입니다. Raw 포인터의 맥락에서 *불변 (immutable)*은 역참조된 후 포인터에 직접 할당할 수 없음을 의미합니다.

참조 및 스마트 포인터와 달리 raw 포인터는 다음과 같습니다.

  • 불변 및 가변 포인터 또는 동일한 위치에 대한 여러 가변 포인터를 가짐으로써 borrow 규칙을 무시할 수 있습니다.
  • 유효한 메모리를 가리킨다는 보장이 없습니다.
  • null 일 수 있습니다.
  • 자동 정리를 구현하지 않습니다.

Rust 가 이러한 보장을 적용하지 않도록 선택함으로써, 더 나은 성능을 얻거나 Rust 의 보장이 적용되지 않는 다른 언어 또는 하드웨어와 인터페이스할 수 있는 능력을 얻기 위해 보장된 안전성을 포기할 수 있습니다.

Listing 19-1 은 참조에서 불변 및 가변 raw 포인터를 만드는 방법을 보여줍니다.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

Listing 19-1: 참조에서 raw 포인터 생성

이 코드에는 unsafe 키워드를 포함하지 않는다는 점에 유의하십시오. 안전한 코드에서 raw 포인터를 만들 수 있습니다. 잠시 후에 보게 되겠지만, unsafe 블록 외부에서는 raw 포인터를 역참조할 수 없습니다.

as를 사용하여 불변 및 가변 참조를 해당 raw 포인터 유형으로 캐스팅하여 raw 포인터를 만들었습니다. 유효하다고 보장된 참조에서 직접 생성했기 때문에 이러한 특정 raw 포인터가 유효하다는 것을 알고 있지만, 임의의 raw 포인터에 대해 그러한 가정을 할 수는 없습니다.

이를 설명하기 위해 다음으로 유효성을 확신할 수 없는 raw 포인터를 만들 것입니다. Listing 19-2 는 메모리의 임의 위치에 대한 raw 포인터를 만드는 방법을 보여줍니다. 임의의 메모리를 사용하려고 하면 정의되지 않은 동작이 발생합니다. 해당 주소에 데이터가 있을 수도 있고 없을 수도 있으며, 컴파일러가 코드를 최적화하여 메모리 접근이 없도록 하거나, 프로그램이 세그먼테이션 오류로 종료될 수 있습니다. 일반적으로 이와 같은 코드를 작성할 좋은 이유는 없지만, 가능합니다.

let address = 0x012345usize;
let r = address as *const i32;

Listing 19-2: 임의의 메모리 주소에 대한 raw 포인터 생성

안전한 코드에서 raw 포인터를 만들 수 있지만, raw 포인터를 역참조하여 가리키는 데이터를 읽을 수는 없다는 것을 기억하십시오. Listing 19-3 에서 unsafe 블록이 필요한 raw 포인터에 역참조 연산자 *를 사용합니다.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

Listing 19-3: unsafe 블록 내에서 raw 포인터 역참조

포인터를 생성하는 것은 해가 없습니다. 가리키는 값에 접근하려고 할 때만 유효하지 않은 값을 처리하게 될 수 있습니다.

또한 Listing 19-1 및 19-3 에서 num이 저장된 동일한 메모리 위치를 가리키는 *const i32*mut i32 raw 포인터를 모두 만들었습니다. 대신 num에 대한 불변 및 가변 참조를 만들려고 하면 Rust 의 소유권 규칙에 따라 불변 참조와 동시에 가변 참조를 허용하지 않으므로 코드가 컴파일되지 않습니다. Raw 포인터를 사용하면 동일한 위치에 대한 가변 포인터와 불변 포인터를 생성하고 가변 포인터를 통해 데이터를 변경하여 데이터 경합 (data race) 을 잠재적으로 생성할 수 있습니다. 주의하십시오!

이러한 모든 위험이 있는데, 왜 raw 포인터를 사용해야 할까요? 주요 사용 사례 중 하나는 "Calling an Unsafe Function or Method"에서 보게 될 C 코드와 인터페이스할 때입니다. 또 다른 경우는 borrow checker 가 이해하지 못하는 안전한 추상화를 구축할 때입니다. unsafe 함수를 소개한 다음 unsafe 코드를 사용하는 안전한 추상화의 예를 살펴보겠습니다.

Unsafe 함수 또는 메서드 호출

unsafe 블록에서 수행할 수 있는 두 번째 유형의 작업은 unsafe 함수를 호출하는 것입니다. Unsafe 함수와 메서드는 일반 함수 및 메서드와 정확히 동일하게 보이지만 정의의 나머지 부분 앞에 추가적인 unsafe가 있습니다. 이 컨텍스트에서 unsafe 키워드는 Rust 가 이러한 요구 사항을 충족했는지 보장할 수 없기 때문에 이 함수를 호출할 때 준수해야 하는 요구 사항이 함수에 있음을 나타냅니다. unsafe 블록 내에서 unsafe 함수를 호출함으로써, 우리는 이 함수의 문서를 읽었고 함수의 계약을 준수할 책임을 진다고 말하는 것입니다.

다음은 본문에서 아무것도 하지 않는 dangerous라는 unsafe 함수입니다.

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

별도의 unsafe 블록 내에서 dangerous 함수를 호출해야 합니다. unsafe 블록 없이 dangerous를 호출하려고 하면 오류가 발생합니다.

error[E0133]: call to unsafe function is unsafe and requires
unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on
how to avoid undefined behavior

unsafe 블록을 사용하면 Rust 에 함수의 문서를 읽었고, 이를 적절하게 사용하는 방법을 이해했으며, 함수의 계약을 이행하고 있음을 확인했다고 주장하는 것입니다.

Unsafe 함수의 본문은 효과적으로 unsafe 블록이므로, unsafe 함수 내에서 다른 unsafe 작업을 수행하기 위해 다른 unsafe 블록을 추가할 필요가 없습니다.

Unsafe 코드를 통한 안전한 추상화 생성

함수에 unsafe 코드가 포함되어 있다고 해서 전체 함수를 unsafe 로 표시해야 하는 것은 아닙니다. 실제로 unsafe 코드를 안전한 함수로 래핑하는 것은 일반적인 추상화입니다. 예를 들어, unsafe 코드가 필요한 표준 라이브러리의 split_at_mut 함수를 살펴보겠습니다. 이를 구현하는 방법을 살펴보겠습니다. 이 안전한 메서드는 가변 슬라이스에 정의되어 있습니다. 하나의 슬라이스를 가져와 인수로 제공된 인덱스에서 슬라이스를 분할하여 두 개로 만듭니다. Listing 19-4 는 split_at_mut을 사용하는 방법을 보여줍니다.

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);

Listing 19-4: 안전한 split_at_mut 함수 사용

안전한 Rust 만 사용하여 이 함수를 구현할 수 없습니다. 시도는 Listing 19-5 와 비슷할 수 있으며, 이는 컴파일되지 않습니다. 단순성을 위해 split_at_mut을 메서드가 아닌 함수로 구현하고 제네릭 타입 T가 아닌 i32 값의 슬라이스에 대해서만 구현합니다.

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

Listing 19-5: 안전한 Rust 만 사용하여 split_at_mut을 구현하려는 시도

이 함수는 먼저 슬라이스의 총 길이를 가져옵니다. 그런 다음 매개변수로 제공된 인덱스가 슬라이스 내에 있는지, 즉 길이가 작거나 같은지 확인하여 어설션합니다. 어설션은 슬라이스를 분할할 길이보다 큰 인덱스를 전달하면 함수가 해당 인덱스를 사용하려고 시도하기 전에 패닉을 일으킨다는 것을 의미합니다.

그런 다음 튜플에서 두 개의 가변 슬라이스를 반환합니다. 하나는 원래 슬라이스의 시작 부분에서 mid 인덱스까지, 다른 하나는 mid에서 슬라이스의 끝까지입니다.

Listing 19-5 의 코드를 컴파일하려고 하면 오류가 발생합니다.

error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:9:31
  |
2 |     values: &mut [i32],
  |             - let's call the lifetime of this reference `'1`
...
9 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`

Rust 의 borrow checker 는 슬라이스의 다른 부분을 빌리고 있다는 것을 이해할 수 없습니다. 동일한 슬라이스에서 두 번 빌리고 있다는 것만 알고 있습니다. 슬라이스의 다른 부분을 빌리는 것은 근본적으로 괜찮습니다. 두 슬라이스가 겹치지 않기 때문이지만, Rust 는 이를 알 만큼 똑똑하지 않습니다. 코드가 괜찮다는 것을 알고 있지만 Rust 가 그렇지 않은 경우, unsafe 코드를 사용해야 할 때입니다.

Listing 19-6 은 unsafe 블록, raw 포인터 및 일부 unsafe 함수 호출을 사용하여 split_at_mut의 구현이 작동하도록 하는 방법을 보여줍니다.

use std::slice;

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
  1 let len = values.len();
  2 let ptr = values.as_mut_ptr();

  3 assert!(mid <= len);

  4 unsafe {
        (
          5 slice::from_raw_parts_mut(ptr, mid),
          6 slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

Listing 19-6: split_at_mut 함수 구현에서 unsafe 코드 사용

"The Slice Type"에서 슬라이스는 일부 데이터에 대한 포인터와 슬라이스의 길이라는 것을 기억하십시오. len 메서드를 사용하여 슬라이스의 길이를 얻고 [1] as_mut_ptr 메서드를 사용하여 슬라이스의 raw 포인터에 접근합니다 [2]. 이 경우, i32 값에 대한 가변 슬라이스가 있으므로 as_mut_ptr*mut i32 유형의 raw 포인터를 반환하며, 이를 변수 ptr에 저장했습니다.

mid 인덱스가 슬라이스 내에 있다는 어설션을 유지합니다 [3]. 그런 다음 unsafe 코드 [4]로 이동합니다. slice::from_raw_parts_mut 함수는 raw 포인터와 길이를 가져와 슬라이스를 생성합니다. 이를 사용하여 ptr에서 시작하고 길이가 mid인 슬라이스를 만듭니다 [5]. 그런 다음 ptr에서 mid를 인수로 사용하여 add 메서드를 호출하여 mid에서 시작하는 raw 포인터를 얻고, 해당 포인터와 mid 이후의 나머지 항목 수를 길이로 사용하여 슬라이스를 만듭니다 [6].

slice::from_raw_parts_mut 함수는 raw 포인터를 가져와 이 포인터가 유효하다고 신뢰해야 하므로 unsafe 입니다. raw 포인터의 add 메서드도 오프셋 위치가 유효한 포인터라고 신뢰해야 하므로 unsafe 입니다. 따라서 slice::from_raw_parts_mutadd에 대한 호출을 둘러싸는 unsafe 블록을 배치하여 호출할 수 있었습니다. 코드를 살펴보고 midlen보다 작거나 같아야 한다는 어설션을 추가함으로써, unsafe 블록 내에서 사용된 모든 raw 포인터가 슬라이스 내의 데이터에 대한 유효한 포인터임을 알 수 있습니다. 이것은 unsafe의 적절하고 적절한 사용입니다.

결과 split_at_mut 함수를 unsafe로 표시할 필요가 없으며 안전한 Rust 에서 이 함수를 호출할 수 있다는 점에 유의하십시오. 이 함수가 접근할 수 있는 데이터에서 유효한 포인터만 생성하기 때문에, 안전한 방식으로 unsafe 코드를 사용하는 함수의 구현과 함께 unsafe 코드에 대한 안전한 추상화를 만들었습니다.

반대로, Listing 19-7 에서 slice::from_raw_parts_mut을 사용하면 슬라이스가 사용될 때 충돌이 발생할 수 있습니다. 이 코드는 임의의 메모리 위치를 가져와 길이가 10,000 개 항목인 슬라이스를 만듭니다.

use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let values: &[i32] = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};

Listing 19-7: 임의의 메모리 위치에서 슬라이스 생성

이 임의의 위치에 있는 메모리를 소유하지 않으며, 이 코드가 생성하는 슬라이스에 유효한 i32 값이 포함되어 있다는 보장이 없습니다. values를 유효한 슬라이스인 것처럼 사용하려고 하면 정의되지 않은 동작이 발생합니다.

extern 함수를 사용하여 외부 코드 호출

때로는 Rust 코드가 다른 언어로 작성된 코드와 상호 작용해야 할 수 있습니다. 이를 위해 Rust 에는 *Foreign Function Interface (FFI, 외부 함수 인터페이스)*를 생성하고 사용하는 것을 용이하게 하는 extern 키워드가 있습니다. FFI 는 프로그래밍 언어가 함수를 정의하고 다른 (외부) 프로그래밍 언어가 해당 함수를 호출할 수 있도록 하는 방법입니다.

Listing 19-8 은 C 표준 라이브러리의 abs 함수와의 통합을 설정하는 방법을 보여줍니다. extern 블록 내에서 선언된 함수는 Rust 코드에서 호출하기 항상 unsafe 합니다. 그 이유는 다른 언어는 Rust 의 규칙과 보장을 적용하지 않으며 Rust 는 이를 확인할 수 없으므로 안전을 보장하는 책임은 프로그래머에게 있기 때문입니다.

Filename: src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!(
            "Absolute value of -3 according to C: {}",
            abs(-3)
        );
    }
}

Listing 19-8: 다른 언어로 정의된 extern 함수 선언 및 호출

extern "C" 블록 내에서 호출하려는 다른 언어의 외부 함수의 이름과 시그니처를 나열합니다. "C" 부분은 외부 함수가 사용하는 *application binary interface (ABI, 응용 프로그램 바이너리 인터페이스)*를 정의합니다. ABI 는 어셈블리 수준에서 함수를 호출하는 방법을 정의합니다. "C" ABI 는 가장 일반적이며 C 프로그래밍 언어의 ABI 를 따릅니다.

다른 언어에서 Rust 함수 호출

또한 extern을 사용하여 다른 언어가 Rust 함수를 호출할 수 있도록 하는 인터페이스를 만들 수 있습니다. 전체 extern 블록을 생성하는 대신, extern 키워드를 추가하고 관련 함수의 fn 키워드 바로 앞에 사용할 ABI 를 지정합니다. 또한 Rust 컴파일러에게 이 함수의 이름을 망글링하지 않도록 지시하기 위해 #[no_mangle] 어노테이션을 추가해야 합니다. *Mangling (맹글링)*은 컴파일러가 함수에 부여한 이름을 다른 이름으로 변경하여 컴파일 프로세스의 다른 부분에서 사용할 수 있도록 더 많은 정보를 포함하지만 사람이 읽기 어렵게 만드는 것입니다. 모든 프로그래밍 언어 컴파일러는 이름을 약간 다르게 맹글링하므로 다른 언어에서 Rust 함수를 명명할 수 있도록 하려면 Rust 컴파일러의 이름 맹글링을 비활성화해야 합니다.

다음 예제에서는 C 코드로 컴파일되어 C 에서 링크된 후 call_from_c 함수를 C 코드에서 접근할 수 있도록 합니다.

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

extern 사용법은 unsafe를 필요로 하지 않습니다.

가변 정적 변수 접근 또는 수정

이 책에서는 아직 전역 변수에 대해 이야기하지 않았습니다. Rust 는 전역 변수를 지원하지만 Rust 의 소유권 규칙과 관련하여 문제가 발생할 수 있습니다. 두 스레드가 동일한 가변 전역 변수에 접근하는 경우 데이터 경합이 발생할 수 있습니다.

Rust 에서 전역 변수는 static (정적) 변수라고 합니다. Listing 19-9 는 문자열 슬라이스를 값으로 사용하는 정적 변수의 선언 및 사용 예제를 보여줍니다.

Filename: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}

Listing 19-9: 불변 정적 변수 정의 및 사용

정적 변수는 "상수"에서 논의한 상수와 유사합니다. 정적 변수의 이름은 관례적으로 SCREAMING_SNAKE_CASE로 표기합니다. 정적 변수는 'static 수명을 가진 참조만 저장할 수 있으며, 이는 Rust 컴파일러가 수명을 파악할 수 있고 명시적으로 주석을 달 필요가 없음을 의미합니다. 불변 정적 변수에 접근하는 것은 안전합니다.

상수와 불변 정적 변수의 미묘한 차이점은 정적 변수의 값은 메모리에 고정된 주소를 갖는다는 것입니다. 값을 사용하면 항상 동일한 데이터에 접근합니다. 반면에 상수는 사용될 때마다 데이터를 복제할 수 있습니다. 또 다른 차이점은 정적 변수가 가변적일 수 있다는 것입니다. 가변 정적 변수에 접근하고 수정하는 것은 *unsafe (안전하지 않음)*입니다. Listing 19-10 은 COUNTER라는 가변 정적 변수를 선언, 접근 및 수정하는 방법을 보여줍니다.

Filename: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {COUNTER}");
    }
}

Listing 19-10: 가변 정적 변수를 읽거나 쓰는 것은 unsafe 합니다.

일반 변수와 마찬가지로 mut 키워드를 사용하여 가변성을 지정합니다. COUNTER에서 읽거나 쓰는 모든 코드는 unsafe 블록 내에 있어야 합니다. 이 코드는 단일 스레드이므로 예상대로 컴파일되고 COUNTER: 3을 출력합니다. 여러 스레드가 COUNTER에 접근하면 데이터 경합이 발생할 수 있습니다.

전역적으로 접근 가능한 가변 데이터의 경우 데이터 경합이 없는지 확인하기 어렵기 때문에 Rust 는 가변 정적 변수를 unsafe 로 간주합니다. 가능한 경우, 컴파일러가 다른 스레드에서 데이터 접근이 안전하게 수행되는지 확인하도록 16 장에서 논의한 동시성 기술과 스레드 안전 스마트 포인터를 사용하는 것이 좋습니다.

Unsafe 트레이트 구현

unsafe를 사용하여 unsafe 트레이트를 구현할 수 있습니다. 트레이트는 컴파일러가 확인할 수 없는 불변성을 하나 이상 가진 메서드가 있는 경우 unsafe 합니다. Listing 19-11 에 표시된 것처럼 trait 앞에 unsafe 키워드를 추가하고 트레이트의 구현을 unsafe로 표시하여 트레이트가 unsafe임을 선언합니다.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

Listing 19-11: Unsafe 트레이트 정의 및 구현

unsafe impl을 사용함으로써 컴파일러가 확인할 수 없는 불변성을 유지하겠다고 약속하는 것입니다.

예를 들어, "Send 및 Sync 트레이트를 사용한 확장 가능한 동시성"에서 논의한 SendSync 마커 트레이트를 기억하십시오. 컴파일러는 우리의 타입이 완전히 SendSync 타입으로 구성된 경우 이러한 트레이트를 자동으로 구현합니다. 원시 포인터와 같이 Send 또는 Sync가 아닌 타입을 포함하는 타입을 구현하고 해당 타입을 Send 또는 Sync로 표시하려는 경우 unsafe를 사용해야 합니다. Rust 는 우리의 타입이 스레드 간에 안전하게 전송되거나 여러 스레드에서 접근될 수 있다는 보장을 유지하는지 확인할 수 없습니다. 따라서 이러한 검사를 수동으로 수행하고 unsafe로 표시해야 합니다.

Union (공용체) 의 필드 접근

unsafe에서만 작동하는 마지막 작업은 union (공용체) 의 필드에 접근하는 것입니다. unionstruct와 유사하지만 특정 인스턴스에서는 선언된 필드 중 하나만 한 번에 사용됩니다. Union 은 주로 C 코드의 union 과 인터페이스하기 위해 사용됩니다. Union 필드에 접근하는 것은 unsafe 합니다. Rust 는 현재 union 인스턴스에 저장된 데이터의 타입을 보장할 수 없기 때문입니다. Rust Reference 에서 union 에 대해 자세히 알아볼 수 있습니다: *https://doc.rust-lang.org/reference/items/unions.html\*\*.\*

Unsafe 코드 사용 시점

방금 논의한 다섯 가지 슈퍼파워 중 하나를 사용하기 위해 unsafe를 사용하는 것이 잘못된 것은 아니며, 심지어 비난받을 일도 아닙니다. 하지만 컴파일러가 메모리 안전성을 유지하는 데 도움을 줄 수 없기 때문에 unsafe 코드를 올바르게 작성하는 것은 더 까다롭습니다. unsafe 코드를 사용해야 할 이유가 있다면 그렇게 할 수 있으며, 명시적인 unsafe 주석은 문제가 발생했을 때 문제의 근원을 추적하기 더 쉽게 만들어줍니다.

요약

축하합니다! Unsafe Rust 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.