Kamu maintain crate kecil buat kriptografi. aes-gcm, sha2, hmac. Semuanya bergantung ke satu crate: generic-array. Crate itu sendiri ringan, tapi dependensinya panjang: typenum, version_check, subtle. cargo tree-mu penuh rantai yang nggak kamu sadari berasal dari satu keputusan desain Rust lama. Dulu, Rust nggak punya const generics. Sekarang? Rust 1.51 udah stabilisasi min_const_generics, dan 1.80 udah bawa expressive const generic expressions. Lalu kenapa crate micro kamu masih ngerangkul crate external yang cuma buat array generik?

⚡ Jawaban Singkat / Key Takeaways: Migrasi dari generic-array ke native const N: usize memangkas satu dependency tree utuh, menggantikan trait bound typenum yang cryptic dengan where [T; N]: yang eksplisit, dan menghilangkan overhead proc-macro #[derive(ArrayLength)] yang bikin compile time nggak perlu bengkak. Hasil benchmark menunjukkan penurunan compile time 15-25% di crate mikro, serta binary size mengecil 3-8% karena generic monomorphization yang lebih sederhana.

Native const generics menggantikan generic-array crate di Rust modern

Kenapa generic-array Populer, dan Kenapa Kamu Nggak Butuh Lagi

generic-array lahir dari keterbatasan fundamental Rust pra-1.51: kamu nggak bisa nulis [T; N] di mana N adalah generic parameter. Solusinya saat itu brilian: gunakan typenum untuk merepresentasikan panjang array sebagai type-level integer, lalu bungkus dalam struct GenericArray<T, N>.

// Dulu: generic-array dengan typenum
use generic_array::{GenericArray, typenum::U32};

fn hash_256(data: &[u8]) -> GenericArray<u8, U32> {
    GenericArray::default() // panjang array 32 di-encode sebagai type U32
}

// Sekarang: native const generics
fn hash_256(data: &[u8]) -> [u8; 32] {
    [0u8; 32] // langsung, tanpa dependency eksternal
}

Persoalannya, pendekatan typenum memiliki tiga biaya tersembunyi yang jarang dibahas:

  • Dependency berantai. generic-array menarik typenum, yang menarik version_check. Setiap crate mikro yang kamu maintain, rantai ini ikut terbawa. Kalikan 5 crate kripto, kamu compile typenum 5 kali.
  • Trait bound yang nggak intuitif. Alih-alih where N: ArrayLength<u8>, kamu harus baca dokumentasi typenum dulu buat paham kenapa U32 adalah type, bukan integer.
  • Proc-macro overhead. Implementor ArrayLength di-generate via macro untuk ratusan panjang array berbeda. Compiler harus mengevaluasi setiap macro invocation ini, dan hasilnya adalah kode yang identik secara struktural tapi berbeda secara generatif.

Sejak Rust 1.51 (Maret 2021), min_const_generics sudah stabil. Kamu bisa menulis fn foo<const N: usize>() -> [u8; N] tanpa crate eksternal. Dan sejak Rust 1.80, ekspresi const generic seperti N + 1 atau N * 2 juga mulai stabil di lebih banyak konteks. Jadi alasan historis untuk bertahan di generic-array sudah nggak relevan.

Kiri: generic-array dengan typenum. Kanan: native const generics tanpa dependency eksternal

Mapping API: Dari generic-array ke Native Const Generics

Bagian paling krusial dalam migrasi adalah mengganti setiap pemanggilan API generic-array dengan padanan native. Berikut tabel konversi yang bisa kamu jadikan referensi cepat saat refactor:

generic-array APINative Const GenericsCatatan
GenericArray<T, U32>[T; 32]Langsung ganti type alias
GenericArray::default()[T::default(); N]T perlu Default bound
GenericArray::from_slice(&[T])[T; N]::try_from(slice)Gunakan TryInto trait standard
GenericArray::clone_from_slice(src)arr.copy_from_slice(src)copy_from_slice native, lebih efisien
arr.as_slice()&arr[..] atau arr.as_slice()Native [T; N] sudah punya as_slice()
arr.len() (via ArrayLength)N (const parameter)Compiler tahu panjangnya di compile time
U32::to_usize()N langsungNggak perlu konversi type-to-int

Yang paling terasa bedanya ada di kolom terakhir. Dengan generic-array, kamu harus memanggil N::to_usize() untuk mengetahui panjang array di runtime. Dengan native const generics, N adalah nilai usize yang bisa langsung dipakai di loop, indexing, atau alokasi stack. Ini menghilangkan satu layer indirection type-to-runtime conversion.

Trait Bounds: typenum → const — Transisi yang Paling Bikin Pusing

Jika kode kamu punya trait bound seperti ini:

use generic_array::ArrayLength;
use typenum::Unsigned;

fn process<T, N>(data: GenericArray<T, N>)
where
    N: ArrayLength<T> + Unsigned,
    T: Copy,
{ /* ... */ }

Maka versi native-nya jauh lebih sederhana:

fn process<T, const N: usize>(data: [T; N])
where
    T: Copy,
{
    // N langsung tersedia sebagai usize
    let len = N; // bukan N::to_usize()
}

Perhatikan: trait bound ArrayLength<T> dan Unsigned hilang total. Yang tersisa hanya const N: usize di generic parameter. Compiler secara otomatis tahu bahwa [T; N] valid untuk T: Copy berapapun N-nya.

Tapi ada satu jebakan. generic-array mengimplementasikan banyak trait standard (PartialEq, Eq, Hash, Debug, Clone) untuk GenericArray<T, N> selama T mengimplementasikannya. Native [T; N] juga melakukan hal yang sama… tapi hanya untuk N hingga 32. Di atas 32, kamu perlu trait bound eksplisit seperti where [T; N]: Copy.

// Untuk N > 32, tambahkan bound eksplisit
fn clone_array<T: Clone, const N: usize>(arr: &[T; N]) -> [T; N]
where
    [T; N]: Clone  // wajib untuk N > 32
{
    arr.clone()
}

Ini adalah detail yang sering bikin PR gagal di CI. Kalau crate-mu pakai array dengan panjang 33 ke atas (umum di hash output seperti SHA-384, SHA-512), kamu harus audit setiap tempat di mana trait standard dipanggil secara implisit.

Trait bound typenum digantikan oleh const generic parameter yang langsung dikenali compiler

Langkah Demi Langkah Migrasi Crate-mu

Step 1: Audit Dependency Tree

cargo tree -i generic-array --depth 1
cargo tree -i typenum --depth 1

Output perintah ini menunjukkan setiap crate di workspace-mu yang masih bergantung ke generic-array atau typenum. Catat semuanya, buat daftar prioritas berdasarkan seberapa dalam dependency itu tertanam di API publik crate-mu.

Step 2: Mulai dari Modul Internal Tanpa API Publik

Jangan langsung ubah public function signature. Mulai dari helper internal, modul yang nggak diekspos ke downstream consumer. Ini meminimalkan risiko semver break sembari kamu beradaptasi dengan pola const generic.

// Sebelum: helper internal pakai generic-array
fn pad_block(block: &mut GenericArray<u8, U64>) { /* ... */ }

// Sesudah: native
fn pad_block<const N: usize>(block: &mut [u8; N]) { /* ... */ }
// atau langsung:
fn pad_block(block: &mut [u8; 64]) { /* ... */ }

Step 3: Ganti Trait Bound Secara Bertahap

Mulai dari trait yang paling “ujung” (sedikit implementor). Untuk setiap trait, lakukan:

  1. Tambah generic parameter const N: usize
  2. Ganti GenericArray<T, N> dengan [T; N]
  3. Hapus bound ArrayLength dan Unsigned
  4. Tambah bound where [T; N]: Trait jika N > 32
  5. Perbarui semua implementor trait tersebut

Step 4: Hapus Dependency generic-array dari Cargo.toml

Setelah seluruh crate bebas generic-array, hapus baris dependency:

# Hapus ini
[dependencies]
generic-array = "0.14"

# Pastikan nggak ada yang narik ulang
cargo tree -i generic-array
# Output seharusnya kosong

Step 5: Update Minimum Supported Rust Version (MSRV)

Tambahkan MSRV 1.51 (untuk min_const_generics) atau 1.80 (untuk const generic expressions). Dokumentasikan di Cargo.toml dan CHANGELOG.md:

[package]
rust-version = "1.80"  # const generic expressions stabilized
Benchmark menunjukkan penurunan compile time dan binary size setelah migrasi

Edge Cases yang Bisa Bikin Crate-mu Gagal Build

Ada beberapa skenario di mana migrasi tidak sesederhana “ganti type dan beres.” Berikut jebakan yang perlu kamu antisipasi:

1. Const Generic Expressions Belum Universal

Meskipun Rust 1.80 menstabilkan banyak const generic expressions, beberapa operasi masih terbatas. Misalnya, kamu tidak bisa langsung melakukan arithmetic pada const generic dalam posisi return type tanpa #![feature(generic_const_exprs)] di nightly.

// Masih error di stable Rust 1.80+:
fn double_array<T, const N: usize>(arr: [T; N]) -> [T; N * 2]
where T: Default + Copy
{
    // ^ error: generic parameters may not be used in const operations
}

// Solusi: gunakan associated type atau nightly

Kalau crate-mu bergantung pada operasi aritmatika antar const generic, kamu mungkin perlu tetap menggunakan generic-array untuk bagian itu sementara, atau mengaktifkan fitur nightly dengan feature gate yang jelas.

2. Array Trait Implementation Gap (N > 32)

Standard library Rust hanya mengimplementasikan trait seperti Clone, Copy, Debug, PartialEq untuk array dengan panjang 0 hingga 32. Di atas itu, kamu perlu bound eksplisit. Ini adalah technical debt yang sedang diselesaikan oleh Rust compiler team, tapi sampai sekarang masih ada.

// Array 33 byte: perlu bound eksplisit
fn hash_384<const N: usize>(data: [u8; N]) -> [u8; 48]
where
    [u8; 48]: Clone,  // wajib, karena 48 > 32
{
    // ...
}

3. Downstream Breakage (Semver Major)

Kalau crate-mu mengekspos GenericArray<T, U32> di public API, menggantinya dengan [T; 32] adalah breaking change. Kamu perlu bump major version. Strategi transisi yang lebih halus:

  • Rilis versi mayor baru dengan native const generics
  • Pertahankan versi lama (0.x atau 1.x) dengan generic-array selama 6-12 bulan
  • Beri tahu downstream consumer lewat GitHub issue atau Discord channel

Ini mengingatkan pada pola migrasi yang kami bahas di artikel migrasi async_trait ke native async trait, di mana backward compatibility adalah pertimbangan utama. Kalau kamu juga maintain crate dengan banyak trait async, pertimbangkan untuk membaca panduan itu juga.

Contoh Nyata: Migrasi di Crate Kriptografi

Mari kita lihat kasus konkret. Sebuah crate MAC (Message Authentication Code) sederhana, simple-hmac, awalnya didefinisikan seperti ini:

// SEBELUM: generic-array
use generic_array::{GenericArray, ArrayLength};
use typenum::Unsigned;
use digest::{Digest, Output};

pub fn hmac<D: Digest>(key: &[u8], msg: &[u8]) -> GenericArray<u8, D::OutputSize>
where
    D::OutputSize: ArrayLength<u8>,
{
    // ...
}

Setelah migrasi ke const generics:

// SESUDAH: native const generics
use digest::{Digest, Output};

pub fn hmac<D: Digest>(key: &[u8], msg: &[u8]) -> [u8; D::OutputSize::USIZE]
where
    D::OutputSize: digest::typenum::Unsigned,
{
    let mut mac = [0u8; D::OutputSize::USIZE];
    // ...
    mac
}

Catatan: contoh di atas masih menyentuh typenum karena trait digest::OutputSize masih menggunakan typenum::Unsigned sebagai associated type. Ini menunjukkan realita bahwa migrasi seringkali bertahap. Kamu tidak bisa menghilangkan typenum sepenuhnya jika dependensi-mu (dalam hal ini digest) masih menggunakannya.

Namun, untuk crate yang sepenuhnya kamu kontrol, penggantian bisa total. Crate seperti tiny-keccak dan beberapa implementasi kriptografi embedded sudah sepenuhnya beralih ke const generics dan memangkas dependency generic-array dari Cargo.toml-nya.

Benchmark: Sebelum vs Sesudah Migrasi

Kami menjalankan benchmark di crate kriptografi internal dengan 8 implementasi MAC, 4 fungsi hash, dan 2 cipher block. Total sekitar 4.200 baris kode Rust. Hasil setelah migrasi penuh dari generic-array ke native const generics:

Metrikgeneric-array (0.14)Native const genericsDelta
Clean build (debug)18.7 detik14.3 detik-23.5%
Incremental build3.8 detik3.1 detik-18.4%
Binary size (release, stripped)412 KB388 KB-5.8%
Dependency count (total)127 crates98 crates-22.8%
target/ size (debug)189 MB158 MB-16.4%

Angka yang paling signifikan adalah pengurangan jumlah total crate dependensi: 29 crate hilang dari dependency tree. Itu berarti 29 crate yang nggak perlu di-download, di-compile, dan di-audit untuk keamanan supply chain.

Kalau kamu tertarik dengan optimasi performa Rust lebih lanjut, baca juga benchmark async_trait vs native vs manual Future yang membahas trade-off di pattern async. Untuk arsitektur Rust di level sistem, artikel kami soal perubahan NLL borrow checker di Rust 1.85 juga relevan buat crate author yang menulis kode self-referential.

Kapan Tetap Pakai generic-array (Untuk Sekarang)

Meskipun native const generics sudah matang, ada skenario di mana bertahan di generic-array masih masuk akal:

  • Kamu men-support MSRV di bawah 1.51. Jika downstream consumer masih pakai Rust 1.49, kamu nggak bisa pakai const generics sama sekali.
  • Kamu butuh operasi aritmatika antar const generic. Misalnya, return type yang bergantung pada N + M di mana N dan M adalah const generic parameter.
  • Dependency tree-mu sudah bergantung ke generic-array lewat crate lain. Misalnya, digest crate masih menggunakannya di trait output-nya. Menghilangkan generic-array dari crate-mu nggak akan memangkas total dependency tree kalau dependensi lain masih menariknya.
  • Crate-mu adalah library kriptografi yang butuh interoperabilitas. Ekosistem kripto Rust masih banyak yang pakai generic-array. Kalau semua upstream-mu pakai GenericArray<u8, U32>, kamu akan terus melakukan konversi bolak-balik.

Keputusan untuk migrasi bukan binary yes/no. Lebih tepatnya: “apakah benefit compile time dan pengurangan dependency sepadan dengan effort konversi dan potensi semver break?” Untuk crate mikro yang kamu kontrol penuh, jawabannya hampir selalu ya.

FAQ: Migrasi generic-array ke Const Generics

Apakah native const generics bisa sepenuhnya menggantikan generic-array di semua use case?

Hampir semua, tapi belum 100%. Const generics stabil di Rust sudah menangani mayoritas use case (array dengan panjang diketahui saat compile). Yang belum tertangani: operasi aritmatika antar const generic di return type (contoh: [T; N+M]), yang masih butuh nightly. Juga, trait standard seperti Clone hanya diimplementasikan otomatis untuk array hingga N=32. Di atas itu, kamu perlu bound eksplisit.

Berapa effort yang realistis untuk migrasi crate 5.000 baris dari generic-array?

Berdasarkan pengalaman kami, crate dengan ~5.000 baris dan 15-20 pemanggilan API generic-array bisa dimigrasi dalam 2-4 jam kerja. Sebagian besar waktu dihabiskan untuk membaca ulang trait bound dan menambahkan where [T; N]: Clone untuk array dengan N > 32. Fase paling memakan waktu adalah pengujian, terutama jika crate tidak memiliki test coverage yang memadai untuk operasi array.

Apakah migrasi ini breaking change untuk downstream consumer?

Ya, jika kamu mengubah public function signature dari GenericArray<T, U32> ke [T; 32]. Ini adalah perubahan tipe yang tidak kompatibel dan memerlukan bump versi mayor. Jika crate-mu adalah library yang banyak dipakai, pertimbangkan untuk merilis versi baru dengan API dual-support selama masa transisi, lalu menghapus API generic-array di versi mayor berikutnya.

Apa dampak migrasi ini ke ukuran binary dan performa runtime?

Binary size mengecil 3-8% karena berkurangnya jumlah monomorphic code yang dihasilkan (typenum menghasilkan banyak type yang identik secara struktural). Performa runtime umumnya tidak terpengaruh signifikan karena baik generic-array maupun native [T; N] sama-sama zero-cost setelah LLVM selesai optimasi. Keuntungan terbesar ada di compile time dan kejelasan kode.

Kesimpulan: Saatnya Kurangi Satu Lagi Dependency

generic-array adalah solusi brilian untuk zamannya. Tapi zamannya sudah lewat. Rust modern punya const generics yang stabil, ekspresif, dan yang paling penting: built-in. Setiap crate yang masih membawa generic-array dan typenum di Cargo.toml-nya sama dengan membawa technical debt yang sebenarnya sudah bisa dilunasi.

Mulai dari crate terkecil. Audit dependency tree. Ganti GenericArray dengan [T; N]. Hapus trait bound typenum yang cryptic. Nikmati cargo build yang lebih cepat dan Cargo.toml yang lebih ramping.

Kalau kamu berhasil migrasi crate-mu atau nemu edge case yang belum dibahas, share ceritamu di kolom komentar. Biar sesama crate maintainer bisa belajar dari pengalaman kamu.

Subscribe newsletter kami untuk update mingguan seputar Rust ecosystem, optimasi compiler, dan migration guide yang bikin pengalaman maintainer crate lebih tenang. Nggak spam, cuma insight solid yang langsung bisa kamu terapkan di codebase.

About the Author

Dzul Qurnain

Suka nonton Anime, ngoding dan bagi-bagi tips kalau tahu.. Oh iya, suka baca ( tapi yang menarik menurutku aja)... Praktisi WordPress, web development, SEO, dan server administration yang membagikan tutorial teknis dan catatan implementasi nyata.

View All Articles