Chapter sebelumnya menutup dengan titik lemah ctypes: hanya tipe C yang dipahami, sehingga std::vector, std::string, dan class C++ harus dipetakan manual satu per satu. pybind11 melengkapi cerita itu — header-only library yang membaca template C++ dan menghasilkan kode konversi tipe secara otomatis, sehingga fungsi C++ yang menerima std::vector<double> bisa dipanggil dari Python dengan list biasa tanpa satu baris pemetaan pun. pybind11 dipakai luas di ekosistem ilmiah Python: sejumlah bagian PyTorch, scikit-image, dan banyak library lain mengekspos kernel C++-nya lewat pustaka ini. Pada Chapter ini kita bongkar anatomi PYBIND11_MODULE, memverifikasi mekanisme intinya dengan program C++ yang benar-benar kita jalankan, lalu menimbang kapan memilih pybind11 dibanding ctypes.

Cara Kerja dan Cara Menginstal #

pybind11 bekerja sebagai “pembungkus” compile time: saat compiler memproses kode binding Anda, template pybind11 menghasilkan fungsi-fungsi yang menerima argumen Python (PyObject*), mengonversinya ke tipe C++ yang diminta, memanggil fungsi Anda, lalu mengonversi nilai balik kembali ke object Python. Hasil akhirnya adalah extension module — file .so di Linux (.pyd di Windows) yang bisa di-import seperti modul Python biasa. Karena header-only, instalasinya ringan: tidak ada library yang perlu di-link selain interpreter itu sendiri. Perintah di bawah memasangnya lewat pip; perintah kedua mencetak jalur header yang baru saja terpasang:

bash
python3 -m pip install pybind11
python3 -m pybind11 --includes

Perintah build lengkapnya biasanya seperti ini (satu baris; python3-config --extension-suffix menghasilkan akhiran yang benar untuk versi interpreter Anda):

bash Perintah build (ilustrasi)
c++ -O3 -Wall -Wextra -shared -std=c++14 -fPIC \
  $(python3 -m pybind11 --includes) modulstat.cpp \
  -o modulstat$(python3-config --extension-suffix)

Dalam proyek serius, perintah itu dibungkus CMake dengan pybind11_add_target agar lintas platform. Lingkungan course ini tidak menginstal pybind11, sehingga contoh binding-nya kita tampilkan sebagai kode penuh tanpa klaim output — salin ke mesin Anda yang sudah punya pip dan semuanya bisa dijalankan.

Anatomi PYBIND11_MODULE #

Berikut modul binding penuh: dua fungsi diekspos, satu berupa lambda, satu lagi pointer ke fungsi biasa.

cpp modulstat.cpp
// modulstat.cpp — modul pybind11: dua fungsi C++ diekspos ke Python.
// Nama modul di PYBIND11_MODULE harus sama dengan nama file hasil build.
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>  // konversi otomatis std::vector <-> list Python

namespace py = pybind11;

double rata_rata(const std::vector<double>& v) {
    if (v.empty()) return 0.0;
    double total = 0.0;
    for (double x : v) {
        total += x;
    }
    return total / static_cast<double>(v.size());
}

PYBIND11_MODULE(modulstat, m) {
    m.doc() = "Modul statistik kecil untuk course C++";
    m.def("tambah", [](int a, int b) { return a + b; },
          py::arg("a"), py::arg("b"),
          "Menjumlahkan dua bilangan bulat");
    m.def("rata_rata", &rata_rata,
          "Menghitung rata-rata sebuah list angka");
}

Bedah bagian pentingnya:

  1. #include <pybind11/pybind11.h> — header inti; <pybind11/stl.h> ditambahkan agar tipe container standar (std::vector, std::map) ikut dikonversi otomatis.
  2. PYBIND11_MODULE(modulstat, m) — makro yang memperluas menjadi fungsi inisialisasi bernama PyInit_modulstat, kontrak yang dicari interpreter Python saat import. m adalah object untuk mendaftarkan isi modul.
  3. m.doc() — docstring modul; inilah yang tercetak saat pengguna menulis help(modulstat).
  4. m.def("tambah", ...) — mendaftarkan fungsi bernama tambah; py::arg("a") memberi nama parameter sehingga Python bisa memanggil tambah(a=3, b=4) dengan keyword argument.
  5. m.def("rata_rata", &rata_rata, ...) — pointer ke fungsi biasa; pybind11 membaca tanda tangannya lewat template dan menyusun konversi list Python ke const std::vector<double>& secara otomatis.

Di balik semua itu terdapat satu struktur sederhana: tabel yang memetakan nama ke fungsi yang bisa dipanggil. Kita bisa membuktikan idenya dengan program C++ polos yang mensimulasikan mekanisme m.def — dan program ini kita jalankan sungguhan:

cpp
// modul-simulasi.cpp — inti modul binding dalam bentuk paling sederhana:
// tabel nama -> fungsi yang bisa dipanggil dari "dunia luar" modul.
#include <cstdio>
#include <functional>
#include <map>
#include <string>
#include <utility>
#include <vector>

using Fungsi = std::function<double(const std::vector<double>&)>;

class Modul {
public:
    void def(const std::string& nama, Fungsi f) {
        tabel_[nama] = std::move(f);
    }

    double panggil(const std::string& nama, const std::vector<double>& args) const {
        auto it = tabel_.find(nama);
        if (it == tabel_.end()) {
            std::printf("fungsi '%s' tidak terdaftar\n", nama.c_str());
            return 0.0;
        }
        return it->second(args);
    }

private:
    std::map<std::string, Fungsi> tabel_;
};

int main() {
    Modul modul;
    modul.def("tambah", [](const std::vector<double>& a) {
        return a[0] + a[1];
    });
    modul.def("rata_rata", [](const std::vector<double>& a) {
        double total = 0.0;
        for (double x : a) {
            total += x;
        }
        return a.empty() ? 0.0 : total / static_cast<double>(a.size());
    });

    std::printf("panggil(\"tambah\", {3, 4})       = %.1f\n",
                modul.panggil("tambah", {3.0, 4.0}));
    std::printf("panggil(\"rata_rata\", {2, 4, 6}) = %.1f\n",
                modul.panggil("rata_rata", {2.0, 4.0, 6.0}));
    modul.panggil("bagi", {8.0, 2.0});
    return 0;
}

Output yang Dihasilkan:

text
panggil("tambah", {3, 4})       = 7.0
panggil("rata_rata", {2, 4, 6}) = 4.0
fungsi 'bagi' tidak terdaftar

Bedah mekanismenya:

  1. Baris 10-29 (class Modul) — tabel std::map<std::string, Fungsi> menyimpan fungsi berdasarkan nama; def() adalah padanan langsung dari m.def() di pybind11.
  2. Baris 33-42 (modul.def(...)) — dua lambda didaftarkan; masing-masing menerima std::vector<double>, sama seperti argumen list di sisi Python.
  3. Baris 48 (modul.panggil("bagi", {8.0, 2.0})) — pemanggilan nama yang tidak terdaftar jatuh ke pesan kesalahan; di pybind11 asli, kondisi yang sama memunculkan AttributeError di sisi Python.

Jadi ketika Anda menulis m.def("tambah", ...), yang benar-benar terjadi adalah pendaftaran fungsi ke tabel modul — pybind11 hanya menambahkan lapisan konversi tipe dan penanganan exception di sekeliling tabel itu.

Skrip Ujicoba dan Konversi Tipe Otomatis #

Sisi Python-nya ternyata sangat biasa saja — import, panggil, selesai:

python coba-modul.py
import modulstat

print(modulstat.__doc__)
print("tambah(a=3, b=4)    =", modulstat.tambah(a=3, b=4))
print("rata_rata([2,4,6])  =", modulstat.rata_rata([2, 4, 6]))
try:
    modulstat.tambah(1, 2, 3)
except TypeError as e:
    print("TypeError:", e)

Output yang diharapkan (ilustrasi — tidak dapat diuji di lingkungan course karena pybind11 tidak terinstal):

text Contoh output
Modul statistik kecil untuk course C++
tambah(a=3, b=4)    = 7
rata_rata([2,4,6])  = 4.0
TypeError: tambah(): incompatible function arguments. ...

Baris terakhir adalah hadiah gratis dari pybind11: jumlah argumen yang salah bukan lagi undefined behavior seperti pada ctypes, melainkan TypeError yang jelas dan bisa ditangkap try/except.

Konversi yang dikerjakan otomatis (setelah <pybind11/stl.h> disertakan) mencakup pemetaan dua arah yang paling sering dibutuhkan:

C++PythonCatatan
int, long longintoverflow dicek, bukan terpotong diam-diam
double, floatfloat
boolbool
std::stringstrUTF-8
std::vector<T>listdisalin; py::array_t untuk zero-copy dengan NumPy
std::map<K, V>dict
std::optional<T>T atau None
fungsi lambda / std::functioncallable

Perbandingan dengan jalur yang sudah kita pelajari:

Aspekctypespybind11
Pekerjaan persiapantanpa build; cukup .so ber-C ABIbuild extension module per interpreter
Konversi tipemanual (argtypes, struct replika)otomatis lewat template
std::string/std::vectortidak didukung; API harus ber-Cdidukung penuh
Class dan inheritance C++tidak; hanya struct datadidukung (py::class_<T>)
Kesalahan argumenundefined behaviorTypeError yang jelas
Cocok untukmemakai library C yang sudah jadiproyek C++ yang ingin hidup di Python

GIL: Satu Kunci untuk Seluruh Interpreter #

Interpreter CPython menjaga satu Global Interpreter Lock (GIL) — mutex yang membuat hanya satu thread yang boleh menyentuh object Python pada satu waktu. Setiap fungsi binding dipanggil dari Python otomatis memegang GIL, sehingga kode C++ Anda bebas membaca object Python. Konsekuensinya muncul ketika fungsi C++ itu berjalan lama: selama perhitungan, GIL tetap ditahan dan seluruh thread Python lain terhenti — justru situasi yang ingin dihindari orang memindahkan kode ke C++. Solusinya, lepaskan GIL selama bagian berat: m.def("hitung", &hitung, py::call_guard<py::gil_scoped_release>()). Kebalikannya juga penting: thread C++ yang berjalan mandiri (mis. callback dari library jaringan) tidak punya GIL, dan harus mengambilnya dulu dengan py::gil_scoped_acquire sebelum boleh menyentuh object Python apa pun.

Dengan pybind11, bagian paling melelahkan dari binding — pemetaan tipe dan class — sudah diotomatiskan, dan tersisa keputusan desain murni: fungsi mana yang diekspos, dan bagian mana yang perlu melepas GIL. Setelah Python terhubung, kebutuhan berikutnya yang paling umum adalah penyimpanan data yang terstruktur — dan untuk itulah Chapter berikutnya membawa database sungguhan ke dalam program C++ Anda: SQLite.

Referensi: pybind11 documentation , Extending and Embedding the Python Interpreter .