Nerds N' Computers

ソフトウェア技術や思ったことについて書きます.

ソフトウェア技術や思ったことについていろいろと書きます.

Pybind11をパッケージ化するまでの流れ

pybind11-logo.png

背景

Pythonfor文は計算時間が長いという問題があり、そこを高速化したかったので、C++アルゴリズムを書いて、pythonにインポートするということをしました。 その際、pybind11というパッケージを利用したのですが、まだあまり参考にできる記事が少なかったので、その際のメモを残したいと思います。

環境

OS: macOS Mojave 10.14.2 python: pyenv 3.6.5

Pybind11とは

公式github: https://github.com/pybind/pybind11 公式ドキュメント: https://pybind11.readthedocs.io/en/stable/

pybind11とは公式ドキュメントによると

pybind11 is a lightweight header-only library that exposes C++ types in Python and vice versa, mainly to create Python bindings of existing C++ code.

ということです。つまり、PythonC++のコードを、またC++Pythonのコードを繋げることができるライブラリです。

実はこういったことを行ってくれる他の1つのライブラリもあり、その1つにBoost.Pythonというのがあるらしいです。が、自分は触ったことがないので、こちらの参考サイトのみ載せておきます。

Pybind11とBoostの大きな違いはBoostはとても大きいライブラリだということです。単純なモジュールを呼ぶためだけにBoostを使うのは非効率的ということもあり、Pybind11は作成されました。

その辺の詳しいベンチマークなどはこちらに詳しく書いてあるので、興味あればみてみてください!

インストール方法

インストール方法は何通りかあるのですが、今回自分はpipを使ったのでそちらの方の紹介をします。

$ pip3 install pybind11

この他にもgithubからのインストール方法もあるのですが、実行方法が異なってくる(コンパイルする際のコマンドが違うため)ので、気をつけた方がいいです!

実装例(簡易版)

背景にも書いたのですが、自分はpythonfor文の計算量が多いのが問題となっていたので、配列を用いたモジュールを書きました。が、公式にも英語のサイトにも中々配列の使い方が綴られておらず(自分の裁量不足ということもあるのですが...)、試行錯誤しました。 その結果、動いたコードの簡略版で実装例を紹介します。

コード

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

void print_arrays(std::vector<int> lst) {
  // 配列の長さ
  int length = sizeof(lst)/sizeof(lst[0]);

  // 配列の各要素を出力する
  for (int i=0; i < length-1; i++) {
    printf("%d\n", lst[i]);
  }
}

// PYBIND11_MODULE()はPythonでimportする際の関数を作成する
PYBIND11_MODULE(example, m) {
  m.doc() = "Example pybind11"; // optional module docstring
  m.def("print_arrays", &print_arrays, "A function that prints array elements.");
}

実行例

あとは実行するだけです!が、ここで私は躓きました。

失敗したコマンド

上は色んなサイトを調べた結果上手くいったコマンドです。 そして、実際に多くの人が行うように、私も公式ドキュメントに従って、以下のコマンドを実行をしました。

c++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix`

その結果、バーっとエラーが吐かれ(長すぎるため省略します)、最後に

ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

と言われてしまいました。

色々調べた結果、こちらのイシューが参考になりました。

成功したコマンド

そこで、上記のサイトを参考に、以下のコマンドで成功しました。

clang++ -O3 -Wall -shared -std=c++11 -fPIC `python -m pybind11 --includes` -undefined dynamic_lookup example.cpp -o example`python3-config --extension-suffix`

その結果、同じディレクトリにexample.cpython-36m-darwin.soという名前のファイルができたと思います。

詳しくは調べきれていないのですが、多分pyenvの影響かと思うので、使っている人は気をつけてください!

実行結果

ここまで来ればあとは簡単です。 先ほどのコード内でPYBIND_MODULE()という関数を用いて、pythonでインポートする際の関数を生成しました。

あとは、REPLで以下のようにすればいけます。

$ python3
>>> import example
>>> example.print_arrays([1, 2, 3, 4, 5])
1
2
3
4
5
>>>

応用

ここまでやると、本格的にライブラリを作りたくなってきました。 というのも、pybind11の公式ドキュメントにビルドシステムというセクションがあり、自分が作ったライブラリを実際にpipでインストールできるようになるまでが書かれていたからです(Pypiに載せるところまではやらないです)。

ディレクトリ構成

ディレクトリ構成は人それぞれの好みがあると思いますが、今回は公式ドキュメントでも紹介されていたgitのプロジェクトを参考にしました。

pybind11_example/
 ├ src/
 │ ├ addition.cpp
 │ ├ subtraction.cpp
 │ └ main.cpp
 ├ tests/
 │ └ test.py
 ├ setup.py
 └ README.md

src/内のコード

src内には任意のファイルを作って大丈夫です!

配列の各要素を足し合わせる関数を格納したファイル

#include <pybind11/stl.h>

int sum_arrays(std::vector<int> lst) {
  // 配列の長さ
  int length = lst.size();
  int sum = 0;

  // 配列の各要素を出力する
  for (int i=0; i < length; i++) {
    sum += lst[i];
  }

  return sum;
}

配列の各要素の積を返す関数を格納したファイル

#include <pybind11/stl.h>

int product_arrays(std::vector<int> lst) {
  // 配列の長さ
  int length = lst.size();
  int pro = 1;

  // 配列の各要素を出力する
  for (int i=0; i < length; i++) {
    pro *= lst[i];
  }

  return pro;
}

上記のモジュールを束ねるファイルmain.cpp

#include <pybind11/pybind11.h>

#include "addition.cpp"
#include "product.cpp"

PYBIND11_MODULE(pybind11_example, m) {
  m.doc() = "Example pybind11"; // optional module docstring
  m.def("sum_arrays", &sum_arrays);
  m.def("product_arrays", &product_arrays);
}

ここで、今回はライブラリ名をpybind11_exampleと設定しました。これは後にpipでインストールする際に同じ名前を用いるので、気をつけてください。

setup.py

あとはsetup.pyを記述するだけです。(README.mdやtest/内は省略をします) これはpybind11の公式ドキュメントにビルドシステムをかなり参考にしました。

基本真似をして、ext_modules=[]setup()の中身を書き換えるだけです。

...
ext_modules = [
    Extension(
        'pybind11_example', // ライブラリ名
        ['src/main.cpp'], // 参照にするファイル
        include_dirs=[
            # Path to pybind11 headers
            get_pybind_include(),
            get_pybind_include(user=True)
        ],
        language='c++'
    ),
]
...
...
setup(
    name='pybind11_example', // ライブラリ名
    version=0.1,
    author='sff1019',
    description='A pybind11 example',
    long_description='',
    ext_modules=ext_modules,
    install_requires=['pybind11>=2.2'],
    cmdclass={'build_ext': BuildExt},
    zip_safe=False,
)

これで完成です!

実行

pybind11_example11の親ディレクトリに入ります。

$ pip install ./pybind11_example
Processing ./pybind11_example
...                                                                         
Successfully installed pybind11-example-0.1

となれば成功です。

あとは好きなところで

import pybind11_example

pybind11_example.sum_arrays([1,2,3])

のように実行ができます。 ちなみに、出力は以下のようになります。

6

最後に

最初のpyenvのところだけが突っかかりましたが、思った以上に簡単にC++のコードをpythonにインポートすることができるのだな、と感じました。

参考サイト