Sebuah API adalah kontrak antara penulis modul dan penggunanya. Sekali API dirilis dan ada kode yang memakainya, perubahan apa pun pada API—menambah parameter, mengubah tipe balikan, atau mengganti nama fungsi—akan memutus kode yang sudah ada. Bab ini membahas tiga pilar desain API yang baik di C: minimality (hanya ekspos fungsi yang benar-benar dibutuhkan klien), error handling yang konsisten (pola error code vs return value vs errno), dan ownership semantics yang eksplisit (siapa yang membebaskan memori—caller atau callee?). Kita juga akan melihat mengapa const qualifier adalah bagian dari kontrak API dan bagaimana function pointer memungkinkan inversion of control tanpa membocorkan detail implementasi.

Tiga Pilar API yang Baik #

API C yang sehat mengikuti tiga prinsip yang saling memperkuat:

  1. Minimal: Header publik hanya berisi apa yang klien butuhkan untuk memakai modul. Helper internal ditandai static dan tidak masuk ke header.
  2. Konsisten: Konvensi penamaan, urutan parameter, dan pola error sama di seluruh modul. Jika stack_push mengembalikan int (0=OK, -1=error), maka queue_push juga harus mengembalikan int dengan konvensi yang sama.
  3. Stable: Setelah dirilis, signature fungsi publik tidak boleh berubah. Untuk menambah parameter, buat fungsi baru (mis. stack_push_v2) atau gunakan struct config yang dapat diperluas.

Error Code Terstruktur dengan Enum #

C tidak memiliki exception. Penanganan error dilakukan melalui tiga mekanisme: kode balikan fungsi, parameter output (out parameter), atau variabel global errno. Untuk modul dengan beberapa jenis kegagalan, pola terbaik adalah enum yang menjadi daftar error codes publik.

c
/* File: test_api1.c - Error code terstruktur dengan enum */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Error codes publik: tiap kode = satu kegagalan spesifik */
typedef enum {
    API_OK             =  0,
    API_ERR_INVALID    = -1,   /* argumen NULL atau tidak valid */
    API_ERR_NOMEM      = -2,   /* alokasi memori gagal */
    API_ERR_NOTFOUND   = -3,   /* key tidak ditemukan */
    API_ERR_OVERFLOW   = -4    /* kapasitas penuh */
} api_status_t;

typedef struct {
    char  key[32];
    int   value;
} entry_t;

typedef struct {
    entry_t *entries;
    size_t   count;
    size_t   cap;
} map_t;

static const char *api_strerror(api_status_t s)
{
    switch (s) {
        case API_OK:           return "sukses";
        case API_ERR_INVALID:  return "argumen tidak valid";
        case API_ERR_NOMEM:    return "alokasi memori gagal";
        case API_ERR_NOTFOUND: return "key tidak ditemukan";
        case API_ERR_OVERFLOW: return "kapasitas penuh";
        default:               return "kode tidak dikenal";
    }
}

/* API: dapatkan nilai untuk key tertentu */
static api_status_t map_get(const map_t *m, const char *key, int *out)
{
    if (m == NULL || key == NULL || out == NULL) {
        return API_ERR_INVALID;
    }
    for (size_t i = 0; i < m->count; i++) {
        if (strcmp(m->entries[i].key, key) == 0) {
            *out = m->entries[i].value;
            return API_OK;
        }
    }
    return API_ERR_NOTFOUND;
}

int main(void)
{
    map_t m;
    m.entries = malloc(2 * sizeof(entry_t));
    m.cap = 2;
    m.count = 0;

    strcpy(m.entries[0].key, "alpha");
    m.entries[0].value = 100;
    strcpy(m.entries[1].key, "beta");
    m.entries[1].value = 200;
    m.count = 2;

    int v;
    api_status_t r1 = map_get(&m, "alpha", &v);
    api_status_t r2 = map_get(&m, "gamma", &v);
    api_status_t r3 = map_get(NULL, "alpha", &v);

    printf("get(alpha) -> %d (%s, v=%d)\n", r1, api_strerror(r1),
           r1 == API_OK ? v : -1);
    printf("get(gamma) -> %d (%s)\n", r2, api_strerror(r2));
    printf("get(NULL)  -> %d (%s)\n", r3, api_strerror(r3));

    free(m.entries);
    return 0;
}

Output yang Dihasilkan:

text
get(alpha) -> 0 (sukses, v=100)
get(gamma) -> -3 (key tidak ditemukan)
get(NULL)  -> -1 (argumen tidak valid)

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-14 (api_status_t): Enum publik berisi semua kemungkinan kegagalan. Klien dapat switch (status) dan menangani setiap kasus berbeda—mis. API_ERR_NOTFOUND mungkin memicu pembuatan entry baru, sementara API_ERR_INVALID adalah bug di sisi pemanggil.
  2. Baris 30-42 (api_strerror): Fungsi terjemahan kode ke string mirip dengan strerror(errno). Tanpa fungsi ini, klien harus menulis switch sendiri untuk mencetak error—duplikasi logika di banyak tempat.
  3. Baris 45-56 (map_get): Fungsi API mengembalikan api_status_t, bukan int hasil. Nilai aktual dikirim melalui out parameter int *out. Pola ini memisahkan “apakah berhasil?” (return) dari “apa hasilnya?” (out), sehingga klien tidak bisa salah menganggap 0 sebagai hasil query.
  4. Baris 48-50 (validasi NULL): Cek NULL eksplisit. Tanpa cek, dereferensi NULL di m->count akan crash. API publik tidak boleh mengasumsikan pemanggil benar—pasti ada yang akan mengirim NULL, baik karena bug maupun inisialisasi yang terlewat.
  5. Baris 51-54 (loop strcmp): Pencarian linear sederhana. Untuk map kecil (<100 entry), linear scan lebih cepat dari hash table karena overhead hashing. API tidak bocor detail ini—klien tidak tahu implementasinya linear atau hash, sehingga kita bebas mengganti ke hash tanpa memutus API.

Ownership Semantics: Siapa Membebaskan Memori? #

Salah satu sumber bug paling umum di C adalah kebingungan tentang siapa yang bertanggung jawab membebaskan memori. Jika fungsi A mengalokasikan buffer dan mengembalikannya ke B, apakah B harus free() atau A yang akan free()? API yang baik menyatakan ini secara eksplisit—baik melalui dokumentasi maupun konvensi nama.

Tiga pola ownership yang umum:

  1. Caller-owned: pemanggil mengalokasikan, memanggil fungsi yang mengisi, lalu memanggil yang membebaskan. Fungsi API hanya borrow pointer.
  2. Callee-owned (factory): fungsi API mengalokasikan dan mengembalikan pointer. Pemanggil memakai, lalu memanggil destruktor API untuk membebaskan.
  3. Shared (reference count): ownership dipakai bersama, counter melacak berapa pemakai. Hanya pemanggil terakhir yang free.
c
/* File: test_api2.c - Ownership semantics: caller-owned vs callee-owned */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Buffer = callee-owned (pemanggil tidak bebasaskan) */
typedef struct {
    char  *data;     /* dimiliki oleh buffer_t */
    size_t len;
    size_t cap;
} buffer_t;

static buffer_t *buffer_buat(size_t cap_awal)
{
    buffer_t *b = malloc(sizeof(*b));
    if (b == NULL) return NULL;
    b->data = malloc(cap_awal);
    if (b->data == NULL) {
        free(b);
        return NULL;
    }
    b->len = 0;
    b->cap = cap_awal;
    return b;
}

/* Ownership: caller-owned string yang dikirim tidak di-free oleh buffer */
static int buffer_append(buffer_t *b, const char *str)
{
    size_t n = strlen(str);
    if (b->len + n + 1 > b->cap) {
        size_t new_cap = (b->len + n + 1) * 2;
        char *p = realloc(b->data, new_cap);
        if (p == NULL) return -1;
        b->data = p;
        b->cap = new_cap;
    }
    memcpy(b->data + b->len, str, n);
    b->len += n;
    b->data[b->len] = '\0';
    return 0;
}

/* Destruktor: free internal data juga */
static void buffer_hancurkan(buffer_t *b)
{
    if (b == NULL) return;
    free(b->data);
    free(b);
}

/* Pola borrow (caller-owned) - tidak mentransfer ownership */
static size_t buffer_len(const buffer_t *b)
{
    return b ? b->len : 0;
}

int main(void)
{
    buffer_t *b = buffer_buat(4);
    if (b == NULL) {
        fprintf(stderr, "buffer_buat gagal\n");
        return 1;
    }
    /* String literal "halo" dan " dunia" - caller-owned, tidak difree */
    buffer_append(b, "halo");
    buffer_append(b, " dunia");
    buffer_append(b, " dari C");

    printf("isi buffer : \"%s\"\n", b->data);
    printf("panjang    : %zu\n", buffer_len(b));
    printf("kapasitas  : %zu\n", b->cap);

    buffer_hancurkan(b);
    /* Setelah ini, b tidak boleh dipakai - callee-owned sepenuhnya */
    return 0;
}

Output yang Dihasilkan:

text
isi buffer : "halo dunia dari C"
panjang    : 17
kapasitas  : 22

Pada contoh di atas, buffer_t adalah callee-owned: fungsi buffer_buat mengalokasikan, fungsi buffer_hancurkan membebaskan. Pemanggil tidak pernah memanggil free(b) langsung—jika dilakukan, destruktor tidak akan jalan dan b->data bocor. Konvensi nama membantu: _buat adalah factory, _hancurkan adalah destruktor, parameter yang berbau “dapat diubah” memakai non-const pointer.

Out Parameter vs Return Value #

C tidak dapat mengembalikan tuple seperti Go (value, err) atau Rust (Result<T, E>). Untuk fungsi yang mengembalikan data sekaligus status, ada dua pilihan: return error dan kirim data via out parameter, atau return data dan kirim error via errno. Pilihan tergantung kompleksitas error.

c
/* File: test_api3.c - Out parameter vs return value untuk error */
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

/* Versi 1: return value = error, hasil via out parameter.
   Cocok untuk tipe primitive (int, double) yang tidak bisa bawa error. */
static int parse_int(const char *s, int *out)
{
    char *end;
    long v = strtol(s, &end, 10);
    if (*end != '\0' || end == s) return -1;
    if (v < INT_MIN || v > INT_MAX) return -2;
    *out = (int)v;
    return 0;
}

/* Versi 2: return value = hasil, error via errno (stdlib pattern).
   Cocok untuk fungsi sederhana tanpa banyak kasus error. */
static int square(int x)
{
    return x * x;
}

int main(void)
{
    int v;
    int rc = parse_int("42", &v);
    if (rc == 0) {
        printf("parse_int(42) -> %d\n", v);
    }

    rc = parse_int("bukan angka", &v);
    printf("parse_int(bukan angka) -> rc=%d\n", rc);

    printf("square(7) = %d\n", square(7));
    return 0;
}

Output yang Dihasilkan:

text
parse_int(42) -> 42
parse_int(bukan angka) -> rc=-1
square(7) = 49

Pada contoh di atas, parse_int memakai pola out parameter karena ada dua kemungkinan kegagalan (input bukan angka, atau angka di luar rentang int). square memakai pola return value karena tidak ada kasus error yang berarti—x * x selalu menghasilkan nilai (meskipun dapat overflow, yang menjadi tanggung jawab pemanggil untuk mencegah).

Aturan const sebagai Kontrak API #

Qualifier const di parameter fungsi adalah bagian dari kontrak API. Kompiler menolak kode yang mencoba memodifikasi const pointer, sehingga pengguna API tahu bahwa fungsi tersebut tidak akan mengubah data mereka. Aturan praktis:

  • Parameter input yang tidak diubah: const T * (pointer konstan ke data).
  • Parameter output yang diisi fungsi: T *out (pointer non-konstan).
  • Parameter input/output: T *inout (non-konstan karena fungsi membaca dan menulis).
  • Pointer yang tidak boleh di-NULL: dokumentasikan dengan jelas; T * (tanpa const) tidak berarti “boleh NULL”.