⚡ Jawaban Singkat / Key Takeaways

Safe C++ menjamin pertukaran data non-UB antara Rust dan C++ lewat verifikasi ownership transfer dan borrow checking di batas FFI. Compiler menghentikanmu sebelum kode yang salah menyentuh production. Hasilnya: double-free, use-after-free, dan buffer overflow di boundary dicegah saat kompilasi, bukan diuji coba lewat valgrind sejam kemudian.

Kamu Percaya FFI Aman? Coba Cek Ulang

Kamu sudah migrasi core engine ke Rust. Ownership model sudah kamu kuasai. Borrow checker sudah jadi teman, bukan musuh. Lalu datang kebutuhan integrasi: library C++ legacy 300 ribu baris yang nggak mungkin ditulis ulang dalam 6 bulan. Kamu bungkus dengan extern "C", tambah unsafe, dan berharap semuanya baik-baik saja.

Tiga minggu kemudian, production server restart sendiri jam 3 pagi. Stack trace menunjuk ke fungsi C++ yang menerima pointer dari Rust. Kamu lihat kode dan sadar: C++ tidak tahu kalau Rust sudah meng-drop ownership data yang dilewatkan. Double-free via dua runtime yang berbeda. Inilah realita FFI tanpa jembatan memory safety.

Menurut laporan CWE Top 25 2024, out-of-bounds write dan use-after-free masih menduduki peringkat teratas software bugs paling berbahaya. FFI boundary adalah hotspot nomor satu untuk dua kelas bug ini. Bukan karena programmer-nya ceroboh. Tapi karena kontrak memori antara dua bahasa tidak terverifikasi oleh siapa pun.

Apa Itu FFI Boundary dan Kenapa Dia Bisa Meledak?

FFI (Foreign Function Interface) adalah jembatan komunikasi antara runtime dua bahasa. Di permukaan, ini sederhana: Rust memanggil fungsi C++, atau sebaliknya, lewat konvensi ABI (Application Binary Interface) yang disepakati. Tapi di balik layar, ada lubang besar: tidak ada yang mengawasi kontrak memori di antara keduanya.

Bayangkan Rust meminjamkan &mut Vec<u8> ke fungsi C++ via pointer mentah. Rust menganggap ownership-nya masih miliknya. Tapi fungsi C++ bisa memanggil free(), atau menyimpan pointer itu di global state untuk dipakai nanti, atau mengubah ukuran buffer tanpa sepengetahuan Rust. Semua ini UB (Undefined Behavior) murni.

Masalahnya bertambah parah di proyek besar. Satu modul Rust bisa memanggil library C++ yang tidak lagi di-maintain. Developer baru tidak tahu asumsi ownership apa yang tertanam di kode unsafe. Review PR hanya bisa berharap tidak ada yang terlewat. Ini bukan soal skill. Ini soal tidak adanya enforcement mekanis.

Potongan kode Rust unsafe block dengan highlight sintaks yang menunjukkan FFI call ke C++
Kode Rust dengan blok unsafe: compiler membebaskanmu dari guardrails, tapi kamu yang tanggung jawab penuh kontrak memori

Safe C++: Bukan Sekadar Dialek, Tapi Kontrak Baru di Boundary

Safe C++ adalah proposal dan toolchain yang memperkenalkan aturan borrow checking di sisi C++. Konsep intinya sederhana: kalau Rust bisa memverifikasi ownership secara statis, kenapa kode C++ yang berinteraksi dengan Rust tidak bisa melakukan hal yang sama? Proposal Safe C++ (Circle compiler) dipelopori oleh Sean Baxter dan menjadi perbincangan panas di ISO C++ committee.

Safe C++ memperkenalkan tiga kontrak fundamental di batas FFI:

  • Ownership transfer eksplisit: Setiap pointer yang melewati boundary punya status owned atau borrowed yang diverifikasi compiler. Tidak bisa C++ tiba-tiba menyimpan pointer pinjaman tanpa izin.
  • Lifetime annotation di boundary: Fungsi FFI diberi anotasi lifetime mirip Rust. Compiler Safe C++ memeriksa bahwa pointer yang keluar tidak hidup lebih lama dari referensi yang masuk.
  • Mutability XOR aliasing: Safe C++ menerapkan aturan eksklusivitas mutable reference. Kalau pointer &mut dikirim ke C++, tidak boleh ada pointer lain yang mengakses data yang sama.
Diagram abstrak alur data antara modul Rust dan C++ dengan lapisan memory safety
Alur data Rust ke C++ tanpa Safe C++ (kiri) vs dengan Safe C++ (kanan): ownership verification terjadi di boundary

Pola 1: Ownership Transfer yang Nggak Ambigu Lagi

Masalah klasik FFI: Rust mengalokasikan objek, mengirim pointer ke C++, lalu siapa yang bertanggung jawab menghapusnya? Di FFI tradisional, jawabannya ada di dokumentasi (yang sering tidak dibaca) atau di komentar kode (yang sudah kadaluarsa). Safe C++ menghilangkan tebakan ini.

// Rust side: mengirim data ke C++
let data = Box::new(TelemetryPacket::new());
unsafe {
    // Box::into_raw() → ownership pindah ke C++
    cpp_process_packet(Box::into_raw(data));
}

// Safe C++ side: menerima ownership eksplisit
[[safety::transfer(rust::owned)]]
void cpp_process_packet(TelemetryPacket* packet) {
    // Safe C++ tahu packet ini milik C++ sekarang
    // Compiler mengeluh kalau packet tidak di-delete sebelum fungsi return
    process(std::move(*packet));
    delete packet;
}

Compiler Safe C++ memverifikasi tiga hal: (1) pointer yang diterima bertipe owned, (2) pointer di-delete atau di-move tepat satu kali, (3) tidak ada dangling reference yang tersisa setelah fungsi return. Kalau salah satu syarat tidak terpenuhi, kompilasi gagal. Inilah yang tidak dimiliki oleh extern "C" biasa.

Pola 2: Borrow Checking di Dua Sisi Sekaligus

Rust mengirim &[u8] ke C++ untuk diproses tanpa transfer ownership. Tapi apa yang terjadi kalau C++ menyimpan pointer itu di global state dan Rust meng-drop buffer-nya? Use-after-free yang sunyi. Safe C++ mencegah ini dengan lifetime annotation.

// Rust side: meminjamkan slice
let buf: Vec<u8> = vec![0; 256];
unsafe {
    cpp_read_samples(buf.as_ptr(), buf.len());
}

// Safe C++ side: lifetime eksplisit
[[safety::lifetime(input, "buf_lifetime")]]
void cpp_read_samples(const uint8_t* data, size_t len) {
    // Compiler Safe C++ MEMASTIKAN data tidak disimpan 
    // ke static/global variable atau return value
    // Lifetime terikat ke scope pemanggil
    analyze(data, len);
    // OK: data hanya digunakan di dalam scope fungsi
}

Ini pergeseran paradigma besar. Sebelumnya, developer mengandalkan komentar atau konvensi penamaan (seperti borrowed_ prefix ala WebKit) untuk menandai pointer pinjaman. Safe C++ mengubah konvensi itu menjadi aturan yang diverifikasi compiler. Kamu tidak bisa “lupa” bahwa pointer ini cuma pinjaman.

Pola 3: Buffer dengan Ukuran Statis, Jaminan Kompilasi

Satu lagi celah klasik: fungsi C++ menerima pointer dan len, tapi tidak ada yang menjamin len sesuai dengan kapasitas buffer sebenarnya. Di Rust, kamu bisa meng-handle ini dengan const generics (seperti yang sudah kami bahas di artikel trik const generics FFI). Safe C++ menyediakan mekanisme serupa di sisi C++.

// Safe C++: buffer fixed-size, ukuran adalah bagian dari tipe
template<size_t N>
[[safety::buffer(input, N)]]
void cpp_fill_buffer(uint8_t (&data)[N]) {
    // Compiler Safe C++ MENJAMIN:
    // - Semua akses data[i] dengan i < N → aman
    // - Tidak bisa menulis ke data[N] → compile error
    // - N konsisten dengan yang dikirim Rust
    for (size_t i = 0; i < N; ++i) {
        data[i] = static_cast<uint8_t>(i);
    }
}

Dipadukan dengan const generics Rust, kamu mendapatkan jaminan ukuran buffer dari ujung ke ujung. Compiler Rust memverifikasi ukuran di sisi pemanggil, compiler Safe C++ memverifikasi akses di sisi penerima. Tidak ada runtime overhead.

Monitor menampilkan kode sistem programming dengan terminal dan compiler output di sampingnya
Dua compiler, satu kontrak: Rust dan Safe C++ memverifikasi memory safety di kedua sisi boundary

Yang Masih Belum Tersentuh (dan Yang Perlu Kamu Waspadai)

Safe C++ bukan silver bullet. Beberapa hal tetap memerlukan kewaspadaan:

  • ABI mismatch: Safe C++ melindungi memory safety, bukan layout compatibility. Pastikan #[repr(C)] di Rust match dengan padding dan alignment di C++.
  • Exception safety: C++ exceptions yang menembus stack frame Rust adalah UB. Gunakan extern "C" (bukan extern "C++") untuk menghindari unwinding antar bahasa.
  • Global mutable state: Safe C++ bisa melarang penyimpanan pointer ke global, tapi tidak bisa mencegah C++ memodifikasi global state yang juga diakses Rust. Dokumentasikan shared mutable state secara eksplisit.
  • Adopsi toolchain: Safe C++ saat ini memerlukan Circle compiler, bukan Clang atau GCC mainstream. Pertimbangkan timeline adopsi di organisasi kamu.

Baca juga artikel kami tentang diagnostik const generics Rust 1.80 yang memperkuat sisi Rust dari jembatan FFI ini. Kalau kamu ingin mendalami keamanan bahasa pemrograman secara lebih luas, cek analisis celah keamanan Rust vs TypeScript vs Python.

Peta Adopsi: Kapan Safe C++ Masuk Pipelinemu?

Kalau kamu systems programmer yang sedang atau akan melakukan transisi C++ ke Rust, berikut peta realistisnya:

  • Hari ini: Mulai tandai fungsi FFI dengan anotasi manual. Gunakan unsafe block sekecil mungkin di Rust dan dokumentasikan kontrak ownership di docstring.
  • 6-12 bulan ke depan: Eksperimen dengan Circle compiler untuk modul C++ baru yang berinteraksi langsung dengan Rust. Safe C++ bisa dipakai untuk greenfield code sementara legacy tetap pakai C++ konvensional.
  • 2-3 tahun ke depan: Jika proposal Safe C++ diterima ISO committee, compiler mainstream akan mengadopsi subset aturan borrow checking. Ini titik balik di mana FFI Rust-C++ benar-benar aman tanpa toolchain eksperimental.

Lihat roadmap resmi di Safe C++ Dashboard dan diskusi ISO C++ di WG21 Papers. Untuk pemahaman mendalam tentang memory safety di C++, rujuk juga tulisan Herb Sutter tentang C++ safety.

FAQ: Memory Safety Bridge di FFI Boundary

Apa bedanya Safe C++ dengan C++ biasa saat berinteraksi dengan Rust?

C++ biasa mengandalkan dokumentasi dan code review untuk memastikan kontrak memori dipatuhi. Safe C++ memverifikasi kontrak itu lewat compiler: ownership (owned vs borrowed), lifetime pointer, dan mutability XOR aliasing ditegakkan secara mekanis. Hasilnya, bug use-after-free dan double-free di FFI boundary tertangkap saat kompilasi.

Apakah Safe C++ bisa dipakai dengan compiler C++ standar seperti GCC atau Clang?

Saat ini Safe C++ hanya didukung oleh Circle compiler (proyek eksperimental Sean Baxter). Proposal standarisasi sedang dibahas di ISO C++ committee, tapi belum ada timeline pasti untuk adopsi di GCC, Clang, atau MSVC. Kamu bisa memulai eksperimen di modul terisolasi sambil menunggu adopsi mainstream.

Apa yang terjadi kalau C++ legacy tidak bisa dimigrasi ke Safe C++?

Kamu bisa menerapkan pola bertahap: bungkus API C++ legacy dengan Safe C++ wrapper tipis yang memverifikasi kontrak di boundary. Safe C++ wrapper bertindak sebagai "firewall" antara kode Rust dan C++ legacy. Kode legacy sendiri tidak perlu diubah. Pola ini mirip dengan cara Rust developer membungkus library C dengan safe wrapper.

Berapa overhead runtime dari Safe C++ dibanding FFI tradisional?

Nol. Semua verifikasi Safe C++ terjadi saat kompilasi (static analysis dan borrow checking). Binary yang dihasilkan identik dengan FFI tradisional dari segi performa. Tidak ada runtime guard, reference counting tambahan, atau garbage collection. Ini zero-cost abstraction, sama seperti ownership system di Rust.

Kesimpulan: Dua Bahasa, Satu Kontrak, Nol UB

FFI Rust-C++ selama ini seperti jembatan tanpa pagar pembatas. Kamu bisa menyeberang, tapi satu langkah salah dan semuanya jatuh. Safe C++ mendirikan pagar itu. Bukan pagar dari dokumentasi atau konvensi tim, tapi pagar yang diverifikasi compiler di kedua sisi.

Ownership yang jelas. Lifetime yang terikat. Buffer yang ukurannya diketahui saat kompilasi. Semua ini dulunya cuma ada di sisi Rust. Safe C++ membawa disiplin yang sama ke sisi C++. Dan itu artinya, untuk pertama kalinya, kamu bisa membangun sistem hybrid Rust-C++ tanpa harus berdoa tiap kali data melewati batas bahasa.

Mulai dari mana? Buka satu fungsi FFI di codebase-mu. Tanya: siapa yang punya pointer ini setelah dipanggil? Kalau jawabannya tidak jelas, Safe C++ adalah tempat kamu harus melihat.

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