pybind11に入門してみた話

# はじめに
こちらの記事は、[eeic （東京大学工学部電気電子・電子情報工学科） Advent Calendar 2022](https://qiita.com/advent-calendar/2022/eeic) の 11 日目の記事として書かれたものです。

pybind11 という Python のライブラリを初めて使ってみたので、その紹介を簡単にしようと思います。

# pybind11 とは
Python に C++ の関数やクラスを組み込めるライブラリです。
Python3.6+ と C++ の標準ライブラリのみに依存しているため、余計な依存関係をないのがポイントです。
古いコンパイラを無視して C++11 で書かれたものに限定することによって、軽量なヘッダのみのライブラリとすることができたらしいです。
詳しくは [公式ドキュメント](https://pybind11.readthedocs.io/en/stable/index.html) をご覧ください。チュートリアルも充実しています。

# 自作ライブラリの作成
`pybind11_demo` というライブラリを作成してみました。
ソースコードは [こちらの GitHub レポジトリ](https://github.com/Yuki-Imajuku/pybind11-demo) にあります。

ヘッダファイルを `include` ディレクトリにまとめています。
`src` ディレクトリに C++ の実装をまとめています。
`integer` と `structure` という2つのサブモジュールがあり、それぞれのサブモジュールの中に関数やクラスが実装されている、という形になっています。
ディレクトリ構造について、 Python の自作ライブラリを書く時みたいに、 `__init__.py` にあたるものが、各（サブ）モジュール内の `(サブ)モジュール名.cpp` になっています。

`setup.py` や `pyproject.toml` はライブラリ作成時に必要なものです。（詳細は省略します。）

```text
pybind11_demo/
    ├── include/
    │       ├── integer.h
    │       ├── pybind11_demo.h
    │       └── structure.h
    ├── src/
    │       ├── integer/
    │       │       ├── add_i16.cpp
    │       │       ├── add_i32.cpp
    │       │       ├── add_i64.cpp
    │       │       └── integer.cpp
    │       ├── structure/
    │       │       ├── pet_private.cpp
    │       │       ├── pet_public.cpp
    │       │       ├── pet.cpp
    │       │       └── structure.cpp
    │       └── pybind11_demo.cpp
    ├── pyproject.toml
    └── setup.py
```

## ※VSCode 上での開発に役立ちそうな話
VSCode 上で C/C++ の拡張機能を使いながら c++ のコードを書いていると、

```c++
#include <pybind11/pybind11.h>
```

のところに波線が引かれてしまうと思います。これは、VSCode 上からだと `pybind11.h` がインクルードのパスの中に見つからないせいなので、VSCode の設定に書き加えてあげます。具体的には、`settings.json` に以下の内容を追加してあげます。

```jsonc
{
    ...
    // add this block
    "C_Cpp.default.includePath": [
        "hoge",
        "fuga",
    ],
    ...
}
```

`hoge` や `fuga` の部分にあたる、追加するべきインクルードパスは、以下のコマンドで調べることができます。

```sh
python -m pybind11 --includes
```


# 挙動の確認
## 整数どうしの足し算
以下のように、整数どうしの足し算であっても、使用する型によって異なる関数を定義することができます。今回は、16 / 32 / 64 bit の符号付き整数を試してみます。

```c++
// 16bit 符号付き整数どうしの足し算
short add_i16(short i, short j) {
    return i + j;
}

// 32bit 符号付き整数どうしの足し算
int add_i32(int i, int j) {
    return i + j;
}

// 64bit 符号付き整数どうしの足し算
long long add_i64(long long i, long long j) {
    return i + j;
}
```

これらの関数を Python から呼び出して使ってみましょう。

```python
>>> add_i16(1, 2)
3
>>> add_i32(1, 2)
3
>>> add_i64(1, 2)
3
```

どれも正しい結果が返ってきています。
Python といえば（？）多倍長整数なので、その挙動も見てみましょう。

```python
>>> add_i16(2**14, 2**14)
-32768
>>> add_i32(2**14, 2**14)
32768
>>> add_i64(2**14, 2**14)
32768
```

`short` 型は $[-2^{15}, 2^{15})$ の値しか取れないのですが、 $2^{14} + 2^{14} = 2^{15}$（$010...0_{(2)} + 010...0_{(2)} = 100...0_{(2)}$）となってしまい、オーバーフローしてしまいます。（詳細が分からない方は [2の補数](https://ja.wikipedia.org/wiki/2%E3%81%AE%E8%A3%9C%E6%95%B0) とかで調べてみてください。）
さらに大きな整数で計算を試してみましょう。

```python
>>> add_i16(2**30, 2**30)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add_i16(): incompatible function arguments. The following argument types are supported:
    1. (arg0: int, arg1: int) -> int
Invoked with: 1073741824, 1073741824
>>> add_i32(2**30, 2**30)
-2147483648
>>> add_i64(2**30, 2**30)
2147483648
```

`short` 型ではエラーが出るようになりました。エラーの内容では、「非互換な関数の引数（$1073741824 = 2^{30}, 1073741824 = 2^{30}$）がある」と言われています。`short` 型の取りうる値以外の入力があると、エラーが出るみたいですね。
`int` 型では、先ほどの `short` 型と同じような原因で、間違った計算結果になってしまっています。

## 自作クラス
基本となる `Pet` クラスに基づいて、以下のような少しずつ異なるクラスを定義します。（詳しくは [ソースコード](https://github.com/Yuki-Imajuku/pybind11-demo/tree/main/pybind11_demo/src/structure) をご覧ください。）
- 通常のクラス（`Pet`）
- Python 接続時に `name` メンバ変数の読み書き権限を与えたクラス（`PetPublic`）
- c++ のクラス内で `naem` を private なメンバ変数としたクラス（`PetPrivate`）

```c++
class Pet {
    public:
        Pet(const std::string &name) : name(name) { }
        void setName(const std::string &name_) { name = name_; }
        const std::string &getName() const { return name; }
        std::string name;
};
```

各クラスに `__repr__` を実装してあるので、それぞれ見ていきましょう。

```python
>>> pet = Pet("hoge")
>>> pet_public = PetPublic("hoge")
>>> pet_private = PetPrivate("hoge")
>>> print(pet)
<pybind11_demo.structure.Pet named 'hoge'>
>>> print(pet_public)
<pybind11_demo.structure.PetPublic named 'hoge'>
>>> print(pet_private)
<pybind11_demo.structure.PetPrivate>
```

`__repr__` を実装する際に、`name` メンバ変数にアクセスすることでペットの名前も表示することを実現したかったのですが、`PetPrivate` クラスでは `name` メンバ変数が private なので、直接アクセスする形では使用することができません。
次に、Python 側から `name` メンバ変数に他の名前を直接代入できるか見てみましょう。

```python
>>> pet.name = "fuga"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'pybind11_demo.structure.Pet' object has no attribute 'name'
>>> pet_public.name = "fuga"
>>> pet_public.name
'fuga'
>>> pet_private.name = "fuga"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
```

Python 上での `Pet` クラスには `name` メンバ変数はないよと怒られてしまっています。以下のように、メンバ変数もちゃんと定義してあげないと、public なメンバ変数であっても、アクセスすることはできません。

```c++
.def_readwrite("name", &PetPublic::name)
```

また、`PetPrivate` クラスにおいて `name` は、`getName` メンバ関数を利用して、以下のように read-only な property として定義されています。そのため、`can't set attribute` というエラーが出されています。（メンバ変数とプロパティを並列に紹介してしまっていいのかは微妙ですが、機能の紹介程度に思っていただければです。）

```c++
.def_property_readonly("name", &PetPrivate::getName)
```

# おわりに
pybind11 ライブラリに入門してみた話でした。C++ で実装した関数や構造体を Python で手軽に使いたいと思った時、こちらのライブラリを思い出してみてください。

# 余談
[chatGPT](https://openai.com/blog/chatgpt/) 呼ばれる大規模言語モデルが公開されました。これに「pybind11に入門してみた話」というクエリを投げてみたところ、以下の出力が得られました。

> pybind11は、C++とPythonを比較的簡単に統合するためのライブラリです。これにより、PythonプログラマーがC++コードを使用して高速なプログラミングを行えるようになります。また、C++プログラマーがPythonの拡張モジュールを作成するのにも便利です。
>
> pybind11を使用するためには、まずpybind11をインストールする必要があります。これは通常、pipを使用して行うことができます。
> 
> ```sh
> pip install pybind11
> ```
> 
> インストールが完了したら、次に、C++のコードをPythonの拡張モジュールに変換するために必要なpybind11のヘッダーファイルをインクルードします。
> 
> ```c++
> #include <pybind11/pybind11.h>
> ```
> 
> 次に、Pythonの拡張モジュールを作成するためのpybind11の関数を呼び出します。この関数には、拡張モジュールの名前とC++の関数を引数として渡します。
> 
> ```c++
> PYBIND11_MODULE(my_extension, m) {
>     m.def("my_function", &my_function);
> }
> ```
> 
> これで、Pythonプログラマーは拡張モジュールをインポートし、C++の関数を呼び出すことができます。
> 
> ```python
> import my_extension
> 
> my_extension.my_function()
> ```
> 
> pybind11を使用することで、C++とPythonを統合することができます。これにより、Pythonプログラマーが高速なC++コードを使用したプログラミングを行うことができます。また、C++プログラマーはPythonの拡張モジュールを簡単に作成することができます。

やばいですね☆
簡単な記事程度なら、このAI使えば書けてしまいそうです......

※参考スクショ
![sukusho1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2962238/fb6daeb4-9312-7c6d-210c-725086b841eb.png)
![sukusho2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2962238/ef939574-4763-a14d-0b18-c707f0cb7848.png)


# 参考記事
1. [pybind11 documentation](https://pybind11.readthedocs.io/en/stable/index.html)
2. [Pybind11をパッケージ化するまでの流れ](https://qiita.com/sff1019/items/bcc0289f6d2912d5ba8a)
3. [【pybind11】サブモジュールごとにファイルを分割してPythonモジュールを作成する](https://tma15.github.io/blog/2021/10/09/pybind11%E3%82%B5%E3%83%96%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%94%E3%81%A8%E3%81%AB%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E5%88%86%E5%89%B2%E3%81%97%E3%81%A6python%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B/)