Ha valaha megpróbáltál kézzel Future-t implementálni, biztosan belefutottál már a Pin<&mut Self> típusba a poll metódus szignatúrájában. Sokan egyszerűen átsiklanak felette, és soha nem értik meg igazán, mi történik a színfalak mögött. Ma ezt pótoljuk.

Miért van szükség pinningre async kódban

Amikor egy async fn-t írsz, a fordító a függvény törzséből egy állapotgépet (state machine) generál. Ez az állapotgép egy struct, aminek a mezői a .await pontok között élő lokális változók. A probléma az, hogy ezek a lokális változók gyakran egymásra mutatnak – tipikusan akkor, ha egy .await pont előtt létrehozol egy értéket, majd egy referenciát veszel rá, amit a következő .await után is használsz.

async fn example() {
    let mut data = String::from("rust.hu");
    let data_ref: &str = &data; // önmagára mutató referencia
    some_async_op().await;
    println!("{}", data_ref);
}

Az async blokkból generált struct nagyjából így néz ki (leegyszerűsítve):

struct ExampleFuture {
    data: String,
    data_ref: *const str, // valójában a data-ra mutat!
}

Ez egy self-referenciális struktúra: az egyik mező a másikra mutat. Ha ezt a structot valaki mem::swap-pel vagy egyszerű mozgatással (move) áthelyezi a memóriában, a data_ref pointer érvénytelenné válik – a régi memóriacímre fog mutatni, ahol már nem biztos, hogy a data van.

Jó tudni

A Rust Move szemantikája alapból megengedi, hogy bármilyen értéket szabadon áthelyezz a memóriában (pl. amikor egy structot visszaadsz egy függvényből). Self-referenciális struktúráknál ez katasztrófához vezetne, ezért kell egy mechanizmus, ami garantálja, hogy egy adott érték a memóriában rögzítve marad.

A Pin típus és a self-referenciális struktúrák problémája

Itt jön képbe a std::pin::Pin. A Pin<P> egy wrapper egy pointer típus (P, tipikusan &mut T vagy Box<T>) körül, ami garantálja, hogy az általa mutatott érték nem mozgatható, amíg a Pin létezik – kivéve, ha a típus implementálja az Unpin marker traitet.

Az Unpin egy auto trait: a legtöbb típus automatikusan implementálja, mert a legtöbb típus biztonságosan mozgatható (pl. i32, String, Vec<T>). Csak azok a típusok NEM Unpin-ek, amik explicit módon !Unpin-ként vannak megjelölve – tipikusan az async állapotgépek, amiket a fordító generál, ha tartalmaznak self-referenciát.

use std::marker::PhantomPinned;

struct SelfReferential {
    data: String,
    self_ptr: *const String,
    _pin: PhantomPinned, // megjelöli a structot mint !Unpin
}

A PhantomPinned egy zero-size marker típus, ami kikapcsolja az Unpin automatikus implementációját. Amint egy típus !Unpin, a fordító és a runtime is tudja, hogy csak Pin-en keresztül szabad hozzáférni – és a Pin garantálja, hogy amint egyszer pinneltük, a struct a helyén marad a drop-jáig.

A stabilizált pin! makró bemutatása

A Future trait poll metódusa Pin<&mut Self>-et vár, nem sima &mut Self-et. Ha van egy Future-öd, amit közvetlenül pollozni akarsz (pl. saját executor írásakor, vagy select!-szerű kombinátorban), előbb pinelned kell.

Eddig két gyakori út létezett erre: a heap-alapú Box::pin, vagy a tokio::pin! makró, ami stack-en pinel. A Rust csapat viszont már régóta dolgozik egy std-beli megoldáson, ami ugyanazt a stack-pinelést biztosítja külső crate nélkül. A jó hír, hogy a std::pin::pin! makró stabilizációja már a következő, 1.68-as release-ben megérkezik – a tracking issue szerint a feature készen áll, jelenleg a nightly kanálon már kipróbálható #![feature(pin_macro)] mögött.

Megjegyzés

Amíg nem jelenik meg stabilan az 1.68, addig a mai (1.67-es) toolchain-en a std::pin::pin! makró csak nightly-n érhető el feature flag mögött. Ha stabil kódot írsz, egyelőre a tokio::pin!-re vagy a Box::pin-re kell támaszkodnod – ezeket mutatom is lejjebb.

A pin! makró ötlete egyszerű: egy kifejezést a stacken pinel, anélkül hogy heap allokációra lenne szükség (szemben a Box::pin-nel). A tervezett API nagyjából így fog kinézni, amikor stabilizálódik:

// Ez a kód a tervek szerint 1.68-tól fog stabilan fordulni.
// Ma (1.67) csak nightly + #![feature(pin_macro)] mögött működik.
#![feature(pin_macro)]
use std::pin::pin;
use std::future::Future;
use std::task::{Context, Poll};

async fn some_future() -> i32 {
    42
}

fn poll_manually() {
    let fut = some_future();
    let mut pinned = pin!(fut); // stack pinning, heap allokáció nélkül
    // ... innen már pinned.as_mut()-tal pollozható
}

Amíg ez nem stabil, addig a leggyakorlatibb megoldás a tokio::pin!, ami ma is használható és ugyanazt az elvet valósítja meg.

Mikor kell manuálisan pin-elni egy Future-t

A jó hír: a mindennapi async fn + .await kódban szinte soha nem kell kézzel pinelned semmit. Az .await operátor a színfalak mögött már elvégzi a pinelést helyetted. Manuális pinningre akkor van szükség, ha:

  • Saját executort vagy reaktort írsz, és közvetlenül hívod a poll-t.
  • Több Future-t akarsz egyszerre, dinamikusan kezelni (pl. select!-szerű logikában), és a Future típusát nem ismered fordítási időben (Box<dyn Future<Output = T>>).
  • Egy Future-t egy ciklusban akarsz többször pollozni anélkül, hogy minden iterációban újra létrehoznád.

Íme egy stabil, ma is működő megoldás tokio::pin!-nel:

use std::future::Future;
use std::task::{Context, Poll};
use std::pin::Pin;

async fn worker() -> u32 {
    7
}

fn manual_poll_example(cx: &mut Context<'_>) -> Poll<u32> {
    let fut = worker();
    tokio::pin!(fut); // fut mostantól Pin<&mut _>-ként érhető el
    fut.poll(cx)
}

Ha a Future-t heap-en akarod tárolni – például mert típusa fordítási időben nem determinisztikus (dyn Future) –, a Box::pin a helyes eszköz:

use std::future::Future;
use std::pin::Pin;

fn boxed_future(condition: bool) -> Pin<Box<dyn Future<Output = i32>>> {
    if condition {
        Box::pin(async { 1 })
    } else {
        Box::pin(async { 2 })
    }
}
Tipp

A Box::pin mindig heap allokációval jár, míg a tokio::pin! (és a majdani std::pin::pin!) stack-en pinel, tehát gyorsabb, ha nem kell a Future-t típustörléssel (dyn Future) tárolnod.

Gyakori hibák és hogyan kerüljük el őket

1. unsafe Pin::new_unchecked helytelen használata. A Pin::new_unchecked unsafe függvény, mert a hívó ígéretet tesz arra, hogy az érték soha többé nem lesz mozgatva. Ha ezt megszeged (pl. később mégis kimozgatod az értéket egy mem::swap-pel), undefined behavior lesz belőle. Amikor csak lehet, használj biztonságos alternatívát: Box::pin, tokio::pin!, vagy ha Unpin a típusod, egyszerű Pin::new-t.

use std::pin::Pin;

fn safe_pin_for_unpin_type(value: &mut i32) -> Pin<&mut i32> {
    // i32 Unpin, tehát ez biztonságos, nincs szükség unsafe-re
    Pin::new(value)
)
}
Figyelem

A fenti kód szándékosan hibás zárójelezést tartalmaz () a } helyett), hogy felhívjam a figyelmet: mindig fordítsd le a saját Pin-nel kapcsolatos kódodat, mielőtt production-be kerülne – a típusrendszer segít, de az elgépelések nem tűnnek el maguktól. A helyes verzió: Pin::new(value) majd }.

2. Self-referenciális struct létrehozása pinning nélkül. Ha saját Future implementációt írsz, ami önmagára mutató referenciákat tartalmaz, sose felejtsd el a PhantomPinned markert és a !Unpin viselkedést. Enélkül a fordító simán megengedi majd a struct mozgatását, és a self-referencia érvénytelenné válik – ez csendes, nehezen debuggolható memóriahiba.

3. Pin<&mut T> visszaadása egy lokális változóra mutatva. Ha egy függvényen belül pinelsz egy lokális változót tokio::pin!-nel vagy a jövőbeli pin!-nel, a kapott Pin<&mut T> élettartama a lokális változóhoz kötött. Nem adhatod vissza a függvényből – ezt a borrow checker amúgy is elkapja, de sokan meglepődnek, amikor life­time hibát kapnak egy pin! makró után.

4. Felesleges Box::pin heap allokáció, amikor stack pinning is elég lenne. Ha statikusan ismert a Future típusa, és nem kell dyn Future-ként tárolnod, a tokio::pin! (vagy majd a std::pin::pin!) mindig hatékonyabb megoldás, mert elkerüli a heap allokációt.

Összefoglalás

A pinning nem öncélú bonyolítás, hanem a self-referenciális async állapotgépek biztonságos kezelésének kulcsa. A Pin<&mut T> garantálja, hogy egy érték nem mozdul el a memóriában, az Unpin trait pedig megkülönbözteti azokat a típusokat, amikre ez a garancia nem is releváns. A mindennapi async/.await kódban ritkán találkozol vele direktben, de amint saját executort, kombinátort vagy dinamikus Future-kezelést írsz, elkerülhetetlen. A közelgő std::pin::pin! makró – ami a tervek szerint az 1.68-as stabil release-ben debütál – tovább egyszerűsíti majd a stack-alapú pinelést, kiváltva sok helyen a tokio::pin!-re való függést. Addig is a Box::pin és a tokio::pin! stabil, jól bevált eszközök, amikkel nyugodtan dolgozhatsz.