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

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 기능을 사용하게 할 때 필수적인 도구입니다.

문자열 상호 운용 (CStringCStr)

Rust 문자열(UTF-8, 길이 정보 포함)과 C 문자열(바이트 배열, \0 종료)은 형식이 다릅니다. 이 간극을 메워주는 전전용 타입이 std::ffi 모듈에 있습니다.

타입대응 개념용도
CStringString (소유형)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)을 잡아주는 전용 도구가 있습니다.

특징MiriValgrind / 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) 뒤로 숨기세요.
  • MiriValgrind를 활용해 unsafe 코드의 무결성을 검증하세요.