안전하지 않은 러스트
지금까지 살펴본 모든 코드에는 컴파일 타임에 러스트의 메모리 안전 보장이 적용되었습니다. 그러나 러스트에는 이러한 메모리 안전 보장을 적용하지 않는 두 번째 언어가 숨겨져 있습니다: 이 언어는 안전하지 않은 러스트 (unsafe Rust) 라고 불리며 일반 러스트와 똑같이 작동하지만 추가 슈퍼파워를 제공합니다.
정적 분석은 본질적으로 보수적이기 때문에 안전하지 않은 러스트가 존재합니다. 컴파일러가 코드가 보증을 준수하는지 여부를 판단하려고 할 때, 일부 유효하지 않은 프로그램을 허용하는 것보다 일부 유효한 프로그램을 거부하는 것이 더 낫습니다. 코드가 아마도 괜찮을 수 있겠지만, 러스트 컴파일러는 확신할 수 있는 정보가 충분하지 않다면 코드를 거부할 것입니다. 이러한 경우, 안전하지 않은 코드를 사용하여 컴파일러에게 ‘날 믿어, 내가 뭘 하고 있는지 알고 있어’라고 말할 수 있습니다. 하지만, 안전하지 않은 러스트를 사용하는 것은 사용자의 책임하에 사용해야 한다는 점에 유의하시기를 바랍니다: 안전하지 않은 코드를 잘못 사용하면, 메모리 불안정성으로 인하여 널 포인터 역참조와 같은 문제가 발생할 수 있습니다.
러스트가 안전하지 않은 분신을 가진 또 다른 이유는 밑바탕이 되는 컴퓨터 하드웨어가 본질적으로 안전하지 않기 때문입니다. 러스트가 안전하지 않은 작업을 허용하지 않으면, 특정 작업을 수행할 수 없습니다. 러스트는 운영 체제와 직접 상호 작용하거나 자체 운영 체제를 작성하는 등의 저수준 시스템 프로그래밍을 할 수 있도록 허용해야 합니다. 저수준 시스템 프로그래밍 작업은 이 언어의 목표 중 하나입니다. 안전하지 않은 러스트로 할 수 있는 작업과 그 방법을 살펴봅시다.
안전하지 않은 슈퍼파워
안전하지 않은 러스트로 전환하려면 unsafe
키워드를 사용한 다음 새 블록을
시작하여 안전하지 않은 코드를 집어넣으세요. 안전하지 않은 러스트에서는
안전하지 않은 슈퍼파워라고 부르는 다섯 가지 작업을 수행할 수 있습니다.
이러한 슈퍼파워에는 다음과 같은 기능이 포함됩니다:
- 원시 포인터 (raw pointer) 역참조하기
- 안전하지 않은 함수 혹은 메서드 호출하기
- 가변 정적 변수에 접근하기 및 수정하기
- 안전하지 않은 트레이트 구현하기
union
의 필드 접근하기
unsafe
가 대여 검사기를 끄거나 러스트의 다른 안전성 검사를 비활성화하지
않는다는 점을 이해하는 것이 중요합니다: 안전하지 않은 코드에서 참조를 사용하면,
검사는 여전히 이루어집니다. unsafe
키워드는 컴파일러가 메모리 안전성을 검사하지
않는 위의 다섯 가지 기능 허용만 제공할 뿐입니다. 안전하지 않은 블록 내부에서도
여전히 어느 정도의 안전성을 확보할 수 있습니다.
더불어 unsafe
라는 것은 블록 내부의 코드가 반드시 위험하거나
메모리 안전에 문제가 있다는 것을 의미하지 않습니다: 그 의도는
unsafe
블록 내부의 코드가 유효한 방식으로 메모리에 접근하도록
프로그래머가 보장해야 한다는 것입니다.
사람은 누구나 실수를 할 수 있고 실수는 일어나기 마련이지만, 이 다섯 가지
안전하지 않은 연산을 unsafe
로 주석 처리된 블록 안에 넣도록 하면
메모리 안전과 관련된 모든 에러는 unsafe
블록 안에 있을 수밖에
없음을 알 수 있습니다. unsafe
블록을 작게 유지하세요; 나중에 메모리
버그를 조사할 때 유용하게 사용할 수 있습니다.
안전하지 않은 코드를 최대한 분리하려면 안전하지 않은 코드를 안전한
추상화 안에 넣고 안전한 API를 제공하는 것이 가장 좋으며, 이는 이 장의
뒷부분에서 안전하지 않은 함수와 메서드를 살펴볼 때 설명할 것입니다.
표준 라이브러리의 일부는 감사를 거친 안전하지 않은 코드 위에 안전한
추상화로 구현되어 있습니다. 안전하지 않은 코드를 안전 추상화로 감싸면
unsafe
코드가 구현된 기능을 사용하려는 모든 곳에서 unsafe
라고
쓰는 것을 방지할 수 있는데, 이는 안전 추상화를 사용하면 안전하기
때문입니다.
다섯 개의 안전하지 않은 슈퍼파워를 차례대로 살펴봅시다. 또한 안전하지 않은 코드에 안전한 인터페이스를 제공하는 추상화도 일부 살펴보겠습니다.
원시 포인터 역참조하기
4장의 ‘댕글링 참조’절에서
컴파일러가 참조가 항상 유효하다는 것을 보장한다고 언급했습니다.
안전하지 않은 러스트에는 참조와 유사한 원시 포인터 (raw pointer) 라는
두 가지 새로운 타입이 있습니다. 참조자와 마찬가지로 원시 포인터는
불변 또는 가변이며 각각 *const T
와 *mut T
로 작성됩니다.
별표는 역참조 연산자가 아니라 타입 이름의 일부입니다. 원시 포인터의
맥락에서 불변이란 포인터가 역참조된 후에 직접 할당할 수 없음을
의미합니다.
참조자와 스마트 포인터와는 다르게 원시 포인터는 다음과 같은 특징이 있습니다:
- 원시 포인터는 대여 규칙을 무시할 수 있으며, 같은 위치에 대해 불변과 가변 포인터를 동시에 가질 수 있거나 여러 개의 가변 포인터를 가질 수 있습니다.
- 원시 포인터는 유효한 메모리를 가리키는 것을 보장받지 못합니다.
- 원시 포인터는 널 (null) 이 될 수 있습니다.
- 원시 포인터는 자동 메모리 정리를 구현하지 않습니다.
러스트가 이러한 보증을 적용하지 않도록 선택하면, 이러한 보장된 안전성을 포기하는 대신 러스트의 보증이 적용되지 않는 다른 언어 또는 하드웨어와 인터페이싱할 수 있는 기능이나 더 나은 성능을 얻을 수 있습니다.
예제 19-1은 참조자로부터 불변과 가변 원시 포인터를 만드는 방법을 보여줍니다.
fn main() { let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; }
이 코드에 unsafe
키워드를 포함시키지 않았음을 주목하세요. 원시 포인터는
안전한 코드에서 생성될 수 있습니다; 잠시 후 보게 될 것처럼, 그저 안전하지
않은 블록 밖에서 원시 포인터를 역참조하는 것이 불가능할 뿐입니다.
as
를 사용하여 불변 참조자와 가변 참조자를 해당 원시 포인터
타입으로 캐스팅하여 원시 포인터를 생성했습니다. 유효성이 보장된
참조자로부터 직접 생성했기 때문에 이러한 특정 원시 포인터가
유효하다는 것을 알지만, 모든 원시 포인터에 대해 이러한 가정을
할 수는 없습니다.
이를 증명하기 위해 다음으로 유효성을 확신할 수 없는 원시 포인터를 생성해 보겠습니다. 예제 19-2는 메모리의 임의 위치에 대한 원시 포인터를 생성하는 방법을 보여줍니다. 임의의 메모리를 사용하려고 하면 해당 주소에 데이터가 있을 수도 있고 없을 수도 있으며, 컴파일러가 코드를 최적화하여 메모리 접근이 없도록 할 수도 있고, 세그먼트 에러로 인해 프로그램에서 에러가 발생할 수도 있습니다. 일반적으로 이런 코드를 작성할 좋은 이유는 없지만, 가능은 합니다.
fn main() { let address = 0x012345usize; let r = address as *const i32; }
안전한 코드에서 원시 포인터를 생성할 수는 있지만, 원시 포인터를 역참조하여
가리키는 데이터를 읽을 수는 없다는 점을 상기하세요. 예제 19-3에서는 원시 포인터에
unsafe
블록이 필요한 역참조 연산자 *
를 사용합니다.
fn main() { 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); } }
포인터를 생성하는 것은 아무런 해를 끼치지 않습니다; 포인터가 가리키는 값에 접근하려고 할 때 유효하지 않은 값을 처리해야 할 수도 있는 경우가 문제를 일으키는 것입니다.
또한 예제 19-1과 19-3에서는 *const i32
와 *mut i32
원시 포인터를
생성했는데, 이 두 포인터는 모두 num
이 저장된 동일한 메모리 위치를
가리키는 것을 주의하세요. 대신 num
에 대한 불변 참조자와 가변 참조자를
생성하려고 시도했다면 코드가 컴파일되지 않았을 것인데, 이는 러스트의 소유권
규칙에 따르면 가변 참조자와 불변 참조자를 동시에 허용하지 않기 때문입니다.
원시 포인터를 사용하면 같은 위치에 대한 가변 포인터와 불변 포인터를 생성하고
가변 포인터를 통해 데이터를 변경하여 잠재적으로 데이터 경합을 일으킬 수
있습니다. 조심하세요!
이런 위험성이 있는데도 왜 원시 포인터를 사용하게 될까요? 한 가지 주요 사용 사례는 다음 절 ‘안전하지 않은 함수 또는 메서드 호출하기’ 에서 볼 수 있듯이 C 코드와 상호작용할 때입니다. 또 다른 경우는 대여 검사기가 이해하지 못하는 안전한 추상화를 구축할 때입니다. 안전하지 않은 함수를 소개한 다음 안전하지 않은 코드를 사용하는 안전한 추상화의 예를 살펴보겠습니다.
안전하지 않은 함수 또는 메서드 호출하기
안전하지 않은 블록에서 수행할 수 있는 두 번째 유형의 작업은
안전하지 않은 함수를 호출하는 것입니다. 안전하지 않은 함수와
메서드는 일반 함수나 메서드와 똑같아 보이지만, 정의 앞부분에
unsafe
가 추가됩니다. 이 컨텍스트에서 unsafe
키워드는 이 함수를
호출할 때 지켜야 할 요구사항이 있음을 나타내는데, 이는 러스트가
이러한 요구사항을 충족했다고 보장할 수 없기 때문입니다. 안전하지
않은 함수를 unsafe
블록 내에서 호출한다는 것은 이 함수의 문서를
읽었으며 함수의 계약서를 준수할 책임이 있음을 의미합니다.
아래는 본문에서 아무 일도 하지 않는 dangerous
라는 이름의 안전하지 않은
함수입니다:
fn main() { unsafe fn dangerous() {} unsafe { dangerous(); } }
dangerous
함수는 반드시 분리된 unsafe
블록 내에서 호출되어야 합니다. unsafe
블록 없이 dangerous
를 호출하려고 시도하면 다음과 같은 에러가 발생합니다:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
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
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` due to previous error
unsafe
블록을 사용하는 것은 해당 함수의 설명서를 읽었고, 해당
함수를 올바르게 사용하는 방법을 이해했으며, 해당 함수의 계약서를
이행하고 있음을 확인한다고 러스트에게 단언하는 꼴입니다.
안전하지 않은 함수의 본문은 사실상 unsafe
블록이므로, 안전하지 않은 함수
내에서 안전하지 않은 연산을 수행하기 위해 또 unsafe
블록을 추가할
필요는 없습니다.
안전하지 않은 코드를 감싸는 안전한 추상화 만들기
함수에 안전하지 않은 코드가 포함되어 있다고 해서 전체 함수를 안전하지 않은
것으로 표시할 필요는 없습니다. 사실 안전하지 않은 코드를 안전한 함수로 감싸는
것은 일반적인 추상화입니다. 예를 들어, 안전하지 않은 코드가 약간 필요한 표준
라이브러리의 split_at_mut
함수를 살펴봅시다. 이를 어떻게 구현할 수 있는지
살펴보겠습니다. 이 안전한 메서드는 가변 슬라이스에 대해 정의됩니다: 하나의
슬라이스를 받아 인수로 주어진 인덱스에서 슬라이스를 분할하여 두 개로 만듭니다.
예제 19-4는 split_at_mut
을 사용하는 방법을 보여줍니다.
fn main() { 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]); }
안전한 러스트만 사용하여 이 함수를 구현할 수는 없습니다. 예제 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..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
이 함수는 먼저 슬라이스의 전체 길이를 얻습니다. 그런 다음 매개변수로 주어진 인덱스가 슬라이스의 길이보다 작거나 같은지 확인하는 것으로 슬라이스 내에 있음을 단언합니다. 이 단언문은 슬라이스를 분할하기 위해 길이보다 큰 인덱스를 전달하면 해당 인덱스를 사용하기 전에 함수가 패닉을 일으키리란 것을 의미합니다.
그다음 두 개의 가변 슬라이스를 튜플 안에 넣어 반환합니다: 하나는 원본 슬라이스의
시작부터 mid
인덱스까지의 슬라이스이고, 다른 하나는 mid
인덱스부터 원본 슬라이스의
끝까지의 슬라이스입니다.
예제 19-5의 코드를 컴파일 시도하면 아래와 같은 에러가 발생합니다.
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&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`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` due to previous error
러스트의 대여 검사기는 슬라이스의 서로 다른 부분을 빌린다는 것을 이해할 수 없습니다; 러스트는 그저 동일한 슬라이스를 두 번 빌린다는 것만 알고 있습니다. 슬라이스의 서로 다른 부분을 빌리는 것은 두 슬라이스가 겹치지 않기 때문에 기본적으로 괜찮지만, 러스트는 이를 알아차릴 만큼 똑똑하지 못합니다. 우리는 코드가 괜찮다는 것을 알지만 러스트는 그렇지 않다면, 안전하지 않은 코드를 이용할 시간입니다.
예제 19-6은 split_at_mut
의 구현체를 작동시키기 위해 unsafe
블록,
원시 포인터, 그리고 안전하지 않은 함수 호출을 사용하는 방법을 보여줍니다.
use std::slice; fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } fn main() { let mut vector = vec![1, 2, 3, 4, 5, 6]; let (left, right) = split_at_mut(&mut vector, 3); }
4장의 ‘슬라이스 타입’ 절에서
슬라이스란 데이터를 가리키는 포인터와 슬라이스의 길이인 것을 기억하세요.
len
메서드를 사용하여 슬라이스의 길이를 얻고 as_mut_ptr
메서드를
사용하여 슬라이스의 원시 포인터를 얻었습니다. 이번 경우에는 i32
값에
대한 가변 슬라이스이므로, as_mut_ptr
는 *mut i32
타입의 원시 포인터를
반환하며, 이 포인터는 ptr
변수에 저장됩니다.
mid
인덱스가 슬라이스 내에 있다는 단언은 유지합니다. 그다음 안전하지
않은 코드에 도달합니다: slice::from_raw_parts_mut
함수는 원시 포인터와
길이를 받아 슬라이스를 생성합니다. 이 함수를 사용하여 ptr
에서 시작하고
mid
길이의 아이템을 가진 슬라이스를 만듭니다. 그런 다음 ptr
에서 mid
를
인수로 add
메서드를 호출하여 mid
에서 시작하는 원시 포인터를 가져오고,
이 포인터와 mid
이후의 나머지 아이템 개수를 길이로 사용하여 슬라이스를
생성합니다.
slice::from_raw_parts_mut
함수는 원시 포인터를 얻어와서 이 포인터의
유효성을 신뢰해야 하기 때문에 안전하지 않습니다. 원시 포인터에 대한 add
메서드도 오프셋 위치가 유효한 포인터임을 신뢰해야 하기 때문에 안전하지
않습니다. 따라서 slice::from_raw_parts_mut
와 add
를 호출할 수
있도록 주위에 unsafe
블록을 넣어야 했습니다. 코드를 살펴보고 mid
가
len
보다 작거나 같아야 한다는 단언문을 추가하면 unsafe
블록 내에서
사용되는 모든 원시 포인터가 슬라이스 내의 데이터에 대한 유효한 포인터가
될 것임을 알 수 있습니다. 이는 unsafe
에 대한 받아들일 만하고 적절한
사용입니다.
결과인 split_at_mut
함수를 unsafe
로 표시할 필요는 없으며,
안전한 러스트에서 이 함수를 호출할 수 있다는 점에 유의하세요.
이 함수는 접근할 수 있는 데이터에서 유효한 포인터만 생성하기 때문에,
unsafe
코드를 안전한 방식으로 사용하는 함수의 구현을 통해 안전하지
않은 코드에 대한 안전한 추상화를 만든 것이 되었습니다.
반면 예제 19-7의 slice::from_raw_parts_mut
사용은 슬라이스가 사용될 때
크래시가 발생하기 쉽습니다. 이 코드는 임의의 메모리 위치를 가져와서 10,000개의
아이템을 가진 슬라이스를 생성합니다.
fn main() { use std::slice; let address = 0x01234usize; let r = address as *mut i32; let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; }
이 임의 위치에 있는 메모리를 소유하지 않고, 이 코드가 생성하는 슬라이스가
유효한 i32
값들을 포함하고 있는지에 대한 보장이 없습니다. values
를 마치
유효한 슬라이스인 것처럼 사용하려고 하면 정의되지 않은 동작이 발생합니다.
extern
함수를 사용하여 외부 코드 호출하기
종종 러스트 코드는 다른 언어로 작성된 코드와 상호작용해야 할 필요가
있습니다. 이를 위해 러스트에는 외래 함수 인터페이스 (Foreign Function Interface, FFI) 의
생성과 사용을 용이하게 하는 키워드 extern
이 있습니다. FFI는 프로그래밍
언어가 함수를 정의하고 다른 (외래) 프로그래밍 언어가 해당 함수를 호출할 수
있도록 하는 방법입니다.
예제 19-8은 C 표준 라이브러리의 abs
함수와의 통합을 설정하는 방법을
보여줍니다. extern
블록 내에 선언된 함수는 러스트 코드에서 호출되기에
항상 안전하지 않습니다. 그 이유는 다른 언어가 러스트의 규칙과 보증을
적용하지 않고, 러스트가 이를 확인할 수 없어서 프로그래머에게 안전을
보장할 책임이 있기 때문입니다.
파일명: src/main.rs
extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } }
extern "C"
블록에는 호출하려는 다른 언어의 외부 함수의
이름과 시그니처를 나열합니다. "C"
부분은 외부 함수가 사용하는
ABI (application binary interface) 를 정의합니다: ABI는
어셈블리 수준에서 함수를 호출하는 방법을 정의합니다. "C"
ABI는
가장 일반적이며 C 프로그래밍 언어의 ABI를 따릅니다.
다른 언어에서 러스트 함수 호출하기
또한
extern
을 사용하여 다른 언어에서 러스트 함수를 호출할 수 있는 인터페이스를 만들 수도 있습니다. 전체extern
블록을 생성하는 대신,extern
키워드를 추가하고 관련 함수에 대한fn
키워드 바로 앞에 사용할 ABI를 지정합니다. 또한#[no_mangle]
어노테이션을 추가하여 러스트 컴파일러가 이 함수의 이름을 맹글링하지 않도록 지시해야 합니다. 맹글링 (mangling) 이란 우리가 함수에 부여한 이름을 컴파일러가 컴파일 과정의 다른 부분에서 사용할 수 있도록 더 많은 정보를 포함하지만 사람이 읽기엔 불편한 다른 이름으로 변경하는 것을 말합니다. 모든 프로그래밍 언어 컴파일러는 이름을 조금씩 다르게 변경하므로, 다른 언어에서 러스트 함수의 이름을 불리도록 하려면 러스트 컴파일러의 이름 맹글링 기능을 비활성화해야 합니다.다음 예제에서는
call_from_c
함수를 공유 라이브러리로 컴파일하고 C에서 링크한 후, C 코드에서 함수에 접근할 수 있도록 합니다:#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); } }
이러한
extern
의 사용에는unsafe
가 필요 없습니다.
가변 정적 변수의 접근 혹은 수정하기
이 책에서 아직 전역 변수 (global variable) 에 대해 언급하지 않았는데, 러스트는 이를 지원하지만 러스트의 소유권 규칙과 문제가 발생할 수 있습니다. 두 스레드가 동일한 가변 전역 변수에 접근하고 있다면 데이터 경합이 발생할 수 있습니다.
러스트에서 전역 변수는 정적 (static) 변수라고 부릅니다. 예제 19-9는 문자열 슬라이스를 값으로 사용하는 정적 변수의 선언과 사용례를 보여줍니다.
파일명: src/main.rs
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("name is: {}", HELLO_WORLD); }
정적 변수는 3장의
‘상수’절에서
다루었던 상수와 유사합니다. 정적 변수의 이름은 관례적으로
SCREAMING_SNAKE_CASE
를 사용합니다. 정적 변수는 'static
라이프타임을 가진 참조자만 저장할 수 있으며, 이는 러스트
컴파일러가 라이프타임을 알아낼 수 있으므로 명시적으로 어노테이션할
필요가 없음을 의미합니다. 불변 정적 변수에 접근하는 것은
안전합니다.
상수와 불변 정적 변수의 미묘한 차이점은 정적 변수의 값이
메모리에 고정된 주소를 갖는다는 점입니다. 값을 사용하면
항상 동일한 데이터에 접근할 수 있습니다. 반면 상수는 사용할
때마다 데이터가 복제될 수 있습니다. 또 다른 차이점은 정적 변수가
가변일 수 있다는 점입니다. 가변 정적 변수에 접근하고 수정하는
것은 안전하지 않습니다. 예제 19-10은 COUNTER
라는 가변 정적
변수를 선언하고, 접근하고, 수정하는 방법을 보여줍니다.
파일명: 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); } }
일반적인 변수와 마찬가지로 mut
키워드를 사용하여 가변성을 지정합니다.
COUNTER
를 읽거나 쓰는 모든 코드는 unsafe
블록 내에 있어야 합니다.
이 코드는 싱글스레드이기 때문에 예상대로 COUNTER: 3
을 컴파일하고
출력합니다. 여러 스레드가 COUNTER
에 접근하면 데이터 경합이 발생할
수 있습니다.
전역적으로 접근할 수 있는 가변 데이터의 경우 데이터 경합이 발생하지 않도록 보장하기가 어려우며, 이것이 러스트가 가변 정적 변수를 안전하지 않은 것으로 간주하는 이유입니다. 가능하면 16장에서 설명한 동시성 기술과 스레드-안전한 스마트 포인터를 사용해 컴파일러가 다른 스레드에서 접근한 데이터에 안전하게 접근하는지 검사하도록 하는 편이 좋습니다.
안전하지 않은 트레이트 구현하기
unsafe
를 사용하여 안전하지 않은 트레이트를 구현할 수 있습니다. 메서드 중
하나 이상에 컴파일러가 확인할 수 없는 불변성 (invariant) 이 있는 경우
그 트레이트는 안전하지 않습니다. 예제 19-11에 표시된 것처럼 trait
앞에 unsafe
키워드를 추가하고 그 트레이트의 구현체도 unsafe
로 표시함으로써
트레이트가 unsafe
하다고 선언할 수 있습니다.
unsafe trait Foo { // 여기에 메소드가 작성됩니다 } unsafe impl Foo for i32 { // 여기에 메소드 구현이 작성됩니다 } fn main() {}
unsafe impl
을 사용하면 컴파일러가 확인할 수 없는 불변성은 우리가
지키겠다는 약속을 하는 것입니다.
예를 들어, 16장의
‘Sync
와 Send
트레이트를 이용한 확장 가능한 동시성’절에서
설명한 Sync
및 Send
마커 트레이트를 상기해 봅시다: 타입이
Send
및 Sync
타입으로만 구성된 경우에는 컴파일러가
이러한 트레이트를 자동으로 구현합니다. 원시 포인터와 같이
Send
혹은 Sync
가 아닌 타입을 포함하고 있는 타입을 구현하고,
해당 타입을 Send
또는 Sync
로 표시하려면 unsafe
를 사용해야
합니다. 러스트는 해당 타입이 스레드 간에 안전하게 전송되거나
여러 스레드에서 접근할 수 있다는 보장을 준수하는지 확인할 수 없습니다;
따라서 이러한 검사를 수동으로 수행하고 unsafe
로 표시해야 합니다.
유니온 필드에 접근하기
unsafe
경우에만 작동하는 마지막 작업은 유니온 (union) 의 필드에 접근하는
것입니다. union
은 struct
와 유사하지만, 특정 인스턴스에서 한 번에 하나의
선언된 필드만 사용됩니다. 유니온은 주로 C 코드의 유니온과 상호작용하는데
사용됩니다. 러스트는 현재 유니온 인스턴스에 저장된 데이터의 타입을 보장할
수 없기 때문에, 유니온 필드에 접근하는 것은 안전하지 않습니다. 유니온에 대한
자세한 내용은 러스트 참고 자료 문서에서 확인할 수 있습니다.
unsafe
코드를 사용하는 경우
unsafe
을 사용하여 방금 설명한 다섯 가지 동작 (슈퍼파워) 중 하나를 수행하는 것은 잘못된
것도 아니고, 심지어 눈살을 찌푸릴 일도 아닙니다. 하지만 컴파일러가 메모리 안전성을
유지할 수 없기 때문에, unsafe
코드를 올바르게 만드는 것은 더 까다롭습니다.
unsafe
코드를 사용해야 할 이유가 있다면 그렇게 할 수 있으며, 명시적인 unsafe
어노테이션을 통해 문제가 발생했을 때 문제의 원인을 더 쉽게 추적할 수 있습니다.