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:

cpp simbol.cpp
#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;
}
bash
g++ -std=c++23 -Wall -Wextra -c simbol.cpp -o simbol.o
nm simbol.o | grep ' T ' | awk '{print $2, $3}'

Output yang Dihasilkan:

text
T _Z6ukuranRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
T _Z6ukurani
T ukuran_c

Simbol 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:

bash
nm simbol.o | grep ' T ' | awk '{print $2, $3}' | c++filt

Output yang Dihasilkan:

text
T ukuran(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
T ukuran(int)
T ukuran_c

Inilah 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:

cpp
// 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:

text
N3app8KaryawanE  =>  app::Karyawan
_ZN3app9totalGajiERKSt6vectorIiSaIiEE  =>  app::totalGaji(std::vector<int, std::allocator<int> > const&)

Bedah singkatnya:

  1. Baris 12-13 (namespace app) — struct dan fungsi di dalam namespace; tanpa mangling, dua proyek yang sama-sama punya Karyawan akan bertabrakan di linker.
  2. Baris 17-22 (demangle) — __cxa_demangle menerima nama mangled, buffer, panjang, dan status; hasilnya char* milik kita, sehingga pembungkus std::unique_ptr dengan deleter std::free memastikan memori dibebaskan — pola RAII yang sudah lama kita kenal.
  3. Baris 27 (typeid(...).name()) — pada GCC, nama type yang dikembalikan adalah mangled: N3app8KaryawanE berarti nested app::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 perubahanDampak biner
Versi compiler/stdlib baruTata letak std::string/std::list bisa berubah antar rilis besar
Skema mangling berkembangSimbol 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 templateSetiap 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 batasAman?Catatan
int, double, const char*YaTata letaknya diatur C ABI
Pointer opaque ke struct internalYaPemakai hanya menyimpan dan mengembalikan
std::string, std::vectorHanya bila kedua sisi compiler/stdlib/flag-nya identikTata letaknya bisa berubah diam-diam
Class C++ dengan fungsi virtualHanya bila tabel virtual cocokBergantung 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 .