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:

  1. Error code: Setiap fungsi yang dapat gagal harus mengembalikan kode error yang terdefinisi. Hindari mengembalikan pointer non-NULL dengan side-effect error tersembunyi.
  2. Ownership eksplisit: Setiap parameter pointer harus didokumentasikan: apakah pemanggil atau library yang bertanggung jawab membebaskannya? String input disalin atau dipakai langsung?
  3. Pola init/cleanup: Setiap resource (handle, koneksi, buffer) dibuat oleh fungsi factory dan dihancurkan oleh fungsi destructor. Pasangan ini wajib untuk opaque handle.
  4. 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.

c
/* 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:

  1. Baris 13-20 (typedef enum TokodbStatus): Semua kemungkinan outcome didefinisikan sebagai enum. Kode sukses TOKODB_OK = 0, kode error bernilai negatif. Konvensi ini memudahkan pemanggil menulis if (s != TOKODB_OK) { ... }. Tipe enum diintrodusir dengan typedef supaya pemanggil dapat menulis TokodbStatus tanpa keyword enum.
  2. Baris 23 (typedef struct Tokodb Tokodb;): Opaque pointer—detail struct disembunyikan. Konsumen hanya memegang pointer. Lihat chapter Opaque Pointers untuk pembahasan pola ini.
  3. Baris 26-27 (tokodb_buat dan tokodb_hancurkan): Pasangan init/cleanup. tokodb_buat mengembalikan NULL bila gagal, dan pemanggil harus menguji sebelum dipakai. tokodb_hancurkan menerima NULL dengan aman (lihat implementasi—mengandalkan if (!db) return;).
  4. Baris 32-33 (tokodb_simpan dan tokodb_ambil): Operasi inti. Komentar blok sebelum deklarasi menjelaskan ownership: string input disalin internal, jadi pemanggil bebas membebaskan sendiri. tokodb_ambil mengembalikan pointer internal yang harus tidak di-free pemanggil. Kontrak ini krusial—pelanggaran menyebabkan double-free atau use-after-free.
  5. Baris 36 (tokodb_status_pesan): Helper untuk menerjemahkan kode error ke teks. Konsumen tidak perlu menulis switch sendiri. Pola ini dipakai oleh strerror di 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.

c
/* 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):

bash
gcc -std=c11 -Wall -Wextra -o tokodb_main tokodb_main.c tokodb.c
./tokodb_main

Output yang Dihasilkan:

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

  1. Baris 5-9 (tokodb_buat + cek NULL): Pola wajib untuk setiap factory. Setelah tokodb_buat, selalu uji if (!db) sebelum melanjutkan. Bila malloc gagal, program tidak crash di tempat lain yang sulit dilacak.
  2. Baris 11-13 (tokodb_simpan dua 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.
  3. Baris 17-18 (tokodb_simpan(db, NULL, ...)): Library menolak argumen NULL dengan TOKODB_ERR_ARG_NULL (-1). Bandingkan dengan mengembalikan pointer NULL saja—kode error memberi tahu pemanggil persis apa yang salah, bukan sekadar “gagal”.
  4. Baris 21-22 (tokodb_ambil dengan parameter keluaran): Parameter size_t *panjang_keluar mengembalikan panjang string tanpa pemanggil harus strlen. Bila pemanggil tidak peduli, ia kirim NULL. Pola “optional out parameter” ini umum di API C (lihat write() POSIX yang menerima ssize_t * opsional).
  5. Baris 26 (tokodb_hancurkan(db)): Wajib di akhir. Tanpa call ini, memori internal Tokodb (array entry, semua string kunci dan nilai) 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:

  1. Destructor menerima NULL dengan aman. Implementasi tokodb_hancurkan diawali if (!db) return; sehingga tokodb_hancurkan(NULL) bukan bug. Sama seperti free(NULL) yang legal di C.
  2. 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.
  3. Factory mengembalikan NULL bila gagal, bukan exit(). Pemanggil yang berhak memutuskan apakah error fatal atau tidak.