C++プログラマへの道 #26 - Pybind11

Last Edited: 3/24/2025

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

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/pybind11git 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-devcmakeをインストールする必要があります。 macOSの場合は、元から含まれているPythonバージョンが通常そのまま動作するため、cmakeのみをインストールする必要があります。 以降のコードでは、ヘッダーファイルと名前空間をインクルードするために#include <pybind11/pybind11.h>namespace py = pybind11;を使用することを前提としています。

関数と変数

pybind11の動作を、関数と変数をエクスポートすることで実証できます。以下のコード例では、デフォルト引数を持つシンプルなadd関数と変数seedを公開しています。

example.cpp
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スクリプトでモジュールをインポートして使用できます。

example.py
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コマンドを提供しており、モジュールのビルドに以下のように利用できます。

CMakeLists.txt
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で配布する場合、pybind11setuptoolsbuildtwineを使用して配布できます。 そのような場合の典型的なファイル構造は以下のようになります。

example/
├── src/
│   └── example.cpp
├── LICENSE
├── pyproject.toml
├── README.md
└── setup.py

pyproject.tomlでは、ビルド依存関係とプロジェクトのメタデータを指定できます。 (モジュールのビルドにsetuptoolsのみを使用する場合は、TOMLファイル内で[tool.setuptools]を使用して、このファイルだけに基づいてビルドを実行できます。)

pyproject.toml
[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.pypybind11setuptoolsが提供するヘルパー関数を使用して拡張モジュールを作成できます。

setup.py
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.gzwhlフォルダを含む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上の大規模リポジトリを見てみることをお勧めします。

リソース