Dalam konteks library native seperti C atau C++, API dan ABI adalah dua lapisan kompatibilitas yang berbeda. Banyak tim hanya menguji bahwa source code consumer masih bisa dikompilasi dan perilaku fungsi masih benar, padahal paket yang sudah terpasang di mesin pengguna sering bergantung pada binary compatibility. Akibatnya, rilis terlihat aman di level source, tetapi program yang sudah ditautkan ke versi lama bisa gagal saat dijalankan atau menghasilkan perilaku yang tidak valid.

Intinya: test yang hanya memverifikasi API sering gagal menangkap breakage ABI. Untuk library native, strategi regression test harus mencakup verifikasi perilaku, integrasi, dan perubahan antarmuka biner. Ini bukan topik baru; diskusi lama di ekosistem Linux dan Debian juga menekankan bahwa API compatibility tidak otomatis berarti ABI compatibility, misalnya dalam percakapan teknis yang bisa dijadikan konteks historis di arsip Debian: debian-user 2004.

Apa beda API dan ABI?

API: kontrak di level source code

API adalah bentuk antarmuka yang dilihat developer saat menulis kode: nama fungsi, parameter, tipe data, konstanta, header, class, method, dan semantik perilaku. Jika consumer perlu mengubah source code agar cocok dengan versi baru library, biasanya itu masalah API.

Contoh API:

int parse_config(const char *path, struct config *out);

Jika fungsi di atas diubah menjadi:

int parse_config(const char *path, struct config *out, int flags);

itu jelas memutus API karena source code lama tidak akan lolos kompilasi tanpa perubahan.

ABI: kontrak di level binary

ABI adalah cara antarmuka itu direpresentasikan setelah dikompilasi: nama simbol, calling convention, ukuran dan layout struct, alignment, vtable, exception model, dependensi simbol pada shared library lain, hingga detail toolchain tertentu. ABI menentukan apakah program yang sudah dikompilasi terhadap versi lama masih bisa berjalan dengan library versi baru tanpa recompile.

Dalam praktiknya, ABI sangat penting untuk:

  • shared library yang dipakai banyak aplikasi,
  • paket sistem yang diperbarui independen dari aplikasinya,
  • plugin atau module yang dimuat dinamis,
  • SDK native yang dipakai consumer eksternal.

Aturan praktis: API berbicara tentang source compatibility, sedangkan ABI berbicara tentang binary compatibility. Library native yang stabil perlu memikirkan keduanya.

Mengapa test API saja tidak cukup?

Unit test dan integration test yang dibangun bersama source library biasanya melakukan dua hal:

  • memastikan fungsi masih berperilaku benar,
  • memastikan contoh consumer yang dikompilasi ulang terhadap header baru masih berjalan.

Masalahnya, banyak pengguna tidak selalu mengompilasi ulang aplikasinya saat library diperbarui. Mereka cukup melakukan upgrade paket shared library. Jika ABI berubah, biner lama bisa mengalami:

  • gagal start karena simbol hilang,
  • segmentation fault karena layout data berubah,
  • hasil salah karena parameter dibaca dengan urutan atau ukuran berbeda,
  • bug intermittent yang sulit direproduksi karena kerusakan memori.

Inilah alasan mengapa test yang hanya memverifikasi API sering memberikan rasa aman yang salah. Semua test hijau di pipeline, tetapi deployment memecahkan aplikasi yang tidak ikut dibangun ulang.

Contoh perubahan yang kompatibel di source level tetapi merusak ABI

1. Mengubah layout struct publik

Ini kasus klasik. Source code consumer mungkin tetap lolos kompilasi, tetapi binary lama yang mengakses field berdasarkan offset lama akan rusak.

/* v1 */
struct session {
    int fd;
    long timeout_ms;
};
/* v2 */
struct session {
    long timeout_ms;
    int fd;
};

Secara source, kode yang menulis s.fd dan s.timeout_ms masih tampak valid jika dikompilasi ulang. Namun aplikasi yang sudah dikompilasi terhadap layout v1 dan dijalankan dengan library v2 dapat membaca offset yang salah.

2. Menambah field ke struct yang diakses langsung consumer

/* v1 */
struct config {
    int retries;
};

/* v2 */
struct config {
    int retries;
    int backoff_ms;
};

Jika struct ini dialokasikan oleh consumer atau diserialisasi dengan asumsi ukuran tertentu, perubahan ukuran dapat memicu overwrite, pembacaan memori tak valid, atau kegagalan interoperabilitas.

3. Mengubah signature dengan tipe yang tampak mirip

/* v1 */
long get_value(void);

/* v2 */
int get_value(void);

Source level mungkin cepat diperbaiki atau bahkan tidak terlihat bermasalah di beberapa konteks, tetapi perubahan ukuran return value atau register yang dipakai dapat merusak ABI, tergantung platform dan ABI calling convention yang berlaku.

4. Mengubah class C++ publik

Pada C++, ABI lebih rapuh. Menambah virtual method, mengubah urutan field, mengganti base class, atau memodifikasi inline behavior tertentu bisa mengubah layout objek dan vtable.

// v1
class Writer {
public:
    virtual ~Writer() = default;
    virtual void write(const std::string& s);
};

// v2
class Writer {
public:
    virtual ~Writer() = default;
    virtual void flush();
    virtual void write(const std::string& s);
};

Secara source, consumer yang dikompilasi ulang mungkin baik-baik saja. Tetapi binary lama yang mengandalkan susunan vtable v1 dapat memanggil fungsi yang salah.

5. Menghapus atau me-rename simbol yang diekspor

Ini paling mudah terlihat saat runtime:

  • aplikasi lama mencari simbol foo_init,
  • library baru hanya mengekspor foo_initialize,
  • hasilnya: linker dinamis gagal memuat library atau program gagal start.

Strategi regression test untuk library native

Strategi yang efektif bukan memilih antara test API atau ABI, melainkan menyusun beberapa lapisan verifikasi yang saling melengkapi.

1. Unit test untuk perilaku dan regresi logika

Unit test tetap fondasi utama. Gunakan untuk memverifikasi:

  • hasil fungsi,
  • error handling,
  • parsing, encoding, serialization,
  • thread-safety bila relevan,
  • perbaikan bug yang pernah terjadi.

Namun pahami keterbatasannya: unit test umumnya dibangun dan dilink terhadap source/header yang sedang diuji, sehingga kurang efektif mendeteksi kompatibilitas biner terhadap consumer lama.

2. Integration test dengan consumer contoh

Buat satu atau lebih program consumer kecil yang menggunakan library Anda seperti pengguna nyata. Jalankan dua mode pengujian:

  1. Build against new headers, run with new library untuk memvalidasi API dan perilaku dasar.
  2. Build against old release, run with new library untuk memvalidasi ABI.

Mode kedua sering diabaikan, padahal justru paling penting untuk mendeteksi breakage biner.

Contoh alur sederhana di CI:

# 1. Build consumer dengan library/header rilis lama
./scripts/build-consumer-against-old-release.sh

# 2. Ganti shared library runtime ke kandidat rilis baru
./scripts/install-new-library-artifact.sh

# 3. Jalankan binary lama
./artifacts/consumer_old_build/smoke_test

Jika binary lama gagal start, gagal resolve simbol, atau hasilnya berubah tanpa alasan, itu sinyal kuat ada isu ABI atau perilaku kompatibilitas.

3. ABI diff check antar rilis

Gunakan pemeriksaan ABI sebagai gate otomatis sebelum rilis. Tujuannya membandingkan artefak biner lama dan baru untuk mendeteksi perubahan seperti:

  • simbol yang hilang atau berubah,
  • perubahan signature yang memengaruhi ABI,
  • perubahan layout type publik,
  • perubahan kompatibilitas class C++.

Di ekosistem Linux, tim sering memakai pendekatan berbasis symbol diff atau tool analisis ABI. Nama tool dapat berbeda sesuai stack dan bahasa, tetapi prinsipnya sama: bandingkan antarmuka biner rilis sebelumnya dengan kandidat rilis sekarang, lalu definisikan aturan kapan perubahan harus memblokir merge atau mewajibkan major version bump.

Yang perlu diperhatikan:

  • ABI diff tidak menggantikan test perilaku; ia hanya melihat bentuk kontrak biner.
  • Untuk C++, hasil bisa sensitif terhadap compiler dan standard library.
  • Jika library Anda memang belum menjanjikan ABI stabil, dokumentasikan kebijakannya secara eksplisit.

4. Matrix compiler dan platform

ABI tidak selalu identik di semua environment. Perbedaan compiler, arsitektur CPU, standard library, dan OS dapat mengubah detail layout, mangling, alignment, atau linking.

Minimal, pertimbangkan matrix untuk:

  • compiler utama yang didukung,
  • arsitektur yang relevan,
  • mode debug/release jika perilaku build berbeda,
  • shared vs static linking bila Anda mendukung keduanya.

Tujuannya bukan menguji semua kombinasi tanpa batas, melainkan menutup kombinasi yang benar-benar dipakai consumer produksi. Jika mayoritas pengguna berada pada satu toolchain tertentu, matrix harus memprioritaskan itu.

5. Contract test untuk consumer penting

Jika library Anda dipakai oleh beberapa aplikasi internal atau pelanggan utama, buat contract test yang merepresentasikan pola pemakaian nyata. Ini lebih kuat daripada sample app generik karena mencerminkan asumsi yang benar-benar dipakai consumer.

Contract test bisa mencakup:

  • urutan pemanggilan API yang umum,
  • penggunaan struct publik,
  • loading plugin atau callback,
  • error contract, timeout, dan resource lifecycle.

Bila memungkinkan, simpan test ini di repositori consumer atau jalankan sebagai downstream verification saat ada kandidat rilis library.

6. Gate di CI/CD

Semua lapisan di atas perlu dijadikan keputusan otomatis, bukan checklist manual menjelang rilis. Contoh gate yang masuk akal:

  • Wajib lulus: unit test, integration test dasar, sanitizers bila tersedia.
  • Wajib lulus untuk rilis minor/patch: ABI diff tidak menunjukkan breakage yang tidak diizinkan.
  • Wajib lulus untuk rilis mayor: ABI break boleh terjadi, tetapi harus terdokumentasi dan divalidasi terhadap migration guide.
  • Wajib lulus untuk branch release: consumer contract test utama.

Dengan pola ini, tim tidak bergantung pada ingatan reviewer untuk menilai apakah sebuah perubahan aman terhadap kompatibilitas biner.

Contoh kebijakan rilis yang praktis

Berikut contoh kebijakan yang dapat diterapkan pada library native dengan target stabil:

  1. Perubahan yang memengaruhi simbol publik atau type publik harus diberi label review khusus.
  2. Setiap pull request yang menyentuh header publik menjalankan ABI diff check terhadap rilis terakhir.
  3. Jika ABI break terdeteksi pada rilis patch atau minor, pipeline gagal kecuali ada override yang disetujui untuk major release.
  4. Setiap kandidat rilis menjalankan binary consumer yang dibangun terhadap rilis sebelumnya.
  5. Untuk consumer prioritas tinggi, jalankan downstream contract test sebelum publish artifact final.

Kebijakan ini sederhana, tetapi efektif karena mengikat perubahan teknis ke aturan rilis yang bisa dipahami semua pihak.

Desain library agar lebih tahan terhadap breakage ABI

Regression test membantu mendeteksi masalah, tetapi desain API/ABI yang hati-hati akan mengurangi risiko sejak awal.

1. Sembunyikan detail implementasi

Untuk C, pertimbangkan opaque pointer daripada membuka layout struct ke consumer.

/* public header */
struct session;

struct session *session_create(void);
void session_destroy(struct session *s);
int session_set_timeout(struct session *s, long timeout_ms);

Dengan pendekatan ini, Anda dapat mengubah isi struct session di implementasi tanpa memecahkan layout yang dilihat consumer.

2. Minimalkan class C++ publik lintas boundary

Jika ABI stabil sangat penting, banyak tim memilih antarmuka C yang tipis di boundary publik, lalu implementasi internal tetap memakai C++. Alasannya sederhana: ABI C cenderung lebih stabil dan lebih mudah dianalisis daripada ABI C++.

3. Hindari expose konstanta ukuran dan layout yang mudah berubah

Begitu ukuran struct, enum, atau format memori menjadi bagian dari kontrak publik, ruang evolusi library menjadi lebih sempit.

4. Kelola symbol visibility

Ekspor hanya simbol yang memang menjadi API publik. Semakin sedikit permukaan ABI yang diekspor, semakin kecil kemungkinan breakage tak sengaja.

Checklist pencegahan flaky test untuk kompatibilitas lintas environment

Pengujian ABI dan kompatibilitas lintas environment mudah menjadi flaky jika pipeline tidak disiplin. Gunakan checklist berikut:

  • Pin environment build dan runtime untuk job yang membandingkan rilis lama dan baru.
  • Bedakan artifact build dan artifact runtime; jangan tanpa sengaja melink consumer lama terhadap header/library baru saat seharusnya menguji ABI.
  • Gunakan container atau image yang konsisten untuk platform yang sama.
  • Catat compiler, linker, dan standard library yang dipakai pada tiap job.
  • Hindari dependency jaringan pada test kompatibilitas kecuali memang bagian kontrak.
  • Nonaktifkan sumber nondeterminisme seperti random seed yang tidak tetap, waktu sistem yang berubah, locale, atau timezone.
  • Pastikan path loading library eksplisit agar binary benar-benar memakai artifact yang sedang diuji, bukan library sistem yang kebetulan terpasang.
  • Bersihkan cache build jika job menguji kombinasi old/new header dan old/new runtime.
  • Validasi simbol yang termuat dengan tool inspeksi biner bila ada dugaan library yang salah termuat.
  • Pisahkan test ABI dari test performa; noise performa sering mengaburkan diagnosis kompatibilitas.
  • Jalankan ulang otomatis pada kegagalan yang diketahui rentan environment hanya jika penyebab nondeterministik sudah dipahami, bukan sebagai penutup masalah desain test.
  • Simpan artifact debugging seperti binary consumer, shared object, log loader, dan hasil inspeksi simbol.

Debugging saat dicurigai ada breakage ABI

Ketika test API lulus tetapi consumer lama rusak, fokuskan investigasi pada antarmuka biner:

  1. Periksa apakah simbol yang diekspor masih ada dan namanya sama.
  2. Bandingkan header publik dan type publik yang berubah antar rilis.
  3. Periksa ukuran dan alignment struct yang dibagikan lintas boundary.
  4. Uji binary lama dengan kandidat library baru secara terisolasi.
  5. Jika C++, curigai perubahan vtable, inheritance, exception boundary, atau inline behavior publik.
  6. Cek apakah kegagalan hanya muncul pada compiler atau arsitektur tertentu.

Banyak kasus ABI break tidak terlihat dari pesan error yang ramah. Karena itu, menyimpan baseline artefak rilis lama dan punya test consumer yang benar-benar dibangun terhadap baseline adalah investasi yang sangat berguna.

Kapan cukup menguji API saja?

Jika library Anda:

  • selalu didistribusikan bersama aplikasi dan selalu dibangun ulang bersamaan,
  • tidak menyediakan shared library untuk pihak ketiga,
  • tidak menjanjikan kompatibilitas antar rilis,

maka fokus utama mungkin cukup pada API dan perilaku. Tetapi begitu library menjadi dependensi lintas tim, lintas layanan, atau paket sistem yang di-upgrade independen, ABI harus masuk ke strategi regression test.

Penutup

Perbedaan API vs ABI untuk strategi regression test library native bukan sekadar istilah akademis. API memastikan developer masih bisa menulis dan mengompilasi kode. ABI memastikan binary yang sudah ada tetap bisa berjalan aman setelah library diperbarui. Di dunia native, keduanya perlu diuji secara terpisah.

Strategi yang paling masuk akal adalah menggabungkan unit test, integration test, ABI diff check, matrix compiler/platform, contract test untuk consumer, dan gate di CI/CD. Dengan pendekatan ini, tim tidak hanya menangkap bug fungsional, tetapi juga mencegah regresi kompatibilitas yang biasanya baru terlihat setelah rilis menyentuh environment pengguna.