このブログ記事では、C++におけるpybind11モジュールについて紹介します。

モジュール作成する際ほとんどの場合、使いやすさ、アクセシビリティ、そして高いパフォーマンスを目指します。 しかし、一般にプログラミング言語では使いやすさとパフォーマンスの間にトレードオフがあります。 Pythonのようなスクリプト言語は、ガベージコレクタによる自動メモリ管理、動的型付け、高度な抽象化ツールにより、一般的に使いやすいです。 しかし、これらの追加機能によって手動メモリ管理と少ない抽象化を持つC++のようなコンパイル言語と比較して、実行速度が遅くなることがよくあります。
ほとんどの場合、パフォーマンスは最優先事項ではなく、パフォーマンスの差も小さい傾向があるため、Pythonが一般的により好ましい選択となります。 一方、機械学習(ML)のようなパフォーマンスが重要なシナリオでは、C++のようなコンパイル言語に頼ることが多いです。 しかし、両方の良いとこ取りをする方法があります。それは、バインディングを作成することで、 コンパイル言語で書かれたモジュールをスクリプト言語に公開するインターフェースとして機能させる方法です。 バインディングによって、初心者でもスクリプト言語内にいながらコンパイル言語の高性能モジュールを利用できるようになります。
実は、NumPy、PyTorch、TensorFlowなど、MLのシリーズで使用しているモジュールの多くは、コードの大部分がC++で書かれ、Pythonに公開されています。 (これによりPythonは高性能となり、一般のプログラマーの間での人気に貢献し、TIOBEインデックスで1位を獲得するほどになっています。) C++コードのPythonバインディングを設定する方法は複数ありますが(Cython、SWIG、Boost.Python)、 この記事では、C++11コードをPythonに公開するためのシンプルなソリューションを提供する軽量なヘッダーオンリーライブラリであるPybind11の基本を紹介します。
Pybind11のインストール
ライブラリをインストールするには、Gitとgithubを使用し、外部依存関係にextern
を使用していると仮定して、
git submodule add -b stable ../../pybind/pybind11 extern/pybind11
とgit submodule update --init
を使用できます。
あるいは、GitHubと相対URLの代わりに、完全なHTTPSまたはSSH URLを使用してライブラリをインストールすることもできます。
ライブラリをビルドする際には、extern/pybind11/include
からヘッダーファイルをインクルードするか、後述する公式の統合ツールを使用できます。
また、PyPI、conda-forge、vcpkg、brewを使用してライブラリを含めることもできます。開発環境が正しく設定されているかを確認するには、build
ディレクトリを作成し、
その中に移動し(cd build
)、cmake ..
を実行し、その後make check -j 4
を実行してテストを行います。
Linuxの場合は、python-dev
またはpython3-dev
とcmake
をインストールする必要があります。
macOSの場合は、元から含まれているPythonバージョンが通常そのまま動作するため、cmake
のみをインストールする必要があります。
以降のコードでは、ヘッダーファイルと名前空間をインクルードするために#include <pybind11/pybind11.h>
とnamespace py = pybind11;
を使用することを前提としています。
関数と変数
pybind11の動作を、関数と変数をエクスポートすることで実証できます。以下のコード例では、デフォルト引数を持つシンプルなadd
関数と変数seed
を公開しています。
int add(int i,int j = 2) {
return i + j;
};
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("add", &add, "A function that adds two numbers",
py::arg("i"), py::arg("j") = 2);
m.attr("seed") = 42;
}
このコードは、clang++ -O3 -Wall -shared -std=c++11 -fPIC `python3-config --includes` -undefined dynamic_lookup example.cpp -o example`python3-config --extension-suffix` -I 'extern/pybind11/include'
コマンドで手動でコンパイルできます。
その後、同じディレクトリにあるPythonスクリプトでモジュールをインポートして使用できます。
import example
print(f"Seed: ${example.seed}") # => Seed: 42
print(f"Add: ${example.add(i=3)}") # => Add: 5
print(f"Add: ${example.add(i=2, j=4)}") # => Add: 6
STL、chrono
(時間)、Eigen(NumPy配列とシームレスに連携)など、多くのデータ型がそのままサポートされています。
サポートされている型のリストはこちらで確認できます。
カスタム型
pybind11を使用すると、C++でカスタム定義されたクラス、データ構造、列挙型をPythonクラスに変換できます。
具体的には、class_
とenum_
を使用して以下のようにバインディングを作成できます。
struct Pokemon {
std::string name; // attribute
Pokemon(const std::string &name) : name(name) {}; // constructor
void setName(const std::string &name_) { name = name_; }; // setter
const std::string &getName() const { return name; }; // getter (const method)
}; // struct can have methods in C++
enum PokemonType { Fire = 0, Water, Grass };
PYBIND11_MODULE(example, m) {
py::class_<Pokemon>(m, "Pokemon")
.def(py::init<const std::string &>())
.def("setName", &Pokemon::setName)
// (任意) print関数を使用した際に、意味のある出力を提供するユーティリティ
.def("getName", &Pokemon::getName).def("__repr__",
[](const Pokemon &a) {
return "<example.Pokemon named '" + a.name + "'>";
}
);
py::enum_<PokemonType>(m, "PokemonType", py::arithmetic())
.value("Fire", &PokemonType::Fire)
.value("Water", &PokemonType::Water)
.value("Grass", &PokemonType::Grass);
}
上記の例では、class_
とビルダーパターンを使用してデータ構造Pokemon
を変換しています。
コンストラクタはコンストラクタのパラメータの型をテンプレート引数として受け取るinit
によって暗黙的に変換されます。
上のデータ構造は以下のようにPythonでメソッドにアクセスできます。
import example
p = example.Pokemon("Pikachu")
print(p.getName()) # => Pikachu
p.setName("Raichu")
print(p) # => <example.Pokemon named 'Raichu'>
また、def_readwrite("name", &Pokemon::name)
または定数フィールドの場合はdef_readonly("name", &Pokemon::name)
を使用してname
属性を公開することもできます。
プライベートなname
属性とゲッター・セッター関数を持つPokemon
クラスの場合、def_property("name", &Pokemon::getName, &Pokemon::setName)
を使用できます。
静的属性用のメソッドも利用可能です。
継承
オブジェクト継承がある場合、Pythonクラスに継承を反映させるためにpybind11にこれを指定する必要があります。これは以下のように実現できます。
class Pokemon {
public:
std::string nickname;
Pokemon(const std::string &nickname) : nickname(nickname) { };
};
class Pikachu : public Pokemon {
Pikachu(const std::string &nickname) : Pokemon(nickname) { };
std::string speak() const { return "Pikachu!" };
};
PYBIND11_MODULE(example, m) {
py::class_<Pokemon>(m, "Pokemon")
.def(py::init<const std::string &>())
.def_readwrite("nickname", &Pokemon::nickname);
py::class_<Pikachu, Pokemon>(m, "Pikachu") // parent class Pokemon specified
.def(py::init<const std::string &>())
.def("speak", &Pikachu::speak);
// 以下でも可能:
// py::class_<Pokemon> pokemon(m, "Pokemon");
// pokemon.def(py::init<const std::string &>())
// .def_readwrite("nickname", &Pokemon::nickname);
// py::class_<Pikachu>(pokemon, "Pikachu")
// .def(py::init<const std::string &>())
// .def("speak", &Pikachu::speak);
}
ただし、親クラスが動的バインディングのための抽象クラスである場合、このアプローチではクラスが適切に公開されない場合があります。 抽象クラスを扱うには、親クラスと子クラスの両方に対してヘルパートランポリンクラスを使用する必要があります。 詳細はこちらで確認できます。
CMakeを使用したモジュールの作成
長いc++
コマンドでモジュールをコンパイルしてビルドする代わりに、CMakeのようなビルドツールを活用できます。
Pybind11はCMake設定とpybind11_add_module
コマンドを提供しており、モジュールのビルドに以下のように利用できます。
cmake_minimum_required(VERSION 3.5...3.29)
project(example LANGUAGES CXX)
add_subdirectory(extern/pybind11)
{/* or find_package(pybind11 CONFIG REQUIRED) */}
pybind11_add_module(example example.cpp)
install(TARGETS example DESTINATION ${CMAKE_SOURCE_DIR})
CMakeコマンドを実行して.so
拡張子を持つモジュールをビルドできます。
デバイスに既にインストールされているパッケージを使用する場合は、add_subdirectory
の代わりにfind_package
を使用できます。
PyPIでモジュールを配布
Pythonパッケージをオンラインで、PyPIで配布する場合、pybind11
、setuptools
、build
、twine
を使用して配布できます。
そのような場合の典型的なファイル構造は以下のようになります。
example/
├── src/
│ └── example.cpp
├── LICENSE
├── pyproject.toml
├── README.md
└── setup.py
pyproject.toml
では、ビルド依存関係とプロジェクトのメタデータを指定できます。
(モジュールのビルドにsetuptools
のみを使用する場合は、TOMLファイル内で[tool.setuptools]
を使用して、このファイルだけに基づいてビルドを実行できます。)
[build-system]
requires = [
"setuptools>=42",
"pybind11>=2.10.0",
]
build-backend = "setuptools.build_meta"
[project]
name="example"
version = "0.0.1"
authors=[
{name = "Testing", email = "testing@example.com"},
]
description = "A test project using pybind11"
readme = "README.md"
license-files = ["LICEN[CS]E*"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python"
]
次に、setup.py
でpybind11
とsetuptools
が提供するヘルパー関数を使用して拡張モジュールを作成できます。
from setuptools import setup
from pybind11.setup_helpers import Pybind11Extension, build_ext
ext_modules = [
Pybind11Extension(
'example_un',
['src/example.cpp'],
language='c++'
),
]
setup(
ext_modules=ext_modules,
cmdclass={'build_ext': build_ext},
zip_safe=False,
)
ルートディレクトリでpython3 -m build
を実行すると、tar.gz
とwhl
フォルダを含むdist
ディレクトリが作成されることが確認できます。
まずTest PyPIにこれをアップロードして、PyPIでどのように表示されるかをテストできます。その為には、twine upload --repository testpypi dist/*
を使用し、
ここでアカウントを設定した後に作成できるAPIトークンを入力します。
python3 -m pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple example==0.0.1
を使用し、
Test PyPIからインストールして、モジュールのメソッドを実行することで、モジュールが機能することを確認できます(example
という名前ではTest PyPIでは機能しない可能性があるため、別の名前を使用することをお勧めします)。
より複雑なサブモジュールを持つモジュールを作成したり、異なるモジュール用のビルドターゲットを設定することもできます。
詳細については、リソースセクションに引用されているPyPA、setuptools、Pybind11の公式ドキュメントを確認することをお勧めします。
結論
この記事では、pybind11
の基本、Pythonバインディングの作成方法、そして様々なツールを使ってモジュールをビルドし配布する方法について説明しました。
これらのツールを使うことは一見簡単に見えますが、より大きく複雑なリポジトリを扱う場合、特に高度なC++機能や複雑なファイル構造を利用する場合、複雑さは指数関数的に増加します。
これらを含むより大きなプロジェクトをきれいに管理する方法を学ぶには、PyTorchなどの様々なGitHub上の大規模リポジトリを見てみることをお勧めします。
リソース
- Jansen, P. 2025. TIOBE Index for March 2025. TOBIE.
- Pybind11. n.d. pybind11 — Seamless operability between C++11 and Python. Pybind11 Documentation.
- PyPA. n.d. Python Packaging User Guide. PyPA.
- SETUPTOOLS. n.d. Documentation. SETUPTOOLS.
- The Cherno. 2018. CONST in C++. YouTube.