14. 실전 Unsafe Rust와 FFI 🔴
학습 목표:
- Rust의 안전 보장 장치를 잠시 해제하는
unsafe키워드의 목적과 책임을 배웁니다.- **원시 포인터(Raw Pointer)**를 제어하고 타 언어(C/C++)와의 접점인 FFI(Foreign Function Interface) 구현법을 익힙니다.
CString/CStr을 활용한 문자열 상호 운용과 안전한 래퍼(Safe Wrapper) 설계 기법을 배웁니다.#[repr(C)]와 패닉 경계 처리 등 실무에서 반드시 지켜야 할 안전 수칙을 확인합니다.
1. Unsafe Rust의 본질: "큰 힘에는 큰 책임이 따른다"
Rust는 기본적으로 메모리 안전을 보장하지만, 하드웨어를 직접 제어하거나 저수준 최적화를 수행할 때는 컴파일러의 엄격한 규칙을 우회해야 하는 순간이 있습니다.
unsafe로 할 수 있는 일:- 원시 포인터(
*const T,*mut T) 역참조 - 다른 언어의 함수 호출 (FFI)
- 가변 정적 변수(
static mut) 접근 및 수정 unsafe트레이트 구현
- 원시 포인터(
- 철학: 프로그래머가 컴파일러를 대신해 메모리 안전(Dangling pointer, Data race 등)을 책임지겠다는 명시적인 선언입니다.
- 원칙:
unsafe블록은 가능한 한 좁게 유지하고, 반드시 그 안전성을 보장하는 근거를// Safety:주석으로 남기는 것이 실무의 정석입니다.
fn main() { let mut num = 42; // 원시 포인터 생성 자체는 안전합니다. (캐스팅) let r1 = &num as *const i32; let r2 = &mut num as *mut i32; // 하지만 실제 역참조(읽기/쓰기)는 오직 unsafe 블록 내에서만 가능합니다. unsafe { println!("r1: {}", *r1); *r2 = 100; // 원격 수정 println!("num 값 변경됨: {}", num); } }
2. 외래 함수 인터페이스: FFI (Foreign Function Interface)
Rust에서 기존 C 라이브러리를 호출하거나, 반대로 C에서 Rust 기능을 사용하게 할 때 필수적인 도구입니다.
문자열 상호 운용 (CString과 CStr)
Rust 문자열(UTF-8, 길이 정보 포함)과 C 문자열(바이트 배열, \0 종료)은 형식이 다릅니다. 이 간극을 메워주는 전전용 타입이 std::ffi 모듈에 있습니다.
| 타입 | 대응 개념 | 용도 |
|---|---|---|
CString | String (소유형) | Rust 데이터를 C로 보낼 때 (\0 추가 발생) |
&CStr | &str (빌림형) | C로부터 데이터를 받을 때 (\0 기준으로 읽음) |
Rust 함수를 외부(C)에 노출하기
이름 바뀜(Name Mangling)을 방지하고 C 표준 호출 규약을 따라야 합니다.
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn rust_sum(a: i32, b: i32) -> i32 { a + b } }
3. 실전 기술: 안전한 래퍼(Safe Wrapper) 설계
unsafe 로직을 날것 그대로 외부에 노출하지 마세요. 위험하고 복잡한 구현은 내부로 숨기고 사용자에게는 안전한 Rust API만 제공하는 것이 핵심입니다.
#![allow(unused)] fn main() { // [불투명 포인터 스타일의 메모리 관리] // C가 가질 포인터는 Box::leak로 메모리 해제를 일시 중지시키고, // 작업 완료 후에는 다시 Box::from_raw로 소유권을 되찾아 자동 해제되도록 설계합니다. #[no_mangle] pub extern "C" fn my_logger_free(ptr: *mut MyLogger) { if !ptr.is_null() { // Safety: ptr은 이전에 Box::leak로 생성된 유효한 포인터임이 보장되어야 함 unsafe { let _ = Box::from_raw(ptr); } // 여기서 자동으로 Drop되어 메모리가 해제됨 (RAII) } } }
💡 검증 도구: Miri vs Valgrind
C++의 Sanitizer 외에도 Rust 특유의 정의되지 않은 동작(UB)을 잡아주는 전용 도구가 있습니다.
| 특징 | Miri | Valgrind / ASan |
|---|---|---|
| 주요 탐지 | Rust 에일리어싱 규칙 위반, 잘못된 enum 값 등 | 메모리 누수, 잘못된 메모리 접근 (전역/힙/스택) |
| 작동 방식 | MIR 중간 단계 인터프리터 방식 | 컴파일된 바이너리 실행 방식 |
| FFI 지원 | 불가능 (순수 Rust만 가능) | 가능 (FII 경계를 넘어 동작 가능) |
| 사용 시점 | 복잡한 unsafe 로직 검증 시 | FFI 연동 후 통합 테스트 시 |
⚠️ FFI 연동 시 주의사항 (Critical)
- 패닉 전파 금지: Rust에서 발생한 패닉이 C/C++ 코드로 흘러 들어가게 해서는 안 됩니다. 이는 즉시 **정의되지 않은 동작(UB)**을 유발하므로,
catch_unwind로 감싸거나 중단(abort) 처리를 해야 합니다. - 메모리 레이아웃 고정: C와 구조체를 공유한다면 반드시
#[repr(C)]속성을 부여하세요. 그렇지 않으면 Rust 컴파일러가 최적화를 위해 필드 순서를 임의로 바꿀 수 있습니다. - 널 포인터 체크: C로부터 넘어오는 포인터는 언제나
null일 수 있다고 가정하고 대응 로직을 짜야 합니다.
📌 요약
- **
unsafe**는 컴파일러가 보장하지 못하는 영역을 개발자가 직접 책임지겠다는 선언입니다. - FFI를 사용할 때는 데이터 레이아웃(
repr(C))과 패닉 경망 처리에 극히 주의해야 합니다. - 모든 불필요한 위험은 안전한 래퍼(Safe Wrapper) 뒤로 숨기세요.
- Miri와 Valgrind를 활용해
unsafe코드의 무결성을 검증하세요.