15. no_std: 표준 라이브러리 없는 Rust 🔴
학습 목표:
- OS가 없는 베어메탈(Bare-metal)이나 임베디드 타겟을 위해 표준 라이브러리를 제외하고 Rust를 작성하는 법을 배웁니다.
- **
core**와alloc크레이트의 근본적인 차이를 이해합니다.- 패닉 핸들러(Panic Handler) 설정 및 임베디드 C 개발 환경과의 결정적인 차이점을 익힙니다.
- 하드웨어 추상화 계층(HAL)을 타입 시스템으로 안전하게 다루는 법을 살펴봅니다.
1. no_std란 무엇인가?
임베디드 C 개발자가 libc 없이 최소한의 런타임만 사용하는 것처럼, Rust에서도 #![no_std] 속성을 사용하면 std 라이브러리에 대한 의존성을 제거할 수 있습니다. 대신 모든 로직은 플랫폼 독립적인 core 라이브러리를 기반으로 동작합니다.
주요 라이브러리 계층 구분
| 레이어 | 제공 기능 | OS / 힙(Heap) 필요 여부 |
|---|---|---|
core | 기본 타입, Option, Result, 반복자, 수학 연산 등 | 불필요 (베어메탈용) |
alloc | Vec, String, Box, Arc 등 동적 할당 컬렉션 | OS 불필요, 할당자(Allocator) 필요 |
std | 파일 I/O, 네트워크, 스레드, HashMap 등 | 필요 (OS 의존적) |
2. 베어메탈 프로젝트 설정
OS가 제공하는 기본 기능이 없으므로, 패닉 발생 시의 행동과 프로그램 진입점(main)을 직접 정의해야 합니다.
// src/main.rs #![no_std] // 표준 라이브러리 사용 안 함 #![no_main] // 표준 main 진입점 사용 안 함 (벡터 테이블에서 직접 호출) use core::panic::PanicInfo; // 패닉 발생 시 호출될 핸들러 정의 (필수) #[panic_handler] fn panic(_info: &PanicInfo) -> ! { // 실제 임베디드 장치에서는 시스템 리셋이나 LED 깜박임 등을 수행합니다. loop {} } // 특정 하드웨어 전용 진입점 (예: Cortex-M) // #[entry] // fn main() -> ! { loop {} }
3. std 기능의 임베디드용 대안
표준 라이브러리가 없어도 강력한 커뮤니티 크레이트들이 그 빈자리를 채워줍니다.
| 사용 불가 기능 | 임베디드 대안 | 특징 |
|---|---|---|
println! | defmt / rtt-target | 효율적인 로깅 (RTT/ITM 방식) |
Vec, String | heapless::Vec, String | 스택(Stack) 기반 고정 용량 컬렉션 |
HashMap | heapless::FnvIndexMap | 할당자 없이 사용 가능한 해시맵 |
malloc (Heap) | embedded-alloc | 힙 메모리가 꼭 필요할 때만 선별적 사용 |
💡 임베디드 C vs Rust: HAL 설계의 차이
C언어의 벤더 HAL은 레지스터 설정 실수나 동시성 제어 실패에 취약합니다. Rust는 이를 타입 시스템과 소유권으로 해결합니다.
// Rust 임베디드 코드 예시 fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); // 싱글톤으로 주변장치 독점권 획득 let gpioa = dp.GPIOA.split(); // GPIO 핀들 소유권 분할 // 이 핀은 이제 '출력 모드인 PA5'라는 고유 타입을 가짐 let mut led = gpioa.pa5.into_push_pull_output(); loop { led.set_high().unwrap(); // 타입이 보장된 안전한 동작만 허용됨 delay.delay_ms(500); } }
- 싱글톤 보장:
take()를 통해 동일 주변장치를 두 번 초기화하는 실수를 원천 차단합니다. - 소유권 강제: 출력 핀으로 설정된 자원을 다른 함수에서 실수로 입력 핀으로 오인해 사용하는 것을 컴파일 타임에 막습니다.
- 데이터 경합 방지: 인터럽트 핸들러와 메인 루프 간의 데이터 공유를 빌림 검사기가 엄격히 검증합니다.
📝 실습 연습: no_std 링 버퍼 구현
할당자 없이 core 기능만 사용하여 고정 크기 **링 버퍼(Ring Buffer)**를 구현해 보세요.
- 요구 사항:
const N: usize를 사용해 컴파일 타임에 크기를 결정합니다.MaybeUninit<T>를 사용하여 초기화되지 않은 메모리를 안전하게 관리합니다.- 버퍼가 가득 찼을 때의 처리와
push/pop메서드를 구현하세요.
📌 요약
- **
#![no_std]**는 OS 없는 환경을 위한 Rust의 필수 문취입니다. - **
core**는 플랫폼 독립적이며 어디서든 사용 가능합니다. heapless크레이트는 동적 할당 없이도 강력한 데이터 구조를 제공합니다.- Rust의 임베디드 개발은 "런타임 에러를 컴파일 타임 에러로 옮기는 과정"입니다.