이 문서는 로드맵 10단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 선택 심화입니다. 임베디드·드라이버·FFI(C++/C LiDAR 드라이버 등)와 연결됩니다.
unsafe는 “안전 검사를 끈다”가 아니라 프로그래머가 계약을 보장하겠다는 선언입니다.
선수: 2단계, 7단계 Box/raw에 가까운 개념. 일상 앱 코드만 짜면 우선순위는 낮아도 됩니다.
Safe Rust가 금지하고, unsafe 블록/unsafe fn 안에서만 가능한 것:
- raw pointer 역참조 (
*const T,*mut T) unsafe함수/메서드 호출- mutable static 접근
unsafetrait 구현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를 만들고 넘기는 것까지는 가능합니다. 위험한 연산(역참조 등)만 블록 안에 둡니다. 최소 범위로 감싸는 게 관례입니다.
/// # 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 섹션을 남기는 것이 사실상 필수 문화입니다.
static mut COUNTER: u32 = 0;
fn main() {
unsafe {
COUNTER += 1;
println!("{COUNTER}");
}
}데이터 레이스 가능 → 실무에서는 AtomicU32, Mutex, OnceLock을 쓰세요. (1단계 static 복습)
- raw pointer를
unsafe없이 역참조해 에러 확인 -
unsafe블록을 역참조 한 줄만 남기도록 좁히기
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()로 배열 두 번째 원소 읽기
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())
}#[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을 넣어 에러 확인
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와 크기가 달라질 수 있음을 인지
로드맵: 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*)스타일을 RustResult로 래핑 -
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을 다룹니다.