Chapter-chapter sebelumnya memakai API POSIX (pthread, fork, pipe, signal) yang berjalan di Linux, macOS, dan BSD. Windows memiliki API sendiri (CreateThread, CreateProcess, CreatePipe, SetConsoleCtrlHandler) yang tidak kompatibel. Untuk menulis kode sistem yang berjalan di kedua platform, kita memakai conditional compilation lewat macro #ifdef _WIN32 dan #ifdef __linux__. Pada chapter ini kita menulis tiga demo konkret: pertama mendeteksi platform saat compile, kedua membungkus thread+mutex dengan typedef dan helper function, ketiga menyediakan sleep milidetik yang portabel. Pola ini adalah inti dari library portabel seperti SDL, libuv, dan bagian inti banyak library open source.

Mendeteksi Platform saat Compile #

Compiler C mengatur beberapa macro otomatis berdasarkan target. Tiga macro paling sering dipakai:

  • __linux__: didefinisikan bila target adalah Linux
  • _WIN32: didefinisikan bila target adalah Windows (32-bit atau 64-bit)
  • __APPLE__: didefinisikan bila target adalah macOS atau iOS
  • __STDC_VERSION__: versi standar C yang dipatuhi compiler (201112L untuk C11, 202311L untuk C23)
c
/* File: deteksi_platform.c - mengetahui platform saat compile */
#include <stdio.h>

int main(void) {
    printf("Deteksi platform:\n");
    printf("  __linux__       = %d\n",
#ifdef __linux__
           1
#else
           0
#endif
          );
    printf("  _WIN32          = %d\n",
#ifdef _WIN32
           1
#else
           0
#endif
          );
    printf("  __APPLE__       = %d\n",
#ifdef __APPLE__
           1
#else
           0
#endif
          );
    printf("  __STDC_VERSION__ = %ldL\n", __STDC_VERSION__);

    printf("\nDetail platform:\n");
#ifdef __linux__
    printf("  Berjalan di Linux (POSIX)\n");
#elif defined(_WIN32)
    printf("  Berjalan di Windows (Win32)\n");
#elif defined(__APPLE__)
    printf("  Berjalan di macOS (POSIX/BSD)\n");
#else
    printf("  Platform tidak dikenali\n");
#endif

    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
Deteksi platform:
  __linux__       = 1
  _WIN32          = 0
  __APPLE__       = 0
  __STDC_VERSION__ = 201112L

Detail platform:
  Berjalan di Linux (POSIX)

Mari kita telaah baris-baris kode di atas:

  1. Baris 6-9 (#ifdef __linux__ ... 1 ... #else ... 0 ... #endif): Pola ini mengkonversi keberadaan macro menjadi literal 1 atau 0 sehingga bisa dicetak dengan printf. Tanpa trik ini, #ifdef hanya bekerja di tingkat preprocessor, tidak bisa dipakai sebagai expression di kode C biasa.
  2. __STDC_VERSION__: Macro standar C. Nilai 201112L berarti compiler mematuhi C11. GCC 14 juga mendukung C17 (201710L) dan C23 (202311L) bila dipanggil dengan -std=c17 atau -std=c23.
  3. Baris 30-39 (#ifdef __linux__ ... #elif defined(_WIN32) ... #elif defined(__APPLE__) ... #endif): Pola percabangan compile-time. Penting: gunakan #elif defined(...) (dengan tanda kurung), bukan #elif __APPLE__, karena #elif tidak otomatis mengecek defined() seperti #ifdef.
  4. Catatan untuk _WIN32: Macro ini didefinisikan untuk kedua Windows 32-bit dan 64-bit. Untuk membedakan, periksa _WIN64 setelah _WIN32. Untuk kebanyakan kode, _WIN32 sudah cukup—API Win32 sama untuk keduanya.

Membungkus Thread dan Mutex dengan Typedef #

Untuk menyembunyikan perbedaan API thread, kita buat lapisan abstraksi sederhana: typedef untuk tipe thread dan mutex, plus fungsi pembantu (thread_create, thread_join, mutex_lock, dll.). Kode pemakai tidak peduli platform—ia hanya memanggil fungsi pembantu.

c
/* File: thread_portabel.c - thread dan mutex lintas platform */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>

#ifdef _WIN32
#include <windows.h>
typedef HANDLE         thread_t;
typedef CRITICAL_SECTION mutex_t;

static int thread_create(thread_t *t, LPTHREAD_START_ROUTINE fn, void *arg) {
    *t = CreateThread(NULL, 0, fn, arg, 0, NULL);
    return (*t == NULL) ? -1 : 0;
}
static int thread_join(thread_t t) {
    WaitForSingleObject(t, INFINITE);
    CloseHandle(t);
    return 0;
}
static void mutex_init(mutex_t *m)    { InitializeCriticalSection(m); }
static void mutex_destroy(mutex_t *m)  { DeleteCriticalSection(m); }
static void mutex_lock(mutex_t *m)     { EnterCriticalSection(m); }
static void mutex_unlock(mutex_t *m)   { LeaveCriticalSection(m); }

#else
/* POSIX */
#include <pthread.h>
typedef pthread_t        thread_t;
typedef pthread_mutex_t  mutex_t;

static int thread_create(thread_t *t, void *(*fn)(void *), void *arg) {
    return pthread_create(t, NULL, fn, arg);
}
static int thread_join(thread_t t) {
    return pthread_join(t, NULL);
}
static void mutex_init(mutex_t *m)    { *m = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER; }
static void mutex_destroy(mutex_t *m) { pthread_mutex_destroy(m); }
static void mutex_lock(mutex_t *m)    { pthread_mutex_lock(m); }
static void mutex_unlock(mutex_t *m)  { pthread_mutex_unlock(m); }
#endif

static long counter = 0;
static mutex_t counter_mtx;

#ifdef _WIN32
static DWORD WINAPI naikkan(LPVOID arg) {
#else
static void *naikkan(void *arg) {
#endif
    (void) arg;
    for (int i = 0; i < 100000; i++) {
        mutex_lock(&counter_mtx);
        counter++;
        mutex_unlock(&counter_mtx);
    }
    return 0;
}

int main(void) {
    mutex_init(&counter_mtx);

    thread_t t1, t2;
    thread_create(&t1, naikkan, NULL);
    thread_create(&t2, naikkan, NULL);
    thread_join(t1);
    thread_join(t2);

    printf("Counter (harus 200000): %ld\n", counter);
    mutex_destroy(&counter_mtx);
    return 0;
}

Compile di Linux dengan -pthread:

bash
gcc -std=c11 -Wall -Wextra -pthread -o thread_portabel thread_portabel.c
./thread_portabel

Output yang Dihasilkan:

text
Counter (harus 200000): 200000

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (#ifdef _WIN32): Pemisah utama. Bila compiler mendeteksi target Windows, blok Windows diaktifkan. Bila tidak (Linux, macOS, BSD), blok POSIX diaktifkan.
  2. Baris 7-8 (typedef HANDLE thread_t; typedef CRITICAL_SECTION mutex_t;): Di Windows, HANDLE adalah tipe untuk thread, dan CRITICAL_SECTION adalah tipe untuk mutex. Kita alias keduanya ke nama yang konsisten: thread_t dan mutex_t.
  3. Baris 10-15 (thread_create, thread_join untuk Windows): Membungkus CreateThread dan WaitForSingleObject. Signature fungsi pembantu sengaja dibuat mirip POSIX: mengembalikan 0 saat sukses, -1 saat gagal.
  4. Baris 27-30 (typedef pthread_t thread_t;): Di POSIX, pthread_t dan pthread_mutex_t adalah tipe asli. Kita alias ke nama yang sama dengan blok Windows—kode di bawah #endif (yaitu naikkan dan main) dapat memakai nama generik.
  5. Baris 41-46 (naikkan): Fungsi thread perlu signature berbeda untuk Windows (DWORD WINAPI(LPVOID)) vs POSIX (void *(void *)). #ifdef di sini hanya memilih signature; isi fungsi sama. Return 0 di Windows dan NULL di POSIX, tetapi kode main membaca keduanya sama.
  6. Baris 60-65 (main): Setelah lapisan abstraksi, kode main tidak punya satu pun #ifdef—ia memanggil thread_create, mutex_lock, dll. yang implementasinya berbeda tapi signature sama.

Sleep Milidetik yang Portabel #

API sleep berbeda jauh antara POSIX dan Windows. POSIX punya nanosleep (resolusi nanodetik), Windows punya Sleep (resolusi milidetik). Kita bungkus keduanya dengan fungsi jeda_ms tunggal.

c
/* File: sleep_portabel.c - tidur milidetik lintas platform */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <time.h>

#ifdef _WIN32
#include <windows.h>
static void jeda_ms(int ms) {
    Sleep(ms);   /* Windows: milidetik */
}
#else
static void jeda_ms(int ms) {
    struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
    nanosleep(&ts, NULL);
}
#endif

int main(void) {
    printf("Tunggu 100 ms...\n");
    fflush(stdout);

    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);

    jeda_ms(100);

    clock_gettime(CLOCK_MONOTONIC, &t1);
    double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
    printf("Waktu sebenarnya: %.3f detik\n", elapsed);
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
Tunggu 100 ms...
Waktu sebenarnya: 0.100 detik

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-9 (#ifdef _WIN32 ... Sleep(ms)): Di Windows, Sleep menerima argumen dalam milidetik. Implementasi langsung—tidak ada konversi.
  2. Baris 11-13 (struct timespec ts = { ms / 1000, ... }): POSIX nanosleep menerima struct timespec dengan detik dan nanodetik terpisah. Kita konversi: bagian detik = ms / 1000, bagian nanodetik = sisa milidetik × 1 juta.
  3. Baris 18-26 (main): Kode yang memakai jeda_ms tidak punya #ifdef—ia bisa dikompilasi ulang untuk Windows atau Linux tanpa perubahan. Inilah inti portabilitas: kompleksitas platform disembunyikan di lapisan abstraksi.
  4. Pengukuran waktu dengan clock_gettime(CLOCK_MONOTONIC, ...): Hanya POSIX. Untuk kode yang juga perlu berjalan di Windows, bungkus juga fungsi pengukuran waktu—QueryPerformanceCounter di Windows, clock_gettime di POSIX. Atau gunakan C11 <time.h> dengan timespec_get.

Strategi Portabilitas Tingkat Lanjut #

Untuk proyek skala produksi, tiga pendekatan tambahan sering dipakai:

  • C11 threads (<threads.h>): Standar C11 mendefinisikan thrd_create, mtx_lock, cnd_wait, dan sejenisnya. Sebagian besar compiler modern mendukungnya, tetapi MSVC lama tidak. Cocok bila Anda dapat membatasi target compiler.
  • Library abstraksi pihak ketiga: Library seperti libuv, SDL2, atau Boost (C++) menyediakan API konsisten lintas platform. Trade-off: dependensi eksternal dan ukuran biner lebih besar.
  • CMake try_compile dan feature detection: Build system CMake dapat mendeteksi apakah suatu fungsi tersedia (check_function_exists) dan men-generate header konfigurasi. Pola ini dipakai oleh proyek open source besar seperti OpenSSL dan curl.