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.
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-arraymenariktypenum, yang menarikversion_check. Setiap crate mikro yang kamu maintain, rantai ini ikut terbawa. Kalikan 5 crate kripto, kamu compiletypenum5 kali. - Trait bound yang nggak intuitif. Alih-alih
where N: ArrayLength<u8>, kamu harus baca dokumentasitypenumdulu buat paham kenapaU32adalah type, bukan integer. - Proc-macro overhead. Implementor
ArrayLengthdi-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.
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 API | Native Const Generics | Catatan |
|---|---|---|
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 langsung | Nggak 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.
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:
- Tambah generic parameter
const N: usize - Ganti
GenericArray<T, N>dengan[T; N] - Hapus bound
ArrayLengthdanUnsigned - Tambah bound
where [T; N]: Traitjika N > 32 - 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
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.xatau1.x) dengangeneric-arrayselama 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:
| Metrik | generic-array (0.14) | Native const generics | Delta |
|---|---|---|---|
| Clean build (debug) | 18.7 detik | 14.3 detik | -23.5% |
| Incremental build | 3.8 detik | 3.1 detik | -18.4% |
| Binary size (release, stripped) | 412 KB | 388 KB | -5.8% |
| Dependency count (total) | 127 crates | 98 crates | -22.8% |
| target/ size (debug) | 189 MB | 158 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 + Mdi mana N dan M adalah const generic parameter. - Dependency tree-mu sudah bergantung ke
generic-arraylewat crate lain. Misalnya,digestcrate masih menggunakannya di trait output-nya. Menghilangkangeneric-arraydari 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 pakaiGenericArray<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
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.
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.
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.
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.
