Chapter sebelumnya menunjukkan bahwa extern "C" membuat nama simbol C++ cocok dengan kode C, dan membekasnya mungkin masih mengganjal: mengapa compiler perlu mengubah nama fungsi seperti kuadrat menjadi _Z7kuadrati? Jawabannya ada pada kontrak paling rendah yang menyatukan dua potong kode biner — ABI, Application Binary Interface. Pada Chapter ini kita naik satu tingkat dari “nama simbol” ke “aturan main biner”: apa saja yang diatur ABI, mengapa ABI C dianggap stabil sementara ABI C++ tidak, dan bagaimana kesimpulan praktisnya menentukan apa yang boleh kita lewatkan di batas antara dua library. Kami juga akan membongkar nama mangled langsung dari dalam program C++.
Apa Itu ABI #
API mengatur kecocokan di level source code: kode Anda dikompilasi ulang, maka semua beres. ABI mengatur kecocokan di level biner, tanpa kompilasi ulang: ukuran dan tata letak struct, urutan register untuk melempar argumen ke fungsi (calling convention), siapa yang bertugas membersihkan stack, nama simbol di object file, hingga bentuk nama type saat di-run-time. Dua potong biner hanya bisa saling dipakai bila keduanya berbicara ABI yang sama. Contoh paling nyata: program kita memanggil printf milik libc yang dikompilasi bertahun-tahun lalu oleh compiler berbeda — itu berhasil bukan karena sumbernya cocok, melainkan karena ABI-nya cocok.
Name Mangling: Cara C++ Melabeli Fungsinya #
C++ memungkinkan dua fungsi bernama sama dengan parameter berbeda, sedangkan linker bekerja dengan nama yang unik. Solusinya: compiler mengodekan namespace, nama, dan tipe parameter menjadi satu simbol. Aturan pengodean yang dipakai GCC dan Clang di Linux dikenal sebagai skema Itanium C++ ABI — bukan karena masalah sejarahnya, tetapi karena komite yang merumuskannya berasal dari proyek compiler Itanium. Mari kita lihat langsung dengan nm:
#include <string>
// Dua fungsi C++ yang di-overload: simbolnya pasti berbeda
int ukuran(const std::string& s) { return static_cast<int>(s.size()); }
int ukuran(int n) { return n; }
// Fungsi ber-linkage C: tidak boleh di-overload, nama tetap polos
extern "C" int ukuran_c(const char* s);
extern "C" int ukuran_c(const char* s) {
int n = 0;
while (s[n] != '\0') ++n;
return n;
}g++ -std=c++23 -Wall -Wextra -c simbol.cpp -o simbol.o
nm simbol.o | grep ' T ' | awk '{print $2, $3}'Output yang Dihasilkan:
T _Z6ukuranRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
T _Z6ukurani
T ukuran_cSimbol pertama adalah ukuran(const std::string&): awalan _Z menandai nama mangled, 6ukuran berarti nama berenam huruf, lalu RKNSt7__cxx11... mengodekan reference konstan ke std::string. Simbol kedua _Z6ukurani adalah ukuran(int). Karena aturannya rapi, nama itu bisa dibongkar kembali dengan c++filt — tool yang menerjemahkan simbol mangled menjadi bentuk yang bisa dibaca manusia:
nm simbol.o | grep ' T ' | awk '{print $2, $3}' | c++filtOutput yang Dihasilkan:
T ukuran(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
T ukuran(int)
T ukuran_cInilah fungsi yang sama, dilihat dari dua sisi ABI. Perhatikan ukuran_c dan main: keduanya tak di-mangle, karena itu program C maupun library C bisa berbicara dengan siapa pun.
Membongkar Mangled Name dari Dalam Program #
c++filt bukan satu-satunya cara — libstdc++ menyediakan fungsi abi::__cxa_demangle() sehingga program sendiri bisa membongkar nama saat berjalan. Ini berguna untuk pencatatan kesalahan (error logging) yang menampilkan nama type hasil typeid:
// abi-demangle.cpp — membaca dan membongkar nama simbol C++ (mangled)
// lewat fasilitas <cxxabi.h> yang disediakan libstdc++ (GCC/Clang).
#include <cxxabi.h>
#include <typeinfo>
#include <cstdlib>
#include <memory>
#include <string>
#include <iostream>
#include <vector>
namespace app {
struct Karyawan { int id; };
int totalGaji(const std::vector<int>&) { return 0; }
}
// Demangle nama hasil Itanium ABI (GCC/Clang) saat program berjalan.
// Pemakaiannya ala C: hasilnya char* yang harus dibebaskan dengan free().
std::string demangle(const char* nama) {
int status = 0;
std::unique_ptr<char, void (*)(void*)> hasil(
abi::__cxa_demangle(nama, nullptr, nullptr, &status), std::free);
return (status == 0 && hasil) ? std::string(hasil.get()) : nama;
}
int main() {
// typeid(...).name() pada GCC mengembalikan nama mangled
const char* mentah = typeid(app::Karyawan).name();
std::cout << mentah << " => " << demangle(mentah) << "\n";
std::cout << "_ZN3app9totalGajiERKSt6vectorIiSaIiEE => "
<< demangle("_ZN3app9totalGajiERKSt6vectorIiSaIiEE") << "\n";
return 0;
}Output yang Dihasilkan:
N3app8KaryawanE => app::Karyawan
_ZN3app9totalGajiERKSt6vectorIiSaIiEE => app::totalGaji(std::vector<int, std::allocator<int> > const&)Bedah singkatnya:
- Baris 12-13 (
namespace app) — struct dan fungsi di dalam namespace; tanpa mangling, dua proyek yang sama-sama punyaKaryawanakan bertabrakan di linker. - Baris 17-22 (
demangle) —__cxa_demanglemenerima nama mangled, buffer, panjang, dan status; hasilnyachar*milik kita, sehingga pembungkusstd::unique_ptrdengan deleterstd::freememastikan memori dibebaskan — pola RAII yang sudah lama kita kenal. - Baris 27 (
typeid(...).name()) — pada GCC, nama type yang dikembalikan adalah mangled:N3app8KaryawanEberarti nestedapp::Karyawan, angka di depan adalah panjang tiap komponen nama.
C ABI Stabil, C++ ABI Tidak #
Di satu platform yang sama, ABI C berubah sangat lambat: int tetap berukuran tertentu, struct tetap tersusun dengan aturan padding yang sama, dan aturan pemanggilan tetap sama selama puluhan tahun. Itulah alasan libc lama masih bisa dipakai program baru. C++ tidak semudah itu: tipe-tipe library-nya berstruktur rumit, dan standar C++ sengaja tidak mengatur tata letak binernya. Beberapa sumber pergeseran yang nyata:
| Sumber perubahan | Dampak biner |
|---|---|
| Versi compiler/stdlib baru | Tata letak std::string/std::list bisa berubah antar rilis besar |
| Skema mangling berkembang | Simbol fitur baru (module, dsb.) punya bentuk baru |
Flag kompilasi (mis. _GLIBCXX_USE_CXX11_ABI) | GCC 5 mengganti tata letak std::string; dua versi simbol hidup berdampingan |
| Instantiasi template | Setiap compiler/versi menghasilkan kode berbeda untuk template yang sama |
Kasus GCC 5 layak dijadikan contoh nyata: library yang dikompilasi dengan GCC 4 dan program yang dikompilasi dengan GCC 5 sama-sama memakai std::string, tetapi keduanya merujuk simbol yang berbeda (std::__cxx11::basic_string versus versi lama) — program yang salah link pun gagal saat dijalankan, bukan saat dikompilasi.
Apa yang Boleh Menyeberangi Batas Binari #
Dari seluruh pembahasan di atas, lahir satu aturan praktis yang dipakai hampir semua SDK besar (SQLite, Vulkan, Python itu sendiri): di batas biner yang harus stabil, pakai tipe C.
| Antar batas | Aman? | Catatan |
|---|---|---|
int, double, const char* | Ya | Tata letaknya diatur C ABI |
| Pointer opaque ke struct internal | Ya | Pemakai hanya menyimpan dan mengembalikan |
std::string, std::vector | Hanya bila kedua sisi compiler/stdlib/flag-nya identik | Tata letaknya bisa berubah diam-diam |
| Class C++ dengan fungsi virtual | Hanya bila tabel virtual cocok | Bergantung compiler dan versi |
Kapan Kita Perlu Peduli #
Tiga situasi kerja membuat topik ini keluar dari teori. Pertama, plugin: sistem yang memuat .so saat berjalan (editor gambar, game engine) mengikat pengembang plugin ke ABI engine-nya. Kedua, library biner: SDK yang Anda distribusikan tanpa source hanya aman bila antarmukanya C, atau bila Anda menjamin compiler yang sama untuk semua pemakai. Ketiga, campuran compiler: menyatukan object file dari GCC dan MSVC dalam satu program bukan sekadar dilarang — masing-masing memakai skema mangling dan calling convention yang berbeda. Untuk course ini, satu kesimpulan cukup: antarmuka C adalah batas yang aman, dan extern "C" adalah gerbangnya.
Pemahaman ABI ini langsung terpakai pada Chapter berikutnya: ketika kita membangun library dan mengonsumsinya — dari header-only sampai library yang dipakai lintas bahasa.
Referensi: typeid operator di cppreference , Link Options dokumentasi GCC .