안전하지 않은 러스트 (unsafe Rust)
- 러스트의 메모리 안전성 검사를 우회할 수 있는 기능
- 운영체제, 드라이버, 임베디드 시스템 등 저수준 프로그래밍에 사용
- unsafe 키워드로 unsafe 블록을 열어 사용
안전하지 않은 슈퍼파워 (안전하지 않은 러스트를 통해 얻을 수 있는 능력)
- 원시 포인터 역참조
- 안전하지 않은 함수/메서드 호출
- 가변 정적 변수 접근/수정
- 안전하지 않은 트레이트 구현
- 유니언의 필드 접근
unsafe는 대여 검사기 자체를 끄지 않음. 단지 위 5가지 기능만 허용.
unsafe 블록은 작게 유지하고, 안전한 추상화로 감싸는 것이 권장됨.
원시 포인터 역참조하기 (raw pointer)
안전하지 않은 러스트에는 참조와 유사한 원시포인터라는 두 가지 새로운 타입이 있음
참조자와 마찬가지로 원시 포인터는 불변 또는 가변이며 각각 *const T와 *mut T로 작성됨
*는 역참조 연산자가 아니라 타입 이름의 일부
원시 포인터의 맥락에서 불변이란 포인터가 역참조된 후에 직접 할당할 수 없음을 의미
원시포인터의 특징
- 원시 포인터는 대여 규칙을 무시할 수 있으며, 같은 위치에 대해 불변과 가변 포인터를 동시에 가질 수 있거나 여러 개의 가변 포인터를 가질 수 있음
- 원시 포인터는 유효한 메모리를 가리키는 것을 보장받지 못함
- 원시 포인터는 null이 될 수 있음
- 원시 포인터는 자동 메모리 정리를 구현하지 않음
러스트가 이러한 보증을 적용하지 않도록 선택하면 러스트의 보증이 적용되지 않는 다른 언어 또는 하드웨어와 인터페이싱 할 수 있는 기능이나 더 나은 성능을 얻을 수 있음
fn main() {
let mut num = 5;
// 참조자로 부터 원시 포인터 생성 (동일한 메모리 위치를 가리킴)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// unsafe 블록 내에서 원시 포인터 역참조하기 (안전한 코드에서는 불가)
unsafe {
println!("r1 as {}", *r1);
println!("r2 as {}". *r2);
}
}
원시 포인터를 사용하는 이유
- C언어 코드와 상호작용할 때
- 대여 검사기가 이해하지 못하는 안전한 추상화를 구축할 때
안전하지 않은 함수 또는 메서드 호출하기
안전하지 않은 함수는 안전하지 않은 블록에서만 호출할 수 있음
그러나 안전하지 않은 함수의 본문은 사실 상 unsafe 블록이므로 안전하지 않은 함수 내에서 안전하지 않은 연산을 수행하기 위해
또 unsafe 블록을 추가할 필요는 없음
unsafe fn dangerous {
}
fn main() {
unsafe {
dangerous();
}
}
안전하지 않은 코드를 감싸는 안전한 추상화 만들기
use std::slice;
fn split_at_mut(values: &mut [i32], mid: uszie) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr(); // 원시 포인터
assert!(mid <= len);
// 원시 포인터를 받아 길이만큼 슬라이스를 생성
// unsafe 블럭이지만 안전한 러스트에서도 사용 가능
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
extern 함수를 사용하여 외부 코드 호출하기
러스트에서는 다른 언어로 작성된 코드와 상호작용을 지원하기 위해 FFI (Foreign function interface)의 생성과 사용을 용이하게 하는 extern 키워드가 있음
FFI는 프로그래밍 언어가 함수를 정의하고 다른 프로그래밍 언어가 해당 함수를 호출할 수 있도록 하는 방법
- C 표준 라이브러리인 abs 함수를 가져와서 사용
// "C" 부분에서 외부 함수가 사용하는 ABI(Application binary Interface)를 정의
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("absolute value of -3 according to C: {}", abs(-3));
}
}
- 다른 언어에서 러스트 함수 호출하기
#[no_mangle] // 러스트 컴파일러가 이 함수의 이름을 맹글링 하지 않도록 함
pub extern "C" fn call_from_c() {
println!("hi");
}
가변 정적 변수의 접근 혹은 수정하기
러스트는 전역변수를 지원하지만 소유권 규칙에서 문제가 발생할 수 있음
두 스레드가 동일한 가변 전역 변수에 접근한다면 데이터 경합이 발생할 수 있기 때문
러스트에서 전역 변수는 정적 변수라고 부름
정적 변수의 이름은 관례적으로 SCREAMING_SNAKE_CASE를 사용
정적 변수는 'static 라이프타임을 가진 참조자만 저장할 수 있으며,
이는 러스트 컴파일러가 라이프타임을 알아낼 수 있으므로 명시할 필요가 없음을 의미
물론 불변 정적 변수에 접근하는 것은 안전함
상수와 불변 정적 변수의 미묘한 차이점은 정적 변수의 값이 메모리에 고정된 주소를 갖는다는 점임
그러나 상수는 사용할 때마다 데이터가 복제될 수 있고
정적 변수는 가변일 수 있음
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
안전하지 않은 트레이트 구현하기
메서드 중 하나 이상에 컴파일러가 확인 할 수 없는 불변성(invariant)가 있는 경우 그 트레이트는 안전하지 않음
unsafe impl을 사용하면 컴파일러가 확인할 수 없는 불변성은 우리가 지키겠다는 약속을 하는 것임
unsafe trait Foo {
}
unsafe impl Foo for i32 {
}
fn main() {
}
유니언 필드에 접근하기
union은 struct와 유사하지만, 특정 인스턴스에서 한 번에 하나의 선언된 필드만 사용됨
유니언은 주로 C 코드의 유니언과 상호작용하는데 사용됨
러스트는 현재 유니언 인스턴스에 저장된 데이터 타입을 보장할 수 없기 때문에 유니언 필드에 접근하는 것은 안전하지 않음
'Programming > Rust' 카테고리의 다른 글
| 러스트 고급 타입 (0) | 2025.10.12 |
|---|---|
| 러스트 고급 트레이트 (0) | 2025.10.12 |
| 러스트 패턴과 매칭 (0) | 2025.10.11 |
| 러스트의 객체 지향 프로그래밍 기능 (0) | 2025.10.11 |
| 러스트 동시성 (0) | 2025.10.09 |
