Mi az a no_std, és mikor van rá szükség?
A Rust standard könyvtára (std) alapból feltételez egy futó operációs rendszert: fájlrendszert, hálózati stacket, szálkezelést, heap allokátort és sok más szolgáltatást. Ez tökéletes desktopon, szerveren vagy akár Raspberry Pi-n Linux alatt, de egy Cortex-M mikrokontrolleren, ahol nincs OS, nincs fájlrendszer és néhány kilobyte RAM van csak, ez az egész infrastruktúra egyszerűen nem létezik.
Itt jön képbe a #![no_std] attribútum. Ezzel megmondod a compilernek, hogy ne linkelje be a std crate-et, hanem elégedj meg a core crate-tel, amely a nyelv és a típusrendszer alapvető funkcióit adja OS-függetlenül.
A no_std nem egy külön nyelv vagy dialektus — ugyanaz a Rust, ugyanazok a nyelvi feature-ök (pl. GATs, let-else is elérhető), csak a rendelkezésre álló könyvtári API-k szűkebbek.
Tipikus no_std use case-ek:
- Mikrokontrolleres firmware (STM32, nRF52, RP2040, AVR)
- Bootloaderek
- Kernel-szintű kód (pl. saját OS írása)
- WebAssembly nagyon szűk futtatási környezetben
Ha desktopos vagy szerveres alkalmazást írsz, nagy valószínűséggel sosem lesz szükséged erre — a no_std elsősorban embedded és low-level rendszerprogramozási terep.
A std és a core könyvtár közötti különbségek
A core crate tartalmazza mindazt, ami OS-függetlenül is működik: primitív típusokat, Option, Result, iterátorokat, a fmt modult formázáshoz, a slice és str alapműveleteket, atomikus típusokat, és persze a traiteket (Clone, Debug, Iterator stb.).
Ami hiányzik a core-ból:
- Heap allokáció (
Box,Vec,String) — ezek azalloccrate-ben vannak, amit külön kell bekapcsolni, és allokátort kell hozzá biztosítani - Fájlkezelés (
std::fs) - Hálózat (
std::net) - Szálak (
std::thread), mutexek OS-támogatással - Standard I/O (
println!— helyette gyakran egy sajátdefmtvagy UART-alapú logolást használunk)
Az alloc crate egy köztes réteg: heap-alapú típusokat ad, de a tényleges memóriakezelést egy általad megadott allokátorra bízza. Ez fontos, mert egy mikrokontrolleren nincs OS-szintű malloc, neked kell definiálnod, honnan és hogyan foglalódjon memória.
#![no_std]
#![no_main]
// A core mindig elérhető, ha no_std-t használunk
use core::fmt::Write;
// Ha heap-et is szeretnénk, ezt kell hozzáadni:
extern crate alloc;
use alloc::vec::Vec;
Ha csak stack-alapú, fix méretű adatszerkezetekkel dolgozol (pl. heapless crate), gyakran meg tudod spórolni az allokátor bevezetését is. Kezdőknek ez az egyszerűbb út.
Panic handler és allokátor beállítása minimális projektben
A std alatt a panic! makró egy jól definiált panic handlerhez fut, amely kiírja a hibát és leállítja a szálat (vagy unwindol). No_std alatt neked kell megadnod, mi történjen panic esetén — a compiler egyszerűen nem fordul le panic handler nélkül.
A legegyszerűbb megoldás egy külső crate, például a panic-halt, amely végtelen ciklusba lépteti a rendszert panic esetén:
#![no_std]
#![no_main]
use panic_halt as _; // panic handlert biztosít, nekünk nem kell kódot írnunk hozzá
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
// itt fut a firmware fő ciklusa
}
}
Ha saját panic handlert akarsz írni (pl. UART-ra kiírni a hibát, majd resetelni az eszközt), az attribútum #[panic_handler]:
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
// itt loggolhatnál, mielőtt megállnál
loop {}
}
Egy binárisban csak egy panic handler lehet. Ha a panic-halt crate-et importálod, és utána saját #[panic_handler]-t is írsz, linker hibát fogsz kapni ütköző szimbólumok miatt.
Ha heap-alapú típusokat (Vec, Box, String) is szeretnél használni, kell egy globális allokátor. Erre jó választás a alloc-cortex-m vagy hasonló crate, amelyet a #[global_allocator] attribútummal regisztrálsz:
#![no_std]
#![no_main]
extern crate alloc;
use alloc_cortex_m::CortexMHeap;
#[global_allocator]
static ALLOCATOR: CortexMHeap = CortexMHeap::empty();
fn init_heap() {
let start = cortex_m_rt::heap_start() as usize;
let size = 1024; // byte-ban megadott heap méret
unsafe { ALLOCATOR.init(start, size) }
}
Ez unsafe blokk, mert a memóriaterület helyességéért (hogy valóban szabad és elég nagy) neked kell garanciát vállalnod — a compiler ezt nem tudja ellenőrizni.
Egyszerű LED villogtató példa no_std környezetben
Nézzünk egy tipikus, minimalista példát, amely egy Cortex-M alapú mikrokontrolleren (a cortex-m, cortex-m-rt és egy HAL crate segítségével) villogtat egy LED-et. A konkrét regiszter- és pin-elnevezések természetesen a választott chip HAL-jától függenek, itt egy generikus, embedded-hal-kompatibilis vázlatot mutatok:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
use embedded_hal::digital::v2::OutputPin;
// Feltételezzük, hogy van egy `hal` nevű HAL crate-ünk,
// amely biztosítja a Peripherals struktúrát és a GPIO API-t
use hal::pac::Peripherals;
use hal::gpio::GpioExt;
use hal::delay::Delay;
#[entry]
fn main() -> ! {
let dp = Peripherals::take().unwrap();
let gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
let mut delay = Delay::new(dp.TIM2, 8_000_000);
loop {
led.set_high().unwrap();
delay.delay_ms(500u32);
led.set_low().unwrap();
delay.delay_ms(500u32);
}
}
Ami ebben a kódban a fontos:
- Nincs
mainvisszatérési érték()-ként —-> !típust adunk vissza, mert egy embedded programban amainsosem tér vissza - Az
#[entry]attribútum (acortex-m-rtcrate-ből) helyettesíti a szokásos runtime-indítási logikát - Az
OutputPintrait azembedded-halcrate-ből jön — ez egy hardver-agnosztikus absztrakció, ami miatt a kód más chipre is könnyen portolható, ha a HAL implementálja ugyanazt a traitet
A .cargo/config.toml-ban be kell állítanod a megfelelő target triple-t (pl. thumbv7em-none-eabihf), és egy linker scriptet (memory.x), amely leírja a flash és RAM memóriatartományokat. Ezek nélkül a build nem fog sikerülni, még ha a Rust kód maga helyes is.
Gyakori hibák és buktatók kezdőknek
Amikor először nyúlsz no_std kódhoz, néhány tipikus csapdába könnyen bele lehet futni:
-
Elfelejtett
#![no_main]— ha a#[entry]makrót használod acortex-m-rt-ből, a szokásosfn main()belépési pontot le kell tiltanod a#![no_main]attribútummal, különben duplikáltmainszimbólum hibát kapsz. -
println!használata — ez std-függő makró, no_std alatt nem létezik. Helyette logoláshoz gyakran adefmtcrate-et vagy egyszerű UART write-okat használnak. -
Vec,Stringhasználata allokátor beállítása nélkül — ha ezeket importálod, de nincs#[global_allocator], linker hibát kapsz. Kezdőként inkább aheaplesscrate fix méretű, stack-alapú kollekcióit ajánlom, amíg nincs igazán szükséged dinamikus allokációra. -
Rossz target triple — ha nem a mikrokontrolleredhez illő target-re (
--target thumbv7em-none-eabihfvagy hasonló) fordítasz, a build vagy egyáltalán nem indul el, vagy host-architektúrára készül bináris, amit a chip nem tud futtatni. -
Panic handler hiánya vagy duplikáltsága — mint korábban említettem, pontosan egy panic handler lehet a binárisban.
-
Interrupt kezelés hibás beállítása — ha megszakításokat (interrupt) is használsz, figyelj arra, hogy a megszakítás-kezelő függvények szignatúrája és regisztrálása pontosan megfeleljen a HAL/PAC crate elvárásainak, különben a vector table rossz helyre fog mutatni futásidőben.
Kezdőknek nagyon ajánlott a cargo generate és egy kész embedded template (pl. a cortex-m-quickstart sablon) használata, mert ez már tartalmazza a helyes memory.x, .cargo/config.toml és build.rs beállításokat — sokkal kevesebb frusztrációval jár, mint mindent nulláról összerakni.
Összefoglalás
A no_std Rust elsőre ijesztőnek tűnhet, mert hirtelen el kell búcsúznod a megszokott std kényelmi funkcióitól, de a core és opcionálisan az alloc crate segítségével továbbra is ugyanazt a kifejező, biztonságos nyelvet használhatod, csak most már bare-metal környezetben. A panic handler és az allokátor beállítása kezdetben extra lépésnek tűnik, de gyorsan rutinná válik, ha egyszer átmentél egy-két projekten. A legjobb tanács: kezdj egy kész quickstart sablonnal, próbáld ki a LED villogtatást, majd fokozatosan bővítsd interruptokkal, perifériákkal és — ha szükséges — heap-alapú adatszerkezetekkel.