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.

Megjegyzés

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 az alloc crate-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át defmt vagy 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;
Tipp

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 {}
}
Figyelem

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 main visszatérési érték ()-ként — -> ! típust adunk vissza, mert egy embedded programban a main sosem tér vissza
  • Az #[entry] attribútum (a cortex-m-rt crate-ből) helyettesíti a szokásos runtime-indítási logikát
  • Az OutputPin trait az embedded-hal crate-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
Jó tudni

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:

  1. Elfelejtett #![no_main] — ha a #[entry] makrót használod a cortex-m-rt-ből, a szokásos fn main() belépési pontot le kell tiltanod a #![no_main] attribútummal, különben duplikált main szimbólum hibát kapsz.

  2. println! használata — ez std-függő makró, no_std alatt nem létezik. Helyette logoláshoz gyakran a defmt crate-et vagy egyszerű UART write-okat használnak.

  3. Vec, String haszná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 a heapless crate fix méretű, stack-alapú kollekcióit ajánlom, amíg nincs igazán szükséged dinamikus allokációra.

  4. Rossz target triple — ha nem a mikrokontrolleredhez illő target-re (--target thumbv7em-none-eabihf vagy 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.

  5. Panic handler hiánya vagy duplikáltsága — mint korábban említettem, pontosan egy panic handler lehet a binárisban.

  6. 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.

Tipp

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.