Topik ini membahas Foreign Function Interface (FFI), yaitu mekanisme Rust untuk memanggil fungsi yang ditulis dalam bahasa C dan untuk menyerahkan fungsi Rust kepada pemanggil C. Pembahasan mencakup deklarasi fungsi asing melalui blok extern "C", alur kompilasi dan penautan yang melibatkan dua rantai alat sekaligus, pembangkitan deklarasi otomatis dengan bindgen, serta gambaran singkat crate cxx untuk proyek C++. Setelah membaca topik ini, pustaka C yang sudah ada dapat dipanggil dari program Rust melalui langkah yang dapat diulang dan diperiksa.
Jembatan dua bahasa lewat ABI#
Sebagian besar pustaka native yang berumur panjang ditulis dalam C, mulai dari kompresi data hingga enkripsi. Kemampuan memakai pustaka tersebut menentukan seberapa jauh Rust dapat bekerja bersama komponen yang sudah ada alih-alih menulis ulang semuanya. Jembatan antarbahasa ini bekerja di atas ABI (Application Binary Interface), yakni kesepakatan tentang cara argumen diletakkan pada register dan tumpukan, cara nilai kembalian dikembalikan, serta cara simbol dinamai di dalam berkas binari. Kompilator menerjemahkan setiap bahasa menurut ABI yang dipilihnya, dan ABI C menjadi titik temu yang didukung hampir semua bahasa native.
Rust membuka dua arah pada ABI tersebut. Fungsi Rust dapat dibangun agar tampak sebagai fungsi C bagi dunia luar melalui penanda extern "C", dan fungsi C dapat dideklarasikan agar tampak sebagai fungsi Rust biasa di dalam crate. Kedua arah tetap melewati satu batas yang sama: pemeriksaan ownership, borrowing, dan batas memori berhenti pada baris extern "C", karena kode di seberangnya memang tidak pernah dilihat oleh kompilator Rust. Akibatnya setiap pemanggilan fungsi asing wajib berada di dalam blok unsafe, dan keamanan berpindah dari tangan kompilator ke tangan penulis deklarasi.
Memanggil fungsi C dari Rust#
Contoh pada bagian ini memakai pustaka C kecil bernama pustaka dengan dua fungsi: jumlah_kuadrat yang menerima dua int, dan rerata yang menerima pointer ke array int beserta panjangnya. Kombinasi tersebut mewakili tiga pola yang paling sering dijumpai pada FFI nyata, yaitu argumen bernilai sederhana, pointer ke sekumpulan data, dan parameter panjang yang menyertainya. Berkas berikut hanya berisi definisi fungsi tanpa main, karena isinya memang ditujukan menjadi pustaka.
// tidak-dijalankan sendirian oleh cc karena berkas ini pustaka tanpa fungsi main.
int jumlah_kuadrat(int a, int b) {
return a * a + b * b;
}
double rerata(const int *nilai, int panjang) {
long total = 0;
for (int i = 0; i < panjang; i++) {
total += nilai[i];
}
return panjang > 0 ? (double) total / panjang : 0.0;
}Sisi Rust mendeklarasikan kedua fungsi dalam satu blok extern "C". Penanda #[link(name = "pustaka")] memerintahkan penaut (linker) mencari simbol pada arsip statis libpustaka.a atau pustaka bersama libpustaka.so, sedangkan extern "C" memilih aturan ABI C untuk kedua fungsi. Tipe argumen mengikuti padanannya: int menjadi i32, double menjadi f64, dan const int * menjadi *const i32. Panjang array memakai std::os::raw::c_int, karena alias itulah padanan resmi bagi int di sisi C, sehingga kode tidak bergantung pada asumsi bahwa int selalu selebar 32 bit.
// tidak-dijalankan sendirian oleh rustc karena memerlukan penautan ke arsip libpustaka.a.
use std::os::raw::c_int;
#[link(name = "pustaka")]
extern "C" {
fn jumlah_kuadrat(a: i32, b: i32) -> i32;
fn rerata(nilai: *const i32, panjang: c_int) -> f64;
}
fn main() {
let hasil = unsafe { jumlah_kuadrat(3, 4) };
println!("jumlah kuadrat 3 dan 4: {}", hasil);
let nilai = [80, 90, 100];
let rata = unsafe { rerata(nilai.as_ptr(), nilai.len() as c_int) };
println!("rerata tiga nilai : {}", rata);
}Kedua pemanggilan dibungkus unsafe karena kedua fungsi berada di luar jangkauan pemeriksaan Rust. Array nilai dikirim sebagai pointer mentah melalui as_ptr(), dan panjangnya dikonversi menjadi c_int karena usize tidak memiliki padanan pada ABI C. Kedua berkas tidak dapat dikompilasi sendirian karena pemanggil membutuhkan simbol dari pustaka. Oleh karena itu, pembangunan dilakukan lewat satu blok perintah yang menyalin keduanya ke /tmp lalu menjalankan kedua rantai alat secara berurutan.
# salin kedua berkas ke /tmp agar pohon konten tetap bersih
cp pustaka.c pemanggil.rs /tmp/
# kompilasi berkas C menjadi berkas objek
gcc -c /tmp/pustaka.c -o /tmp/pustaka.o
# kemas berkas objek menjadi arsip statis bernama libpustaka.a
ar rcs /tmp/libpustaka.a /tmp/pustaka.o
# kompilasi pemanggil Rust dan tautkan ke arsip tersebut
rustc --edition=2021 /tmp/pemanggil.rs -L /tmp -lpustaka -o /tmp/pemanggil
# jalankan binari hasil penautan
/tmp/pemanggiljumlah kuadrat 3 dan 4: 25
rerata tiga nilai : 90Keluaran tersebut berasal dari satu binari pemanggil yang menggabungkan hasil kerja gcc dan rustc. Urutan perintahnya bukan kebetulan: gcc -c menghasilkan berkas objek (object file) pustaka.o, ar rcs mengemasnya menjadi arsip statis (static library) libpustaka.a, dan rustc mengompilasi pemanggil.rs sambil menyerahkan tahap akhir kepada penaut sistem. Penanda #[link] pada kode dan opsi -L /tmp -lpustaka pada baris perintah saling melengkapi: penanda menyatakan pustaka yang dibutuhkan, sedangkan -L menunjukkan direktori tempat penaut mencarinya. Diagram berikut memetakan pertemuan kedua rantai alat tersebut.
bindgen: deklarasi dari berkas header#
Menyalin deklarasi dari header ke blok extern "C" terasa ringan untuk dua fungsi, tetapi pustaka nyata sering memuat ratusan fungsi beserta struct dan enum yang saling bertaut. Satu tipe yang tersalin salah tidak terdeteksi hingga program berjalan dan membaca memori secara keliru. bindgen menghilangkan pekerjaan ini dengan membaca header C secara langsung dan membangkitkan berkas Rust yang memuat seluruh deklarasi extern, penanda repr(C) untuk struct, serta padanan tipe dari std::os::raw. Berkas masukan berikut adalah header untuk pustaka contoh di atas.
// tidak-dijalankan karena berkas ini hanya deklarasi antarmuka tanpa implementasi.
int jumlah_kuadrat(int a, int b);
double rerata(const int *nilai, int panjang);Pada proyek nyata, bindgen dipanggil dari skrip build.rs agar deklarasi dibangkitkan ulang setiap kali header berubah, sedangkan pemanggilan langsung dari baris perintah cukup untuk sekali coba. Alat ini membutuhkan pustaka libclang karena pembacaan header dilakukan oleh parser clang, bukan oleh bindgen sendiri. Lingkungan pada topik ini tidak menyediakan libclang, sehingga perintah berikut tidak dijalankan.
# tidak-dijalankan karena bindgen memerlukan libclang yang tidak tersedia di lingkungan ini.
bindgen pustaka.h -o pustaka_ffi.rsHasilnya berupa berkas Rust yang isinya sepadan dengan blok extern "C" yang ditulis manual di atas, lengkap dengan dokumentasi asli apabila header menyediakannya. Dengan begitu pustaka C dapat diperbarui tanpa satu baris pun deklarasi Rust yang tersentuh. Pekerjaan manual yang tersisa hanyalah penautan, karena bindgen membangkitkan deklarasi, bukan konfigurasi build.
cxx untuk kode C++#
C++ menambah hal yang tidak dimiliki C: pengubahan nama simbol (name mangling), pengecualian (exception), dan tipe bawaan seperti std::string yang tidak punya padanan ABI sederhana. Blok extern "C" polos tetap dapat memanggil fungsi C++ yang dibungkus extern "C" di sisinya, tetapi pendekatan itu menjadi tidak praktis untuk basis kode yang besar. Crate cxx mengisi celah ini dengan jembatan yang diperiksa tipenya di kedua sisi: penulis menyatakan fungsi dan tipe pada satu blok #[cxx::bridge], lalu cxx membangkitkan perekat Rust dan C++ sekaligus, termasuk penerjemahan String, Vec, std::string, dan std::vector secara otomatis.
// tidak-dijalankan karena cxx adalah crate eksternal di luar lingkungan ini.
#[cxx::bridge]
mod ffi {
extern "C++" {
include!("pustaka.hxx");
fn jumlah_kuadrat(a: i32, b: i32) -> i32;
}
}cxx layak dipakai ketika pustaka yang dipanggil memang ditulis dalam C++, karena seluruh panggilan melewati pemeriksaan tipe lintas bahasa alih-alih memercayakan penyalinan manual. Untuk pustaka C murni, cxx justru menambah beban build tanpa manfaat, dan bindgen beserta extern "C" tetap menjadi jalur yang lebih ringan. Pilihan di antara keduanya ditentukan oleh bahasa pustaka yang dipanggil, bukan oleh selera.
Kesalahan umum#
Mengirim &str sebagai const char *. Potongan &str tidak diakhiri bita nol dan tidak boleh dibebaskan oleh sisi C. Kirim teks melalui CString yang menambah penanda akhir sekaligus memegang kepemilikan, dan baca teks balik dari C melalui CStr sebelum dikonversi menjadi &str apabila isinya UTF-8 yang sah.
Nama penautan yang tidak cocok. Penanda #[link(name = "pustaka")] membuat penaut mencari libpustaka.a atau libpustaka.so. Apabila berkas arsip diberi nama lain, penautan gagal dengan pesan undefined reference meskipun kode Rust sendiri terkompilasi. Samakan nama pada penanda dengan nama berkas arsip, atau nyatakan pustaka seluruhnya lewat opsi -l pada baris perintah.
Menyerahkan data berumur pendek kepada C. Metode as_ptr() hanya meminjamkan alamat tanpa memperpanjang umur pemiliknya. Apabila nilai Rust dijatuhkan selagi C masih memegang pointer ke nilai itu, pointer tersebut menunjuk memori yang sudah dibebaskan, dan pelanggaran ini tidak pernah ditolak kompilator. Pastikan pemilik data hidup lebih lama daripada masa pemakaian di sisi C, atau pindahkan kepemilikan alokasi sepenuhnya ke pustaka.
Ringkasan#
- Blok
extern "C"mendeklarasikan fungsi asing sebagai item Rust, dan setiap pemanggilannya wajib berada dalamunsafekarena jaminan Rust berhenti pada batas ABI. - Pembangunan melibatkan dua rantai alat: gcc menghasilkan berkas objek dan arsip statis, sedangkan rustc mengompilasi pemanggil dan menautkan keduanya melalui
-Lserta-l. - bindgen membangkitkan deklarasi langsung dari header C sehingga penyalinan manual yang rawan salah hilang, dengan syarat libclang tersedia pada mesin pembangun.
- cxx menyediakan jembatan yang diperiksa tipenya untuk C++, dan hanya menjadi pilihan yang tepat apabila pustaka yang dipanggil memang ditulis dalam C++.
Referensi#
- Calling Foreign Functions — The Rustonomicon — Aturan FFI dari sisi Rust beserta contoh deklarasi dan penautan.
- std::os::raw — Rust standard library
— Daftar tipe padanan seperti
c_intdanc_doubleuntuk ABI C. - bindgen — User Guide — Panduan pembangkitan deklarasi Rust dari header C dengan bindgen.
- CXX — Safe interop between Rust and C++ — Dokumentasi resmi crate cxx untuk jembatan dua arah dengan pemeriksaan tipe.