Rust tidak hidup sendirian. Pustaka sistem, driver database, engine game, hingga library kriptografi yang dipakai perusahaan kita kemungkinan besar ditulis dalam C atau C++ puluhan tahun sebelum Rust lahir — dan menulis ulang semuanya bukan pilihan realistis. Rust menjawab kebutuhan ini dengan Foreign Function Interface (FFI): jembatan yang memungkinkan kode Rust memanggil fungsi terkompilasi dari bahasa lain, dan sebaliknya fungsi Rust dipanggil dari bahasa lain. Kata kunci extern adalah pintu jembatan itu, dan seluruh mekanisme bertumpu pada satu kesepakatan sederhana bernama ABI — konvensi bagaimana argumen diletakkan di register dan hasil dikembalikan. Chapter ini memanggil fungsi C sungguhan tanpa dependensi tambahan, mengekspor fungsi Rust untuk dikonsumsi C, lalu menguji keduanya lintas bahasa secara nyata.
extern "C" dan Konvensi ABI #
Dua fungsi dengan nama dan parameter yang sama bisa dipanggil dengan cara yang berbeda total: argumen dimasukkan ke register mana, dalam urutan apa, dan siapa yang membersihkan stack. Aturan mainnya disebut Application Binary Interface (ABI). Kompilator Rust bebas mengoptimalkan pemanggilan antar fungsi Rust karena kedua sisi dikendalikan Rust — tetapi kode C terkompilasi mengikuti aturan ABI miliknya. Penanda extern "C" pada blok deklarasi menyatakan: fungsi-fungsi di dalam blok mengikuti ABI “C”, konvensi hampir-semesta yang menjadi bahasa penghubung antar bahasa sejak puluhan tahun.
Deklarasi extern sendiri tidak memuat implementasi — kita hanya memberi tahu kompilator Rust bahwa “fungsi bernama begini ada di luar sana, dengan bentuk begini”. Tugas menautkan (linking) nama tersebut ke kode aslinya dikerjakan linker pada saat kompilasi akhir.
Memanggil Fungsi C dari Rust #
Contoh paling ringkas yang benar-benar bisa dijalankan tanpa dependensi adalah abs — fungsi nilai mutlak bawaan pustaka C standar yang tersedia di sistem. rustc sudah menautkan libc secara bawaan pada platform ini, jadi tidak ada Cargo.toml atau crate tambahan yang perlu disiapkan:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
let selisih_harga = -5;
let jarak_mutlak = unsafe { abs(selisih_harga) };
println!("Input: {}", selisih_harga);
println!("Nilai mutlak: {}", jarak_mutlak);
}Output yang Dihasilkan:
Input: -5
Nilai mutlak: 5Mari kita bedah kode di atas baris per baris:
- Baris 1 (
extern "C" {): blok deklarasi dibuka dengan penanda ABI “C”. Semua fungsi di dalamnya diperlakukan sebagai kode luar yang mengikuti konvensi C. - Baris 2 (
fn abs(input: i32) -> i32;): signatureabsdideklarasikan sesuai dokumentasi C-nya — menerimaint, mengembalikanint, yang di Rust keduanya sepadan dengani32. Tidak ada tubuh fungsi: implementasinya hidup di libc, bukan di file ini. - Baris 6 (
let selisih_harga = -5;): nilai negatif disiapkan sebagai masukan — skenario penghitung jarak harga yang gagal sinkron antara dua cabang toko. - Baris 7 (
let jarak_mutlak = unsafe { abs(selisih_harga) };): pemanggilan fungsiexternadalah operasiunsafe, sehingga harus berada dalam blok. Alasannya fundamental: kompilator Rust tidak membaca kode C, jadi tidak ada satu pun jaminan yang bisa diberikannya — signature bisa salah ketik, fungsi bisa memodifikasi memori global, atau pustaka bisa tidak hadir saat program berjalan. - Baris 9–10 (
println!("Input: {}", ...); println!("Nilai mutlak: {}", ...);): hasil5dicetak dua baris bersama masukannya, membuktikan argumen benar-benar melintasi batas bahasa dan kembali dengan nilai yang benar.
Penanda unsafe pada pemanggilan bukan formalitas: seluruh tanggung jawab Chapter Kapan Unsafe berlaku di sini. Salah deklarasi tipe parameter — misalnya i64 padahal C menjanjikan int 32-bit — membuat pemanggilan menjadi undefined behavior tanpa satu pun error kompilasi, karena Rust hanya percaya deklarasi kita.
Kebalikannya: Fungsi Rust untuk C #
Arah kedua FFI mengekspor fungsi Rust agar bisa dipanggil C, dan dua penanda diperlukan. extern "C" pada definisi memastikan fungsi mengikuti ABI C (fungsi Rust biasa memakai ABI internal Rust yang tidak stabil antar versi), sedangkan #[no_mangle] menghentikan name mangling — proses pembekukan nama menjadi simbol unik seperti _ZN12pangkat_dua17h...E yang tidak bisa ditebak pihak C:
#[no_mangle]
pub extern "C" fn pangkat_dua(angka: i32) -> i32 {
angka * angka
}
fn main() {
println!("pangkat_dua(12) = {}", pangkat_dua(12));
}Output yang Dihasilkan:
pangkat_dua(12) = 144Mari kita bedah kode di atas baris per baris:
- Baris 1 (
#[no_mangle]): attribute memerintahkan kompilator membiarkan nama simbol tetappangkat_duadi file hasil kompilasi, persis seperti yang dikenali linker C. - Baris 2 (
pub extern "C" fn pangkat_dua(angka: i32) -> i32 {):pubmengekspos fungsi keluar crate,extern "C"memilih ABI C, dan parameter serta hasil bertipei32yang sepadan denganintdi sisi C. Tipe sepertiStringatau&Vec<T>tidak boleh melewati batas ini — hanya tipe dengan representasi C yang jelas (integer, float, pointer, struct ber-#[repr(C)]). - Baris 7 (
println!("pangkat_dua(12) = {}", pangkat_dua(12));): dari dalam Rust, fungsi ini dipanggil seperti fungsi biasa — penandaexternpada definisi tidak mewajibkan blokunsafe(berbeda dari pemanggilan blokexterndeklaratif di bagian sebelumnya).
Perhatikan pembedaan penting tersebut: mengekspor fungsi aman adalah operasi biasa, karena kompilator Rust tetap memeriksa tubuh fungsinya; bahaya FFI justru melekat pada pemanggil yang tidak dikenal. Penanda unsafe di sisi C tentu tidak ada — C tidak punya konsep itu, dan justru karena itulah tanggung jawab keamanan pemanggilan jatuh ke dokumentasi yang kita tulis.
Uji Nyata Lintas Bahasa #
Bukti terkuat bahwa kedua arah FFI bekerja adalah menjalankannya sungguhan. Kompilator rustc mendukung --crate-type=cdylib yang menghasilkan pustaka dinamis (.so di Linux) berisi simbol-simbol C, dan mesin ini dilengkapi gcc untuk sisi pemanggilnya. Siapkan dua file dalam satu direktori. File pertama, pustaka.rs:
#[no_mangle]
pub extern "C" fn pangkat_dua(angka: i32) -> i32 {
angka * angka
}File kedua, pemanggil.c — program C murni yang mendeklarasikan prototipe fungsi lalu memanggilnya:
#include <stdio.h>
int pangkat_dua(int angka);
int main(void) {
int hasil = pangkat_dua(12);
printf("pangkat_dua(12) = %d\n", hasil);
return 0;
}Kompilasi dan jalankan ketiganya dengan urutan berikut:
rustc --edition 2021 --crate-type=cdylib pustaka.rs -o libpustaka.so
gcc pemanggil.c -L. -lpustaka -o pemanggil
LD_LIBRARY_PATH=. ./pemanggilOutput yang Dihasilkan:
pangkat_dua(12) = 144Tiga perintah tersebut memuat seluruh siklus FFI. Baris pertama mengompilasi pustaka.rs menjadi pustaka dinamis libpustaka.so — nama file mengikuti konvensi Linux lib<nama>.so agar opsi -lpustaka menemukannya. Baris kedua mengompilasi program C dengan -L. (cari pustaka di direktori saat ini) dan -lpustaka (tautkan libpustaka.so). Baris ketiga menjalankan binary C dengan LD_LIBRARY_PATH=. supaya loader menemukan pustaka dinamis kita pada saat program dinyalakan. Angka 144 tercetak oleh printf C dari hasil perhitungan angka * angka yang dikerjakan Rust — dua bahasa, satu proses, tanpa kesalahan sejengkal pun.
Pada proyek nyata, ketiga langkah dikerjakan cargo lewat pengaturan crate-type = ["cdylib"] di Cargo.toml, dan tipe majemuk dijembatani crate libc (konstanta dan tipe seperti c_int) serta std::ffi::CString/CStr untuk string — teks C berupa pointer ke byte berakhiran \0, bukan String Rust berbasis panjang.
Ringkasan #
FFI mempertemukan Rust dengan dunia luar melalui ABI: blok extern "C" mendeklarasikan fungsi luar yang pemanggilannya wajib unsafe karena kompilator tidak membaca kode sisi seberang, sementara #[no_mangle] + pub extern "C" fn mengekspor fungsi Rust dengan nama dan ABI yang dikenali C. Fungsi abs dari libc terbukti terpanggil tanpa dependensi tambahan, dan uji cdylib + gcc membuktikan arah sebaliknya bekerja nyata: program C mencetak hasil perhitungan Rust. Risiko utamanya adalah kontrak tak tertulis — signature salah, kepemilikan memori kabur, dan string yang bentuknya berbeda — semuanya berujung undefined behavior tanpa peringatan kompilator. Dengan lima kemampuan unsafe, raw pointer, static mut, safe abstraction, dan FFI kini di tangan, kita siap menutup perjalanan Rust dengan section penutup tentang macros.