FFI di Rust: Memanggil C dari Rust
FFI adalah singkatan dari Foreign Function Interface (antarmuka fungsi asing). Inilah cara Rust berbicara dengan kode yang ditulis di bahasa lain. Hampir selalu, bahasa lain itu adalah C, atau sesuatu yang mengikuti aturan C, karena C adalah “bahasa bersama” yang dipahami hampir semua bahasa pemrograman dan sistem operasi.
Bayangkan dua orang yang bahasa ibunya berbeda, tapi sama-sama bisa sedikit bahasa Inggris. C adalah bahasa bersama itu. Dengan FFI, kamu bisa memakai library C yang sudah matang dari Rust, atau menulis library Rust yang cepat dan bisa dipanggil dari program Python, Node.js, atau C. Halaman ini melanjutkan materi Unsafe Rust, jadi baca halaman itu dulu kalau blok unsafe masih baru bagimu.
Memanggil fungsi C
Section titled “Memanggil fungsi C”Standard library C selalu ikut ditautkan (linked) ke program Rust di Linux, macOS, dan Windows, jadi fungsi-fungsinya sangat cocok untuk percobaan pertama. Untuk memanggilnya, kamu mendeklarasikannya di dalam blok extern "C": namanya, parameternya, dan tipe kembaliannya. Lalu kamu memanggilnya di dalam unsafe:
use std::ffi::c_int;
unsafe extern "C" { // Ini adalah `int abs(int x);` milik C, ditulis dalam Rust. fn abs(x: c_int) -> c_int;}
fn main() { let n = unsafe { abs(-42) }; println!("abs(-42) = {n}");}Output:
abs(-42) = 42Beberapa hal yang perlu diperhatikan:
"C"menyebutkan ABI-nya, yaitu aturan tentang bagaimana argumen dikirim dan nilai dikembalikan. Aturan C adalah aturan yang disepakati semua pihak.c_intadalah nama Rust untukintmilik C. Modulstd::ffipunya tipe sepertic_int,c_longdanc_char, supaya deklarasimu cocok persis dengan C di setiap platform.- Kamu menulis
unsafe extern "C". Di edition 2024,unsafedi sini wajib, karena kamu berjanji bahwa deklarasinya benar. Kode yang lebih lama (dan edition 2021) sering menulisextern "C" { ... }biasa. Versi denganunsafeberfungsi di keduanya, jadi halaman ini memakainya di semua contoh.
Kenapa setiap panggilan unsafe, dan cara menyembunyikannya
Section titled “Kenapa setiap panggilan unsafe, dan cara menyembunyikannya”Compiler Rust tidak bisa membaca kode C. Ia tidak tahu apakah abs benar-benar menerima int, apakah ia bisa crash, atau apakah ia menyimpan pointer yang kamu berikan. Jadi setiap panggilan ke fungsi asing itu unsafe: kamulah yang bertanggung jawab memakainya dengan benar.
Kabar baiknya, kamu hanya perlu memikirkannya dengan serius sekali saja. Pola yang biasa dipakai adalah menulis safe wrapper (pembungkus yang aman) kecil, yaitu fungsi Rust biasa yang memeriksa aturannya lalu melakukan panggilan unsafe. Semua orang lain cukup memanggil pembungkus itu.
Ini contoh nyata. Di C, abs(INT_MIN) adalah undefined behavior (perilaku yang tidak terdefinisi), karena jawabannya terlalu besar untuk muat di int. Pembungkus kita memeriksa kasus itu lebih dulu:
use std::ffi::c_int;
unsafe extern "C" { fn abs(x: c_int) -> c_int;}
// Safe wrapper: pemanggil tidak butuh `unsafe`.fn safe_abs(x: i32) -> Option<i32> { // abs(i32::MIN) tidak punya jawaban yang valid di C, jadi kita menolaknya. if x == i32::MIN { return None; } // SAFETY: x bukan i32::MIN, jadi abs punya hasil yang valid. Some(unsafe { abs(x) })}
fn main() { println!("{:?}", safe_abs(-42)); println!("{:?}", safe_abs(i32::MIN));}Output:
Some(42)NoneKomentar // SAFETY: adalah kebiasaan Rust yang patut ditiru. Komentar itu menjelaskan kenapa blok unsafe yang satu ini aman, supaya pembaca berikutnya tidak perlu menebak-nebak.
Kalau sebuah fungsi C benar-benar tidak punya aturan apa pun untuk input apa pun, kamu bisa menandainya safe fn di dalam blok unsafe extern, lalu memanggilnya tanpa unsafe. abs bukan fungsi seperti itu (ingat INT_MIN), jadi kita tetap membiarkannya unsafe dan memeriksa inputnya sendiri.
String: CString, CStr, dan byte NUL
Section titled “String: CString, CStr, dan byte NUL”String adalah tempat sebagian besar bug FFI bersarang, karena Rust dan C menyimpannya dengan cara berbeda:
String/&strdi Rust tahu panjangnya sendiri dan boleh berisi byte apa saja, termasuk0.- String C hanyalah pointer ke deretan byte. String itu berakhir di byte
0yang pertama (disebut NUL). Panjangnya tidak disimpan.
Jadi kamu tidak bisa langsung mengirim &str ke C. Pakai CString (bahasa Inggris) untuk membuat salinan milik sendiri yang diakhiri NUL, dan .as_ptr() untuk mendapatkan *const c_char yang diminta C:
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn c_strlen(text: &str) -> Option<usize> { // CString::new menambahkan NUL di akhir untuk kita. let c_text = CString::new(text).ok()?; // SAFETY: c_text adalah string valid yang diakhiri NUL dan hidup sampai akhir fungsi ini. Some(unsafe { strlen(c_text.as_ptr()) })}
fn main() { let hello = CString::new("hello").unwrap(); println!("{:?}", hello.as_bytes_with_nul());
println!("{:?}", c_strlen("hello")); println!("{:?}", c_strlen("héllo"));
match CString::new("hel\0lo") { Ok(_) => println!("ok"), Err(e) => println!("error: {e}"), }}Output:
[104, 101, 108, 108, 111, 0]Some(5)Some(6)error: nul byte found in provided data at position: 3Di contoh ini string-nya sengaja dibiarkan dalam bahasa Inggris, karena output-nya bergantung pada byte dan panjang teks. Ada dua kejutan di sini, dan keduanya penting:
strlen("héllo")hasilnya 6, bukan 5. C menghitung byte, danémemakan dua byte di UTF-8.CString::newgagal kalau teksmu berisi byte0. C akan mengira string-nya berakhir di situ dan diam-diam memotong"lo". Rust menolak alih-alih membiarkan hal itu terjadi, dan karena itulahCString::newmengembalikanResult. Pesan error-nya (dari Rust, dalam bahasa Inggris) artinya: ada byte nul di data yang diberikan, di posisi 3.
Untuk arah sebaliknya, saat C memberimu char *, bungkus dengan CStr (bahasa Inggris). Hubungan CStr dengan CString sama seperti &str dengan String: sebuah tampilan pinjaman. Fungsi strchr milik C mengembalikan pointer ke karakter pertama yang cocok, jadi cocok untuk demo:
use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" { fn strchr(s: *const c_char, c: c_int) -> *mut c_char;}
fn find_from(text: &CStr, ch: u8) -> Option<String> { let ptr = unsafe { strchr(text.as_ptr(), ch as c_int) }; if ptr.is_null() { return None; // C bilang "tidak ketemu" dengan null pointer } // SAFETY: ptr menunjuk ke dalam `text`, yang masih hidup dan diakhiri NUL. let found = unsafe { CStr::from_ptr(ptr) }; Some(found.to_string_lossy().into_owned())}
fn main() { let text = CString::new("halo, dunia").unwrap(); println!("{:?}", find_from(&text, b',')); println!("{:?}", find_from(&text, b'z'));
// Literal c"..." adalah &CStr yang sudah diberi NUL. println!("{:?}", find_from(c"rust-lang.org", b'.'));}Output:
Some(", dunia")NoneSome(".org")to_string_lossy() mengubah byte dari C menjadi teks Rust. Ia mengganti UTF-8 yang tidak valid dengan �, karena string C tidak menjamin isinya UTF-8. Pakai .to_str() kalau kamu lebih suka mendapat error.
Berbagi struct dengan #[repr(C)]
Section titled “Berbagi struct dengan #[repr(C)]”Rust boleh mengubah urutan field struct untuk menghemat tempat. C tidak pernah melakukannya: field disimpan di memori sesuai urutan penulisannya, dengan padding (ruang kosong) di antaranya. Contohnya, struct dengan field u8, u32, u8 memakan 8 byte dengan layout Rust saat ini (Rust mendekatkan kedua u8), tapi 12 byte dengan layout C. Kalau C dan Rust tidak sepakat soal layout, C akan membaca byte yang salah, dan tidak ada yang memperingatkanmu.
Jadi, saat sebuah struct melintasi batas antarbahasa, suruh Rust memakai layout C dengan #[repr(C)]. Fungsi div milik C mengembalikan struct div_t dengan dua field int, yaitu quot (hasil bagi) dan rem (sisa):
use std::ffi::c_int;
// Sama dengan milik C: typedef struct { int quot; int rem; } div_t;#[repr(C)]#[derive(Debug)]struct DivT { quot: c_int, rem: c_int,}
unsafe extern "C" { fn div(numerator: c_int, denominator: c_int) -> DivT;}
fn main() { let result = unsafe { div(17, 5) }; println!("{result:?}"); println!("17 / 5 = {} sisa {}", result.quot, result.rem);}Output:
DivT { quot: 3, rem: 2 }17 / 5 = 3 sisa 2Membiarkan C memanggil Rust
Section titled “Membiarkan C memanggil Rust”FFI bekerja dua arah. Tulis extern "C" fn (tanpa blok) dan kamu mendapat fungsi Rust yang mengikuti aturan pemanggilan C, sehingga kode C bisa memanggilnya. Fungsi qsort milik C menerima fungsi pembanding sebagai argumen, dan kita bisa memberinya fungsi Rust:
use std::ffi::{c_int, c_void};
unsafe extern "C" { fn qsort( base: *mut c_void, count: usize, size: usize, compare: extern "C" fn(*const c_void, *const c_void) -> c_int, );}
// Fungsi Rust yang akan dipanggil oleh C.extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int { // SAFETY: qsort hanya memberi kita pointer ke dalam array i32 milik kita. let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) }; a.cmp(&b) as c_int // Less = -1, Equal = 0, Greater = 1}
fn sort_with_c(numbers: &mut [i32]) { unsafe { qsort( numbers.as_mut_ptr() as *mut c_void, numbers.len(), std::mem::size_of::<i32>(), compare_i32, ); }}
fn main() { let mut numbers = [42, 7, 19, -3, 0]; sort_with_c(&mut numbers); println!("{:?}", numbers);}Output:
[-3, 0, 7, 19, 42](Di kode sungguhan, kamu cukup memanggil sort(). Contoh ini hanya untuk menunjukkan C memanggil balik ke Rust.)
Kalau program C perlu menemukan fungsimu berdasarkan nama, tambahkan #[no_mangle]. Biasanya Rust “mengacak” (mangle) nama menjadi sesuatu seperti _ZN4main11add_numbers17h...E supaya crate yang berbeda tidak pernah bentrok. #[no_mangle] mempertahankan nama polosnya, add_numbers:
#[no_mangle]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}Catatan edition 2024: contoh Playground di situs ini memakai edition 2021, dan di sana #[no_mangle] tidak masalah. Di edition 2024 (bawaan untuk proyek baru dari cargo new), atribut ini harus ditulis #[unsafe(no_mangle)], karena dua fungsi dengan nama tanpa-mangle yang sama akan merusak program. Penulisan baru ini juga berfungsi di edition 2021.
Untuk membangun library yang bisa ditautkan oleh C, atur tipe crate menjadi cdylib di Cargo.toml:
[lib]crate-type = ["cdylib"]// src/lib.rs (edition 2024)#[unsafe(no_mangle)]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}cargo build --release menghasilkan target/release/libmathlib.so (.dylib di macOS, .dll di Windows) untuk crate bernama mathlib. Program C lalu mendeklarasikan fungsinya dan menautkan library itu:
#include <stdio.h>#include <stdint.h>
int32_t add_numbers(int32_t a, int32_t b);
int main(void) { printf("2 + 3 = %d\n", add_numbers(2, 3)); return 0;}$ cc main.c -L target/release -lmathlib -o main$ LD_LIBRARY_PATH=target/release ./main2 + 3 = 5Menulis header C seperti itu secara manual cepat membosankan. cbindgen (bahasa Inggris) membuatnya otomatis dari kode Rust-mu.
Meng-compile kode C sendiri dengan build.rs dan cc
Section titled “Meng-compile kode C sendiri dengan build.rs dan cc”Sejauh ini kita hanya memakai fungsi yang sudah ditautkan. Untuk memakai file C milikmu sendiri, biarkan Cargo meng-compile-nya. File bernama build.rs di root proyek adalah build script: Cargo menjalankannya sebelum meng-compile crate-mu. Crate cc (bahasa Inggris) membuat semua itu cukup beberapa baris saja.
cargo new hello-ccd hello-ccargo add --build cc#include <stdio.h>
int add_ints(int a, int b) { return a + b;}
void greet(const char *name) { printf("Halo dari C, %s!\n", name); fflush(stdout);}fn main() { // Compile src/greet.c menjadi static library lalu tautkan. cc::Build::new().file("src/greet.c").compile("greet"); // Jalankan ulang skrip ini hanya saat file C berubah. println!("cargo::rerun-if-changed=src/greet.c");}use std::ffi::{c_char, c_int, CString};
unsafe extern "C" { fn add_ints(a: c_int, b: c_int) -> c_int; fn greet(name: *const c_char);}
fn main() { let sum = unsafe { add_ints(2, 3) }; println!("2 + 3 = {sum}");
let name = CString::new("Ferris").unwrap(); unsafe { greet(name.as_ptr()) };}$ cargo run -q2 + 3 = 5Halo dari C, Ferris!Kamu perlu compiler C yang sudah terpasang (gcc, clang, atau MSVC di Windows); cc akan menemukannya untukmu.
Untuk library C besar dengan ratusan fungsi, menulis setiap deklarasi extern secara manual itu lambat dan gampang salah. bindgen (bahasa Inggris) membaca file header C dan membuat deklarasi Rust-nya untukmu, biasanya juga dari build.rs. Sebagian besar crate -sys di crates.io (seperti openssl-sys) dibuat dengan cara ini.
Lebih dari C: Python, Node.js, dan web
Section titled “Lebih dari C: Python, Node.js, dan web”Karena C adalah bahasa bersama, ide yang sama memungkinkan Rust berbicara dengan banyak bahasa lain. Untuk ini kamu jarang menulis kode extern "C" mentah: sebuah crate mengerjakan bagian unsafe-nya dan memberimu API yang aman dan nyaman:
- Python: PyO3 (bahasa Inggris) memungkinkanmu menulis modul Python dengan Rust (dan memanggil Python dari Rust). Alat pendampingnya,
maturin, membangun dan menerbitkannya sebagai paketpipbiasa. Library seperti Polars dan pydantic-core memakainya. - Node.js: napi-rs (bahasa Inggris) membangun add-on native Node.js dengan Rust, lengkap dengan tipe TypeScript yang dibuat otomatis.
- Proyek C dan C++:
cdylibplus cbindgen, seperti yang ditunjukkan di atas. - Browser: itu bukan FFI dalam arti C, tapi
wasm-bindgenmemainkan peran yang mirip. Lihat WebAssembly.
Kesalahan umum
Section titled “Kesalahan umum”Memanggil fungsi asing di luar unsafe.
unsafe extern "C" { fn abs(x: i32) -> i32;}
fn main() { let n = abs(-7); println!("{n}");}error[E0133]: call to unsafe function `abs` is unsafe and requires unsafe function or block --> src/main.rs:6:13 |6 | let n = abs(-7); | ^^^^^^^ call to unsafe functionCompiler bilang: memanggil fungsi unsafe abs butuh fungsi atau blok unsafe. Perbaikannya: bungkus panggilan itu dengan unsafe { ... }, atau lebih baik lagi, dengan fungsi safe wrapper seperti di atas.
Pointer ke CString yang sudah hilang. Kode ini bisa di-compile, tapi ini bug sungguhan:
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn main() { let ptr = CString::new("hello").unwrap().as_ptr(); let len = unsafe { strlen(ptr) }; println!("{len}");}warning: this creates a dangling pointer because temporary `CString` is dropped at end of statement --> src/main.rs:8:46 |8 | let ptr = CString::new("hello").unwrap().as_ptr(); | ------------------------------ ^^^^^^ pointer created here | | | this `CString` is dropped at end of statement | = help: bind the `CString` to a variable such that it outlives the pointer returned by `as_ptr`CString itu hanya nilai sementara (temporary), jadi ia dibebaskan di ;. Sekarang ptr menunjuk ke memori yang sudah dibebaskan, alias dangling pointer (pointer menggantung). Perbaikannya: simpan CString di variabelnya sendiri (let s = CString::new("hello").unwrap();) dan panggil s.as_ptr() selagi s masih hidup. Jangan pernah mengabaikan warning ini.
extern "C" biasa di proyek edition 2024. Kode yang disalin dari tutorial lama gagal seperti ini:
error: extern blocks must be unsafe --> src/main.rs:3:1 |3 | / extern "C" {4 | | fn add_ints(a: c_int, b: c_int) -> c_int;5 | | fn greet(name: *const c_char);6 | | } | |_^Artinya: blok extern harus unsafe. Perbaikannya: tulis unsafe extern "C" { ... }.
Lupa #[repr(C)]. Tidak ada error sama sekali, C hanya membaca data sampah. Pasang #[repr(C)] di setiap struct dan enum yang melintasi batas antarbahasa.
Ringkasan
Section titled “Ringkasan”- Deklarasikan fungsi C di dalam blok
unsafe extern "C" { ... }, dengan tipe C daristd::ffisepertic_intdanc_char. - Setiap panggilan ke fungsi asing itu
unsafe. Bungkus sekali saja di fungsi Rust yang aman, yang memeriksa aturannya, dengan komentar// SAFETY:. - String C berakhir di byte NUL. Pakai
CStringuntuk mengirim teks ke C danCStruntuk membaca teks dari C. JagaCStringtetap hidup selama C memakai pointer-nya. - Pakai
#[repr(C)]untuk struct yang dibagi dengan C, karena Rust bisa mengubah urutan field. extern "C" fnplus#[no_mangle](#[unsafe(no_mangle)]di edition 2024) memungkinkan C memanggil Rust;cdylibmembangun library untuk itu.build.rs+ccmeng-compile kode C-mu;bindgenmembuat deklarasinya; PyO3 dan napi-rs menghubungkan Rust dengan Python dan Node.js.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa setiap panggilan ke fungsi dari blok extern "C" itu unsafe?
2. Apa yang terjadi dengan CString::new("hel\0lo")?
3. Kenapa struct yang dibagi dengan C butuh #[repr(C)]?
4. Di crate edition 2024, bagaimana cara menulis atribut yang mempertahankan nama polos fungsi untuk C?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- FFI — The Rustonomicon (bahasa Inggris): pembahasan mendalam resmi tentang callback, linking, nullable pointer, dan lainnya.
std::ffi— dokumentasi std (bahasa Inggris):CString,CStr,c_int,c_chardan kawan-kawannya, dengan contoh.- External blocks — The Rust Reference (bahasa Inggris): aturan persis untuk blok
extern, ABI, dan itemsafe/unsafe. - The bindgen User Guide (bahasa Inggris): membuat binding Rust dari header C.
- The PyO3 User Guide (bahasa Inggris): menulis ekstensi Python dengan Rust.