Skip to content

Latest commit

 

History

History
309 lines (226 loc) · 7.32 KB

10.md

File metadata and controls

309 lines (226 loc) · 7.32 KB

Rust 문법 로드맵 — 10단계: Unsafe & 저수준 문법 (상세 교재, 선택)

이 문서는 로드맵 10단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 선택 심화입니다. 임베디드·드라이버·FFI(C++/C LiDAR 드라이버 등)와 연결됩니다. unsafe는 “안전 검사를 끈다”가 아니라 프로그래머가 계약을 보장하겠다는 선언입니다.

선수: 2단계, 7단계 Box/raw에 가까운 개념. 일상 앱 코드만 짜면 우선순위는 낮아도 됩니다.


10.1 unsafe가 허용하는 다섯 가지

Safe Rust가 금지하고, unsafe 블록/unsafe fn 안에서만 가능한 것:

  1. raw pointer 역참조 (*const T, *mut T)
  2. unsafe 함수/메서드 호출
  3. mutable static 접근
  4. unsafe trait 구현
  5. union 필드 접근
fn main() {
    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1 = {}", *r1);
        *r2 = 10;
        println!("r2 = {}", *r2);
    }
}

unsafe 밖에서는 raw pointer를 만들고 넘기는 것까지는 가능합니다. 위험한 연산(역참조 등)만 블록 안에 둡니다. 최소 범위로 감싸는 게 관례입니다.

unsafe fn — 호출자가 계약을 지켜야 함

/// # Safety
/// `ptr`는 유효하고 정렬된 `i32`를 가리켜야 하며, 수명 동안 유효해야 한다.
unsafe fn read_i32(ptr: *const i32) -> i32 {
    unsafe { *ptr }
}

fn main() {
    let x = 42;
    let v = unsafe { read_i32(&x) };
    println!("{v}");
}

문서에 # Safety 섹션을 남기는 것이 사실상 필수 문화입니다.

mutable static

static mut COUNTER: u32 = 0;

fn main() {
    unsafe {
        COUNTER += 1;
        println!("{COUNTER}");
    }
}

데이터 레이스 가능 → 실무에서는 AtomicU32, Mutex, OnceLock을 쓰세요. (1단계 static 복습)

체크포인트

  • raw pointer를 unsafe 없이 역참조해 에러 확인
  • unsafe 블록을 역참조 한 줄만 남기도록 좁히기

10.2 Raw pointer 문법

fn main() {
    let mut x = 5i32;

    let p: *const i32 = &x;
    let q: *mut i32 = &mut x;

    // 널 포인터
    let null: *const i32 = std::ptr::null();
    let null_mut: *mut i32 = std::ptr::null_mut();

    // 오프셋 (bounds는 프로그래머 책임)
    unsafe {
        let a = [10, 20, 30];
        let base = a.as_ptr();
        println!("{}", *base.add(1)); // 20
    }

    let _ = (p, q, null, null_mut);
}

참조(&T)와의 차이:

&T / &mut T *const T / *mut T
유효성 컴파일러 보장 프로그래머 보장
null 없음 가능
얼라이먼트 보장 깨질 수 있음
빌림 규칙 적용 적용 안 됨

slice::from_raw_parts, Box::from_raw / into_raw 등은 FFI·커스텀 할당자에서 자주 만납니다.

fn main() {
    let b = Box::new(123);
    let ptr = Box::into_raw(b); // 소유권 누수 형태로 넘김 — free 책임 이전
    unsafe {
        println!("{}", *ptr);
        let _ = Box::from_raw(ptr); // 다시 Rust가 drop
    }
}

체크포인트

  • Box::into_raw / from_raw 왕복
  • as_ptr()로 배열 두 번째 원소 읽기

10.3 FFI — extern "C"

C ABI로 외부 함수를 선언합니다.

use std::os::raw::{c_char, c_int};
use std::ffi::CStr;

unsafe extern "C" {
    fn abs(input: c_int) -> c_int;
    // fn lidar_init(cfg: *const c_char) -> c_int; // 드라이버 예
}

fn main() {
    unsafe {
        println!("abs(-3) = {}", abs(-3));
    }
}

Rust → C 문자열:

use std::ffi::CString;

fn main() {
    let c = CString::new("hello").expect("NUL 불가");
    let ptr = c.as_ptr(); // *const c_char
    // C 함수에 ptr 전달 — c가 살아 있는 동안만
    let _ = ptr;
}

C → Rust 문자열:

use std::ffi::CStr;
use std::os::raw::c_char;

unsafe fn from_c(ptr: *const c_char) -> Option<String> {
    if ptr.is_null() {
        return None;
    }
    Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned())
}

#[no_mangle] + extern "C" — Rust를 C에서 호출

#[unsafe(no_mangle)] // 에디션/버전에 따라 #[no_mangle]
pub unsafe extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

(에디션·rustc 버전에 따라 속성 문법이 조금 다를 수 있습니다. 프로젝트 컴파일러에 맞추세요.)

체크포인트

  • libc 또는 extern "C" { fn abs... }로 시스템 함수 하나 호출
  • CString::new에 내부 \0을 넣어 에러 확인

10.4 #[repr(C)] — 메모리 레이아웃

Rust 기본 레이아웃은 최적화로 필드 순서가 바뀔 수 있습니다. C와 맞춰야 하면:

#[repr(C)]
struct LidarPoint {
    x: f32,
    y: f32,
    z: f32,
    intensity: u16,
}

#[repr(C)]
enum CLike {
    A = 1,
    B = 2,
}

자주 쓰는 repr:

속성 의미
#[repr(C)] C 호환 레이아웃
#[repr(transparent)] 뉴타입 하나를 내부와 동일 ABI로
#[repr(u8)] 등 enum 판별자 크기 고정
#[repr(packed)] 패딩 제거 (얼라이먼트 주의, 위험)
#[repr(transparent)]
struct UserId(u64); // FFI에서 u64처럼 전달 가능

체크포인트

  • #[repr(C)] struct의 size_of / offset_of (또는 수동 출력)로 필드 오프셋 확인
  • repr 없는 같은 struct와 크기가 달라질 수 있음을 인지

10.5 종합 실습 — C 함수 하나를 Rust에서 호출 (스케치)

로드맵: LiDAR C++ 드라이버의 단순 함수 FFI. 최소 예는 C로도 충분합니다.

c_api.h / c_api.c (가상):

// int lidar_version(void) { return 42; }

build.rs (개념):

// cc::Build::new().file("c_api.c").compile("lidar");

Rust:

unsafe extern "C" {
    fn lidar_version() -> i32;
}

pub fn version() -> i32 {
    unsafe { lidar_version() }
}

fn main() {
    // println!("lidar version = {}", version());
    println!("FFI 스케치 — 실제 .c / build.rs 를 프로젝트에 연결하세요");
}

실습 체크리스트:

  • cc 크레이트로 C 파일 컴파일
  • #[repr(C)]로 포인트 구조체 공유
  • null 체크·에러 코드를 Result로 래핑해 safe 공개 API만 밖으로 노출

핵심 패턴: unsafe는 모듈 내부에 가두고, 바깥에는 safe 함수만.

mod ffi {
    unsafe extern "C" {
        pub(super) fn lidar_version() -> i32;
    }
}

pub fn lidar_version_safe() -> i32 {
    unsafe { ffi::lidar_version() }
}

확장 과제

  • C의 int parse(const char*, int*) 스타일을 Rust Result로 래핑
  • Box::into_raw로 Rust 할당 버퍼를 C에 넘기고, 콜백에서 from_raw로 회수
  • static mut 대신 AtomicI32로 카운터 작성

이 단계 한 장 요약

unsafe 5가지: *ptr, unsafe fn, mut static, unsafe trait, union
raw pointer ≠ 참조 (유효성·null·빌림)
extern "C" + CString/CStr
#[repr(C)] / transparent / u8
패턴: unsafe 내부 은닉 → safe API

다음 단계 예고

11단계 Async 문법에서는 Tokio 사용법을 넘어, async fn = impl Future, .await와 상태 머신, Pin, 수동 poll을 다룹니다.