Mendesain API library C berkualitas produksi melibatkan keputusan yang berbeda dari menulis kode aplikasi sekali pakai. API library harus jelas tentang kepemilikan memori (ownership), mendiagnostik error secara konsisten, mengikuti pola init/cleanup, dan menyatakan versi lewat SONAME. Library yang baik tidak hanya berfungsi; library yang baik dapat dipakai bertahun-tahun tanpa memutus konsumen lama. Pada chapter ini kita akan membangun library tokodb (key-value store sederhana) yang mempraktikkan empat pilar desain API: error code terdefinisi, ownership eksplisit, init/cleanup, dan dokumentasi yang jelas di header.
Empat Pilar API Library C #
Sebelum menulis kode, kita perlu memutuskan empat hal fundamental:
- Error code: Setiap fungsi yang dapat gagal harus mengembalikan kode error yang terdefinisi. Hindari mengembalikan pointer non-NULL dengan side-effect error tersembunyi.
- Ownership eksplisit: Setiap parameter pointer harus didokumentasikan: apakah pemanggil atau library yang bertanggung jawab membebaskannya? String input disalin atau dipakai langsung?
- Pola init/cleanup: Setiap resource (handle, koneksi, buffer) dibuat oleh fungsi factory dan dihancurkan oleh fungsi destructor. Pasangan ini wajib untuk opaque handle.
- Versioning via SONAME: Versi library ditandai lewat soname (
libtokodb.so.1) sehingga konsumen lama tetap dapat link ke versi lama bila versi baru datang dengan ABI breaking. Pembahasan penuh di chapter Versioning C API.
Header Sebagai Kontrak #
Header library adalah dokumen kontrak. Setiap komentar dan deklarasi harus cukup jelas sehingga konsumen dapat memakai library tanpa membaca file .c. Berikut header tokodb yang menerapkan empat pilar di atas.
/* File: tokodb.h - API bersih dengan error code dan ownership eksplisit */
#ifndef TOKODB_H
#define TOKODB_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Error code: integer negatif untuk error, 0 untuk sukses */
typedef enum {
TOKODB_OK = 0,
TOKODB_ERR_ARG_NULL = -1,
TOKODB_ERR_ARG_INVALID = -2,
TOKODB_ERR_NOMEM = -3,
TOKODB_ERR_NOT_FOUND = -4,
TOKODB_ERR_IO = -5
} TokodbStatus;
/* Opaque handle */
typedef struct Tokodb Tokodb;
/* Init/cleanup pattern */
Tokodb *tokodb_buat(void);
void tokodb_hancurkan(Tokodb *db);
/* Ownership:
* - kunci string "k" disalin internal; pemanggil tetap punya kunci sendiri
* - nilai string "v" disalin internal
* - tokodb_ambil mengembalikan pointer internal, JANGAN di-free oleh pemanggil */
TokodbStatus tokodb_simpan(Tokodb *db, const char *kunci, const char *nilai);
const char *tokodb_ambil(const Tokodb *db, const char *kunci, size_t *panjang_keluar);
/* Helper: konversi error code menjadi pesan */
const char *tokodb_status_pesan(TokodbStatus s);
#ifdef __cplusplus
}
#endif
#endif /* TOKODB_H */Mari kita telaah baris-baris kode di atas:
- Baris 13-20 (
typedef enum TokodbStatus): Semua kemungkinan outcome didefinisikan sebagai enum. Kode suksesTOKODB_OK = 0, kode error bernilai negatif. Konvensi ini memudahkan pemanggil menulisif (s != TOKODB_OK) { ... }. Tipe enum diintrodusir dengantypedefsupaya pemanggil dapat menulisTokodbStatustanpa keywordenum. - Baris 23 (
typedef struct Tokodb Tokodb;): Opaque pointer—detail struct disembunyikan. Konsumen hanya memegang pointer. Lihat chapter Opaque Pointers untuk pembahasan pola ini. - Baris 26-27 (
tokodb_buatdantokodb_hancurkan): Pasangan init/cleanup.tokodb_buatmengembalikanNULLbila gagal, dan pemanggil harus menguji sebelum dipakai.tokodb_hancurkanmenerimaNULLdengan aman (lihat implementasi—mengandalkanif (!db) return;). - Baris 32-33 (
tokodb_simpandantokodb_ambil): Operasi inti. Komentar blok sebelum deklarasi menjelaskan ownership: string input disalin internal, jadi pemanggil bebas membebaskan sendiri.tokodb_ambilmengembalikan pointer internal yang harus tidak di-free pemanggil. Kontrak ini krusial—pelanggaran menyebabkan double-free atau use-after-free. - Baris 36 (
tokodb_status_pesan): Helper untuk menerjemahkan kode error ke teks. Konsumen tidak perlu menulisswitchsendiri. Pola ini dipakai olehstrerrordi POSIX dan oleh hampir semua library C modern.
Implementasi dan Error Checking #
Implementasi tokodb.c menunjukkan bagaimana error code dipakai di setiap jalur kegagalan. Setiap call ke malloc diperiksa; setiap argumen divalidasi sebelum dipakai.
/* File: tokodb_main.c - konsumsi API dengan error checking */
#include <stdio.h>
#include "tokodb.h"
int main(void) {
Tokodb *db = tokodb_buat();
if (!db) {
fprintf(stderr, "Inisialisasi gagal\n");
return 1;
}
TokodbStatus s1 = tokodb_simpan(db, "nama", "Budi");
TokodbStatus s2 = tokodb_simpan(db, "kota", "Bandung");
printf("simpan nama: %s\n", tokodb_status_pesan(s1));
printf("simpan kota: %s\n", tokodb_status_pesan(s2));
/* Error: argumen null */
TokodbStatus s3 = tokodb_simpan(db, NULL, "Cici");
printf("simpan NULL: %s (kode %d)\n", tokodb_status_pesan(s3), (int) s3);
/* Baca */
size_t n = 0;
const char *v = tokodb_ambil(db, "nama", &n);
if (v) printf("nama = %s (panjang %zu)\n", v, n);
else printf("nama tidak ditemukan\n");
const char *w = tokodb_ambil(db, "tidak_ada", NULL);
printf("cari tidak_ada: %s\n", w ? w : "(null)");
tokodb_hancurkan(db);
printf("database ditutup.\n");
return 0;
}Compile dan jalankan (dengan asumsi tokodb.c ada di direktori yang sama):
gcc -std=c11 -Wall -Wextra -o tokodb_main tokodb_main.c tokodb.c
./tokodb_mainOutput yang Dihasilkan:
simpan nama: sukses
simpan kota: sukses
simpan NULL: argumen null (kode -1)
nama = Budi (panjang 4)
cari tidak_ada: (null)
database ditutup.Mari kita telaah baris-baris kode di atas:
- Baris 5-9 (
tokodb_buat+ cek NULL): Pola wajib untuk setiap factory. Setelahtokodb_buat, selalu ujiif (!db)sebelum melanjutkan. Bila malloc gagal, program tidak crash di tempat lain yang sulit dilacak. - Baris 11-13 (
tokodb_simpandua kali): Pemanggil mengabaikan kode error karena hanya demo. Pada kode produksi, setiap call harus diperiksa:if (s1 != TOKODB_OK) { handle error; }. Library tidak boleh menyembunyikan kegagalan—panggilan fungsi yang “berhasil” diam-diam adalah sumber bug. - Baris 17-18 (
tokodb_simpan(db, NULL, ...)): Library menolak argumen NULL denganTOKODB_ERR_ARG_NULL (-1). Bandingkan dengan mengembalikan pointer NULL saja—kode error memberi tahu pemanggil persis apa yang salah, bukan sekadar “gagal”. - Baris 21-22 (
tokodb_ambildengan parameter keluaran): Parametersize_t *panjang_keluarmengembalikan panjang string tanpa pemanggil harusstrlen. Bila pemanggil tidak peduli, ia kirimNULL. Pola “optional out parameter” ini umum di API C (lihatwrite()POSIX yang menerimassize_t *opsional). - Baris 26 (
tokodb_hancurkan(db)): Wajib di akhir. Tanpa call ini, memori internalTokodb(arrayentry, semua stringkuncidannilai) bocor. Pola opaque pointer memaksa pemanggil lewat destructor.
Pola Lengkap Init/Cleanup #
Init/cleanup pattern adalah konvensi bahwa setiap resource dibuat oleh pasangan *_buat/*_hancurkan (atau *_init/*_fini / *_new/*_free). Pola ini dipakai oleh fopen/fclose, sqlite3_open/sqlite3_close, curl_easy_init/curl_easy_cleanup. Tiga aturan penting:
- Destructor menerima NULL dengan aman. Implementasi
tokodb_hancurkandiawaliif (!db) return;sehinggatokodb_hancurkan(NULL)bukan bug. Sama sepertifree(NULL)yang legal di C. - Destructor idempoten. Memanggil dua kali pada pointer yang sama tidak boleh crash—setelah panggilan pertama, resource sudah dibebaskan. Pemanggil bertanggung jawab tidak memakai handle setelah destroy.
- Factory mengembalikan NULL bila gagal, bukan exit(). Pemanggil yang berhak memutuskan apakah error fatal atau tidak.