Tutorial

Zig Pointer Stability untuk ArrayList: Mengamankan Referensi saat Kapasitas Tumbuh

Zig Pointer Stability untuk ArrayList: Mengamankan Referensi saat Kapasitas Tumbuh

Pointer adalah sumber kebahagiaan sekaligus sumber bencana dalam bahasa pemrograman sistem. Di Zig, salah satu jebakan klasik adalah menyimpan pointer ke elemen sebuah ArrayList, lalu list tersebut tumbuh dan realokasi memory terjadi, sehingga pointer lama menunjuk ke memory yang sudah tidak valid. Bug seperti ini sering muncul diam-diam dan baru meledak saat aplikasi berjalan lama.

Kabar baiknya, Zig baru saja memperkenalkan fitur pointer stability untuk std.ArrayList. Fitur ini memungkinkan developer mengunci alokasi memory list selama pointer masih dipakai, sehingga referensi tetap valid meskipun kapasitas list bertambah. Artikel ini membahas cara kerjanya, contoh kasus yang sering salah, dan bagaimana menggunakannya dengan benar.

Masalah Klasik: Pointer yang Menjadi Tidak Valid

ArrayList di Zig bekerja seperti vector di bahasa lain: data disimpan dalam satu blok memory yang kontigu. Selama kapasitas mencukupi, penambahan elemen tidak mengubah lokasi blok memory tersebut. Tetapi begitu kapasitas penuh dan elemen baru masuk, ArrayList harus mengalokasikan blok memory yang lebih besar, menyalin data lama ke lokasi baru, dan membebaskan blok lama.

Masalahnya muncul ketika kode lain masih memegang pointer ke elemen di dalam blok lama. Setelah realokasi, pointer tersebut menunjuk ke memory yang sudah dibebaskan atau ke lokasi yang sekarang diisi data lain. Membaca atau menulis melalui pointer yang sudah tidak valid adalah undefined behavior, dan di Zig, ini bisa menyebabkan segfault atau korupsi data yang sulit dilacak.

Jebakan ini sangat umum terjadi pada kode yang menyimpan slice atau pointer ke isi list, lalu melanjutkan operasi lain yang menambahkan elemen ke list yang sama.

Contoh Bug yang Sering Terjadi

Perhatikan contoh berikut, yang dimodifikasi dari devlog resmi Zig. Terdapat dua ArrayList: satu menyimpan isi input, dan satu lagi menyimpan potongan-potongan baris yang menunjuk ke isi list pertama.

const std = @import("std");

const Context = struct {
    history: std.ArrayList(u8),
    lines: std.ArrayList([]const u8),

    fn parse(ctx: *Context, allocator: std.mem.Allocator, input: []const u8) !void {
        const slice = try ctx.history.addManyAsSlice(allocator, input.len);
        @memcpy(slice, input);

        var it = std.mem.tokenizeScalar(u8, slice, '
');
        while (it.next()) |line| {
            try ctx.lines.append(allocator, line);
        }
    }
};

Kode ini terlihat masuk akal, tetapi menyimpan bug yang halus. Elemen dari ctx.lines bergantung pada lokasi ctx.history.items. Masalahnya, lokasi ctx.history.items bisa berubah setiap kali ctx.history perlu tumbuh melebihi kapasitasnya.

Kalau input cukup panjang sehingga history harus melakukan realokasi di tengah proses tokenisasi, semua slice di lines yang sudah dibuat akan menunjuk ke memory lama yang tidak lagi valid. Hasilnya bisa segfault, atau lebih buruk lagi, data yang korup tanpa error yang jelas.

Reproduksi bug ini cukup mudah dengan input yang sengaja dibuat panjang, sehingga memaksa history tumbuh lebih dari sekali. Dalam kasus nyata, bug seperti ini sering muncul hanya pada input tertentu, membuatnya sangat sulit dilacak.

Pointer Stability sebagai Solusi

Zig sudah memiliki mekanisme serupa untuk Hash Map sejak 2024. Kini, teknik yang sama dibawa ke std.ArrayList. Idenya sederhana: developer bisa mengunci pointer list, yang menjamin bahwa alokasi memory tidak akan berpindah selama terkunci.

Cara pakainya:

  • Panggil lockPointers() saat pertama kali menyimpan pointer ke elemen atau slice yang didukung oleh ArrayList.
  • Panggil unlockPointers() saat pointer tersebut tidak lagi dibutuhkan.

Sementara pointer terkunci, ArrayList tidak akan melakukan realokasi. Operasi yang membutuhkan kapasitas tambahan akan gagal dengan error alih-alih diam-diam memindahkan memory. Ini adalah pendekatan fail-safe: kalau kode mencoba menambahkan elemen saat pointer terkunci dan kapasitas penuh, kode tersebut akan tahu ada yang salah, bukan mengalami segfault misterius.

Dengan menggunakan lock dan unlock, contoh bug di atas bisa diperbaiki dengan mengunci pointer history sebelum tokenisasi dimulai dan membukanya setelah semua slice selesai dipakai.

Cara Memperbaiki Kode yang Bermasalah

Dengan pointer stability, perbaikan untuk contoh di atas menjadi langsung. Kunci pointer history sebelum mengambil slice dan membuat token, lalu buka kembali setelah semua baris selesai diproses:

fn parse(ctx: *Context, allocator: std.mem.Allocator, input: []const u8) !void {
    ctx.history.lockPointers();
    defer ctx.history.unlockPointers();

    const slice = try ctx.history.addManyAsSlice(allocator, input.len);
    @memcpy(slice, input);

    var it = std.mem.tokenizeScalar(u8, slice, '
');
    while (it.next()) |line| {
        try ctx.lines.append(allocator, line);
    }
}

Dengan lockPointers() di awal dan defer unlockPointers(), kode menjamin bahwa history tidak akan berpindah selama seluruh fungsi berjalan. Semua slice yang disimpan ke lines tetap valid sampai fungsi selesai.

Perhatikan bahwa kunci ini bersifat manual dan berbasis scope. Developer bertanggung jawab untuk memastikan masa hidup pointer tidak melebihi masa kunci. Kalau pointer disimpan lebih lama dari kunci, masalah yang sama bisa muncul kembali.

Kapan Harus Memakai lockPointers

Pointer stability bukan fitur yang harus dipakai di mana-mana. Ada biaya: selama pointer terkunci, ArrayList tidak bisa tumbuh, sehingga operasi append yang membutuhkan kapasitas baru akan gagal. Memakai lock tanpa alasan akan membuat kode lebih kaku tanpa manfaat.

Gunakan lockPointers() ketika:

  • Anda menyimpan pointer atau slice ke elemen list untuk dipakai di kemudian hari.
  • Anda membuat slice yang menunjuk ke dalam list, lalu list tersebut mungkin bertambah sebelum slice selesai dipakai.
  • Anda membutuhkan jaminan bahwa alamat memory elemen tetap stabil selama operasi tertentu.

Jangan gunakan ketika:

  • Pointer hanya dipakai seketika dan tidak melewati batas operasi append.
  • List sudah pasti tidak akan tumbuh selama pointer hidup.
  • Anda lebih suka menyalin data daripada memegang referensi.

Seperti banyak fitur di bahasa pemrograman sistem, pointer stability adalah alat yang tepat untuk masalah tertentu, bukan solusi universal.

Hubungannya dengan Memory Safety di Zig

Salah satu nilai jual Zig adalah kendali penuh atas memory tanpa garbage collector. Namun, kendali penuh juga berarti tanggung jawab penuh. Fitur seperti pointer stability membantu menutup salah satu celah paling umum dalam pola pemrograman dengan list dinamis.

Pendekatan Zig di sini menarik: alih-alih melarang pola yang berbahaya, bahasa menyediakan mekanisme untuk membuat pola tersebut aman secara eksplisit. Developer yang paham apa yang mereka lakukan bisa memakai pola yang sama dengan jaminan yang lebih kuat, tanpa harus menulis ulang struktur data mereka.

Ini sejalan dengan filosofi Zig yang lebih memilih eksplisit daripada implisit: kalau kamu menyimpan pointer, kamu harus tahu apa yang kamu lakukan, dan sekarang bahasa memberimu alat untuk melakukannya dengan benar.

Pola Lain yang Sering Menyimpan Bug Serupa

Bug realokasi tidak hanya terjadi pada ArrayList yang menyimpan slice ke dirinya sendiri. Ada beberapa pola serupa yang sering muncul di codebase Zig dan bahasa sistem lain:

  • Menyimpan pointer ke elemen list, lalu menambah elemen ke list yang sama di tempat lain.
  • Menahan slice dari buffer sementara buffer tersebut diisi ulang dengan data baru.
  • Meneruskan pointer ke elemen list ke fungsi lain yang secara tidak langsung memicu pertumbuhan list.
  • Menggabungkan dua list di mana slice dari list pertama dipakai setelah operasi penggabungan.

Pola-pola ini sulit dilacak karena tidak selalu terlihat di lokasi yang sama. Pointer dibuat di satu fungsi, disimpan di struct, dan dipakai di fungsi lain yang jauh. Ketika bug muncul, stack trace sering kali tidak menunjukkan akar masalahnya, karena segfault terjadi di tempat pemakaian pointer, bukan di tempat realokasi terjadi.

Karena itu, selain memakai pointer stability, ada baiknya menerapkan disiplin tambahan: minimalkan penyimpanan pointer jangka panjang ke dalam list dinamis, dan kalau harus menyimpan, dokumentasikan dengan jelas masa hidup pointer tersebut. Kombinasi alat yang tepat dan kebiasaan yang hati-hati adalah pertahanan terbaik.

Membandingkan dengan Pendekatan Bahasa Lain

Masalah pointer yang tidak valid akibat realokasi bukan hal baru, dan setiap bahasa punya caranya sendiri untuk menanganinya. Di C, developer harus mengelola semuanya manual dan sering gagal. Di C++, std::vector memiliki konsep iterator invalidation yang serupa, dan aturannya dikenal rumit. Di Rust, borrow checker mencegah pola ini pada level kompilasi, tetapi dengan biaya ergonomi.

Pendekatan Zig dengan lockPointers menarik karena berada di tengah-tengah. Tidak ada borrow checker yang membatasi pola pemrograman, tetapi bahasa menyediakan mekanisme eksplisit untuk mengunci stabilitas memory saat dibutuhkan. Developer tetap memegang kendali, tetapi sekarang punya alat yang terdokumentasi untuk menutup jebakan yang umum.

Ini sejalan dengan filosofi Zig: percaya pada developer, tetapi berikan alat yang membuat kesalahan umum lebih sulit terjadi. Pointer stability bukan pengganti kehati-hatian, tetapi membuat kehati-hatian itu lebih mudah dipraktikkan.

Perlu dicatat juga bahwa fitur ini masih relatif baru di std.ArrayList, dan pola pemakaiannya akan semakin matang seiring waktu. Komunitas Zig sedang mendokumentasikan kasus-kasus penggunaan nyata, dan developer yang mengadopsi lebih awal akan membantu membentuk praktik terbaik. Kalau menemukan pola yang menarik atau jebakan yang belum terdokumentasi, berkontribusi ke dokumentasi atau forum komunitas adalah cara yang baik untuk berbagi pengalaman. Pada akhirnya, fitur seperti ini hanya sebaik pola pemakaian yang berkembang di komunitas, dan setiap developer yang memakai lockPointers secara disiplin ikut memperkuat ekosistem tersebut.

Bagi yang baru mulai belajar Zig, fitur seperti ini juga menjadi contoh bagus tentang bagaimana bahasa sistem modern mendekati masalah memory safety: bukan dengan melarang pola tertentu, tetapi dengan memberi alat yang jelas untuk membuat pola tersebut aman. Memahami alasan di balik desain fitur sama berharganya dengan memahami cara memakainya.

Kesimpulan

Pointer stability untuk ArrayList adalah tambahan yang kecil tetapi sangat berguna di Zig. Fitur ini mengubah jebakan keamanan memory yang umum menjadi pola yang terdokumentasi dan bisa diverifikasi: kunci pointer, kerjakan, buka kembali.

Bagi developer Zig yang sering bekerja dengan list dinamis, fitur ini layak dipelajari dan dipakai secara konsisten. Bug realokasi adalah salah satu bug yang paling sulit dilacak di bahasa pemrograman sistem, dan mencegahnya sejak awal jauh lebih murah daripada men-debug segfault yang muncul sekali dalam seminggu.

Mulai dari kode yang menyimpan slice ke list sambil terus menambah elemen, evaluasi apakah pointer stability bisa diterapkan, dan biasakan memakai lockPointers saat jaminan stabilitas dibutuhkan. Investasi kecil ini bisa menyelamatkan berjam-jam debugging di kemudian hari.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.