Kamu nulis event loop untuk embedded device. Ada struct SensorReader yang pegang buffer internal dan futures yang reference buffer itu sendiri. Logikanya aman. Semua borrow selesai sebelum drop. Tapi cargo check teriak: “cannot borrow self as mutable because it is also borrowed as immutable”. Padahal konteks runtime-nya jelas nggak bakal konflik.
Kamu bukan yang pertama frustrasi. Pola self-referential di async block adalah pain point bertahun-tahun buat developer embedded dan systems programmer yang nulis event loop, interrupt handler, dan async state machine. Rust 1.85 akhirnya bawa perbaikan NLL (Non-Lexical Lifetimes) yang signifikan, dan borrow checker sekarang cukup pintar buat menerima pola yang sebelumnya ditolak mentah-mentah.
⚡ Jawaban Singkat / Key Takeaways: Rust 1.85 memperkenalkan analisis liveness berbasis borrow yang lebih granular di dalam async block, memungkinkan compiler mengenali bahwa reference ke self tidak hidup berdampingan dengan mutable borrow di titik yang sama secara runtime. Ini artinya self-referential structs di async context kini bisa compile tanpa unsafe atau workaround seperti Pin::as_mut projection yang kompleks.
Kenapa Self-Referential Struct di Async Block Susah Banget
Self-referential struct adalah struktur data yang salah satu field-nya memegang reference ke field lain dalam struct yang sama. Contoh sederhana di context event loop sistem tertanam:
// Pola umum di embedded event loop
struct SpiDriver {
buffer: [u8; 256],
dma: Option<DmaTransfer<'?, &'? [u8]>>,
// ^^^ reference ke buffer di atas!
}
Di Rust, ini problematik karena borrow checker melacak lifetime secara statis. Kalau dma pinjam buffer, maka SpuDriver nggak bisa dipindah (move) karena reference internal-nya invalid. Solusi klasik: Pin<Self>.
Tapi masalahnya: di async block, compiler generate state machine otomatis. State machine ini penuh dengan implisit move antar yield point. Jadi meskipun kamu udah bungkus struct dengan Pin, compiler lama tetap nggak bisa membuktikan bahwa borrow ke self nggak overlapping.
Apa yang Berubah di NLL Rust 1.85 untuk Async Block
NLL sendiri udah hadir sejak Rust 2018. Tapi NLL versi awal cuma bekerja di scope fungsi biasa. Di dalam async block, analisisnya masih konservatif karena generator/state machine transform terjadi sebelum borrow checking.
Rust 1.85 menambahkan dua perbaikan kunci:
- Borrow liveness tracking lintas yield point: Compiler sekarang bisa membuktikan bahwa sebuah borrow ke field
self.buffertidak “hidup” di yield point yang sama di manaselfdi-mutable-borrow. Jadi kalau pola-mu aman secara temporal, compiler akan menerima. - Two-phase borrow di async context: Sebelumnya, two-phase borrow (mutable reference yang awalnya nggak aktif lalu jadi aktif) cuma berlaku di function body. Sekarang berlaku di async block juga, yang artinya pola “reserve mutable slot dulu, isi nanti” bisa jalan tanpa trik
Option::take.
Simpelnya: borrow checker sekarang baca async state machine sebagai flow temporal, bukan cuma struktur statis.
Contoh Nyata: Pola yang Dulu Error, Sekarang Compile
Event Loop Driver SPI (Sebelum vs Sesudah)
// Sebelum Rust 1.85: DITOLAK
impl SpiDriver {
async fn transfer(&mut self, data: &[u8]) {
self.buffer[..data.len()].copy_from_slice(data);
let transfer = self.dma.start(&self.buffer[..data.len()]);
// ERROR: cannot borrow `self.buffer` as immutable
// because `self` is also borrowed as mutable
transfer.await;
}
}
// Rust 1.85: DITERIMA
// Compiler paham bahwa `&self.buffer` bersih sebelum
// mutable borrow berikutnya aktif di yield point
Interrupt Handler dengan Deferred Work
struct UartRx {
ring: RingBuffer<u8>,
pending: Option<ReadFuture<'?, &'? RingBuffer<u8>>>,
}
impl UartRx {
async fn read_byte(&mut self) -> u8 {
loop {
if let Some(byte) = self.ring.pop() {
return byte;
}
// Dulu error: self.ring dipinjam immutable
// sementara self di-mutable-borrow
self.pending = Some(self.wait_for_interrupt());
self.pending.as_mut().unwrap().await;
}
}
}
Pola ini umum banget di RTIC, Embassy, dan framework async embedded lain. Sebelum 1.85, kamu terpaksa pakai unsafe atau refactor ke channel-based architecture yang lebih boros memory.
Nggak Semua Pola Tiba-Tiba Jadi Aman
Jangan ekspektasi setiap self-referential async block langsung compile. Ada batasannya:
- Borrow harus tidak tumpang tindih secara temporal. Kalau reference dan mutable borrow benar-benar hidup bersamaan di yield point yang sama, compiler tetap menolak.
- Move antar yield point masih diawasi ketat. Kalau kamu
.awaitdi tengah borrow ke&self, compiler akan cek apakahselfbisa dipindah saat itu. - Pin projection tetap diperlukan untuk self-referential murni. NLL improvement ini mengurangi false positive, bukan menghilangkan kebutuhan
Pin.
Intinya: borrow checker sekarang lebih akurat membaca runtime flow async block kamu. Tapi dia tetap nggak kompromi soal soundness.
Dampak Langsung ke Ekosistem Embedded Rust
Framework embedded async seperti Embassy dan RTIC selama ini harus menyediakan abstraction layer yang menutupi keterbatasan borrow checker. Akibatnya: macro tebal, unsafe block terselubung, dan binary size yang nggak optimal.
Dengan NLL 1.85, library author bisa:
- Menghapus workaround unsafe di modul interrupt handler
- Mengurangi boxing dan alokasi heap yang sebelumnya diperlukan buat “mengelabui” borrow checker
- Menyederhanakan API yang sebelumnya memaksa user ke pola callback-based
Di sisi lain, menurut Rust Release Blog, tim compiler mengonfirmasi bahwa regresi compile time dari analisis tambahan ini di bawah 2% untuk kebanyakan codebase. Jadi peningkatan akurasi nggak dibayar dengan compile time yang lebih lambat.
Cara Mulai Manfaatkan NLL Baru di Proyek Kamu
- Update toolchain:
rustup update stabledan pastikan versi >= 1.85.0 - Cek ulang kode yang sebelumnya pakai
unsafeuntuk self-referential pattern. Coba hapusunsafeblock-nya dan lihat apakah compiler menerima. - Refactor workaround lama seperti
Option::take,RefCell, atauunsafepointer cast yang dipasang cuma buat menyenangkan borrow checker. - Test di hardware target: embedded code punya surface area bug yang lebih kecil setelah refactor karena unsafe block berkurang.
- Kalau masih ditolak, periksa apakah memang borrow-mu overlapping secara temporal. Jangan dipaksa
unsafe.
Baca juga pengalaman kami soal memory-safe backend services dengan Rust dan migrasi async_trait ke native AFIT di Rust 1.85 untuk konteks lebih luas soal ekosistem async Rust saat ini.
Pola yang Kini Bisa Tanpa unsafe: Referensi Cepat
| Pola | Sebelum 1.85 | Rust 1.85 |
|---|---|---|
| Self-referential borrow di async fn | Error (false positive) | Compile ✅ |
&self.field + &mut self di yield point beda | Error | Compile ✅ |
| Two-phase borrow di async block | Terbatas | Full support ✅ |
| Borrow ke field yang sama, yield point beda | Error | Compile ✅ |
| True overlapping borrow (memang unsafe) | Error | Error (tetap ditolak) |
FAQ: NLL Rust 1.85 untuk Async Block
Self-referential struct adalah struct yang salah satu field-nya menyimpan reference ke field lain dalam struct yang sama. Di async block, compiler generate state machine yang penuh implicit move antar yield point. Sebelum 1.85, borrow checker tidak bisa membuktikan bahwa reference internal tidak overlapping dengan mutable borrow di titik yang berbeda secara temporal, jadi langsung menolak meskipun secara runtime aman.
Tidak. Hanya pola yang borrow-nya tidak tumpang tindih secara temporal yang kini diterima. Kalau reference dan mutable borrow benar-benar hidup bersamaan di yield point yang sama, compiler tetap menolak. NLL 1.85 mengurangi false positive, bukan menghilangkan semua batasan borrow checking.
Ya, Pin tetap diperlukan untuk memastikan struct tidak dipindah (move) setelah reference internal dibuat. Perbaikan NLL di 1.85 fokus pada analisis borrow sebelum Pin berperan di runtime. Jadi Pin masih jadi bagian wajib dari pola self-referential, tapi kamu tidak perlu unsafe workaround tambahan untuk menyenangkan borrow checker.
Framework async embedded seperti Embassy dan RTIC adalah beneficiary terbesar. Keduanya banyak menggunakan pola self-referential di driver hardware, interrupt handler, dan DMA transfer yang sebelumnya memerlukan unsafe workaround. Library author kini bisa menghapus banyak unsafe block dan menyederhanakan API.
Kesimpulan: Borrow Checker Makin Cerdas, Kode Makin Bersih
Rust 1.85 bukan revolusi, tapi evolusi penting buat siapa pun yang nulis async di level sistem. Borrow checker sekarang cukup pintar membaca maksud kode-mu, bukan cuma struktur statisnya. Buat developer embedded yang udah lama bergulat dengan unsafe workaround di event loop dan interrupt handler, ini sinyal bagus: Rust makin ramah ke bare-metal tanpa kehilangan garansi memory safety-nya.
Kalau kamu nemu pola yang dulu error dan sekarang compile, share di komentar. Kita semua belajar dari battle story masing-masing.
Subscribe newsletter kami buat update mingguan soal Rust systems programming, optimasi compiler, dan trik embedded yang jarang dibahas di tutorial mainstream. Nggak spam, cuma insight yang bikin firmware kamu lebih bersih dan cepat.
