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:
python3 -m pip install pybind11
python3 -m pybind11 --includesPerintah build lengkapnya biasanya seperti ini (satu baris; python3-config --extension-suffix menghasilkan akhiran yang benar untuk versi interpreter Anda):
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.
// 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:
#include <pybind11/pybind11.h>— header inti;<pybind11/stl.h>ditambahkan agar tipe container standar (std::vector,std::map) ikut dikonversi otomatis.PYBIND11_MODULE(modulstat, m)— makro yang memperluas menjadi fungsi inisialisasi bernamaPyInit_modulstat, kontrak yang dicari interpreter Python saatimport.madalah object untuk mendaftarkan isi modul.m.doc()— docstring modul; inilah yang tercetak saat pengguna menulishelp(modulstat).m.def("tambah", ...)— mendaftarkan fungsi bernamatambah;py::arg("a")memberi nama parameter sehingga Python bisa memanggiltambah(a=3, b=4)dengan keyword argument.m.def("rata_rata", &rata_rata, ...)— pointer ke fungsi biasa; pybind11 membaca tanda tangannya lewat template dan menyusun konversilistPython keconst 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:
// 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:
panggil("tambah", {3, 4}) = 7.0
panggil("rata_rata", {2, 4, 6}) = 4.0
fungsi 'bagi' tidak terdaftarBedah mekanismenya:
- Baris 10-29 (
class Modul) — tabelstd::map<std::string, Fungsi>menyimpan fungsi berdasarkan nama;def()adalah padanan langsung darim.def()di pybind11. - Baris 33-42 (
modul.def(...)) — dua lambda didaftarkan; masing-masing menerimastd::vector<double>, sama seperti argumen list di sisi Python. - Baris 48 (
modul.panggil("bagi", {8.0, 2.0})) — pemanggilan nama yang tidak terdaftar jatuh ke pesan kesalahan; di pybind11 asli, kondisi yang sama memunculkanAttributeErrordi 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:
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):
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++ | Python | Catatan |
|---|---|---|
int, long long | int | overflow dicek, bukan terpotong diam-diam |
double, float | float | |
bool | bool | |
std::string | str | UTF-8 |
std::vector<T> | list | disalin; py::array_t untuk zero-copy dengan NumPy |
std::map<K, V> | dict | |
std::optional<T> | T atau None | |
fungsi lambda / std::function | callable |
Perbandingan dengan jalur yang sudah kita pelajari:
| Aspek | ctypes | pybind11 |
|---|---|---|
| Pekerjaan persiapan | tanpa build; cukup .so ber-C ABI | build extension module per interpreter |
| Konversi tipe | manual (argtypes, struct replika) | otomatis lewat template |
std::string/std::vector | tidak didukung; API harus ber-C | didukung penuh |
| Class dan inheritance C++ | tidak; hanya struct data | didukung (py::class_<T>) |
| Kesalahan argumen | undefined behavior | TypeError yang jelas |
| Cocok untuk | memakai library C yang sudah jadi | proyek 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 .