MyEnigma

とある自律移動システムエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

pybind11を使ってPythonからC++コードを実行する方法

目次

はじめに

Python Advent Calendar 2016の17日目の記事です。

 

何かのシステムを作る時に、

はじめはPythonでプロトタイプを作り、

pandasで入力データを流し込みつつ、

matplotlibで結果を確認しながら、

システムを作ることが多いです。

 

その後、そのシステムのより最適化するために、

C++に移植したりすることがあるのですが、

システムをいきなりPythonからC++に移植するのではなく、

システムの一部をC++に移植して、

残りのPythonシステムはそのままで実行したい時があります。

これにより、元のシステムからの移植の際に

バグが混入していないかが、確認しやすくなったり、

どの部分をC++に変えることで計算が早くなるのかが

わかりやすくなります。

 

そこで今回は

C++11のコードをPythonから利用できるようにする

pybind11というツールの概要と、

いくつかのサンプルを紹介したいと思います。

 

pybind11とは?

pybind11は、C++コードとPythonを連携させるための、

軽量ライブラリです。

github.com

 

似たツールとしては、Boost.Pythonがありますが、

Boost.Python - 1.62.0

Boost.Pythonは様々なコンパイラや、古いシステムに対応するために、

かなり複雑な構造になっているという問題があります。

 

pybind11は、よりシンプルな構成になっており、

依存関係も通常のPythonとC++の標準ライブラリのみで構成されており、

ヘッダーライブラリなので、事前のビルド等が不要で、

簡単に使うことができます。

  

pybind11のドキュメントはこちらになります。

pybind11 — Seamless operability between C++11 and Python — pybind11 2.3.dev0 documentation

 

シンプルなサンプルコードの実行方法

実際に、上記のドキュメントにある

サンプルコードの実行の仕方を説明します。

1. pybind11をDL

Githubのリポジトリから、

ソースコードをDLします。

github.com

ヘッダライブラリなので、

特にライブラリのビルドなどは不要です。

2. c++のコードを書く

まず初めに、Pythonから呼び出したいC++のコードを書きます。

今回は下記のような、シンプルな足し算の関数をhoge.hという

ヘッダライブラリに記述しました。

int add(int i,int j){
  return i+j;
}

3. pybind11のバインドc++コードを書く。

続いて、上記のコードをラッピングし、

pythonから実行可能にするために、

バインドC++コードを書きます。

#include <pybind11/pybind11.h>
#include "hoge.h"

namespace py = pybind11;

PYBIND11_PLUGIN(example){
  py::module m("example", "pybind11 example plugin");

  m.def("add", &add, "test1");

  return m.ptr();
}

pybind11のヘッダと、先程のコードをインクルードして、

pybind11の関数を使って、pythonモジュールを設定します。

上記のコードにおいて、exampleの部分はモジュール名になり、

moduleコンストラクタの引数は、モジュールのdoc stringになります。

def関数は、C++の関数をbindする関数です。

pythonから呼ぶ関数名と、関数のdocstringを設定しています。

最後の行は、モジュールのpythonオブジェクトを

pythonコードに渡す関数です。

4. C++コードをコンパイルする。

続いて、pythonからモジュールを参照できるように、

先程のC++コードをコンパイルします。

公式ドキュメント通りにコンパイルしてみたのですが、

自分の環境では、いくつか問題が出たのでメモしておきます。

Macの場合

ドキュメント通り、下記のコマンドでコンパイルできたのですが、

$ c++ -O3 -shared -std=c++11 -I ../../pybind11/include/ python-config --cflags --ldflags example.cpp -o example.so

Macのシステム用のPython以外でコンパイルすると、

import時にエラーが出てしまいました。

pyenvでpythonの仮想環境を作っているのですが、

2.7.xでも3.5.xでもエラーが出ていました。

解決法を探している最中です。

ubuntuの場合

下記のコマンドのように、

-fpicというフラグを追加し、

-Wstrict-prototyesという

ワーニングメッセージを無くすために、

sedを使って、python configのコマンドを修正しています。

$ c++ -O3 -shared -std=c++11 -fpic -I ../../pybind11/include/ python-config --cflags --ldflags | sed s/-Wstrict-prototypes// example.cpp -o example.so

上記のコマンドで問題なく、

python用のオブジェクトファイルができました。

5. pythonコードで読み込ませる

先程のコンパイルで、example.soというファイルが出来た場合、

(windowsでは example.pyd)

あとはpythonコードから、importすれば、

C++の関数を実行できます。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import example
print(example.add(5, 1))

標準出力に、6という値が表示されるはずです。

デフォルト引数

デフォルト引数を設定する場合は、

下記のように、c++のデフォルト引数を設定し、

int add(int i=1,int j=2){
  return i+j;
}

pyhonのバインディング側にも、

同じデフォルト引数を指定する必要があります。

m.def("add", &add, "test1", py::arg("i") = 1, py::arg("j") = 2);

あとは、python側で特に引数を指定しない場合は、

デフォルト引数が使われるようになります。

C++コードの変数をpython側で利用する

C++側のコードの中に、

下記のようなグローバル変数があり、

int NUMBER=5;
float NUMBER2=5.2;
std::string str="binding";

この変数の値を、モジュールの変数として

利用したい場合は、bindingコードの中で、

attr関数とcast関数を利用します。

PYBIND11_PLUGIN(example){
  py::module m("example", "pybind11 example plugin");
  m.attr("NUMBER") = py::cast(NUMBER);
  m.attr("NUMBER2") = py::cast(NUMBER2);
  m.attr("str") = py::cast(str);
 return m.ptr();
}

attr関数は、モジュールに変数を登録し、

cast関数はpythonのオブジェクトにデータをcastする関数です。

あとは、python側で下記のようにモジュールの変数にアクセスできます。

import example
print(example.NUMBER)
print(example.NUMBER2)
print(example.str)

 

C++クラスのバインディング

続いて、C++のクラスのバインディングをしてみたいと思います。

下記のような、クラスがPet.hというコードに書かれているとします。

#include<string>

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;
    }

    static std::string getClassName(){
      return "Pet";
    }

    std::string name;
};

上記のコードをbindingする

C++コードは下記のようになります。

テンプレート化されたclass_というAPIに対して、

def関数で関数を追加して行けばOKです。

py::initという関数はコンストラクタを設定する関数です。

static関数の場合は、def_staticという特別な関数になります。

 

/**
 *  @brief sample code of pybind11
 */
#include <pybind11/pybind11.h>
#include "Pet.h"

namespace py = pybind11;

PYBIND11_PLUGIN(cpplib){
  py::module m("cpplib", "pybind11 example plugin");


  py::class_<Pet>(m, "Pet")
        .def(py::init<const std::string &>())
        .def("setName", &Pet::setName)
        .def("getName", &Pet::getName)
        .def_static("getClassName", &Pet::getClassName);

  return m.ptr();
}

あとは、前述の方法でコンパイルすることで、

下記のように

pythonのクラスのように利用することができます。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import cpplib
pet = cpplib.Pet("pochi")
print(pet.getName())
pet.setName("tama")
print(pet.getName())
print(pet.getClassName())

 

継承

続いてC++の継承コードをバインディングする方法を説明します。

下記のようなPetクラスがあり、

#include <string>

class Pet {
public:
  Pet(const std::string &name) : name(name) {}
  std::string name;
};

このペットクラスを継承したDogクラスが下記のように

存在していたとします。

#include "Pet.h"
#include <string>

class Dog : public Pet {
public:
  Dog(const std::string &name) : Pet(name) {}

  std::string Bark() const { return "wan"; }
};

上記の継承コードをバインディングする場合は、

下記のようにbindingコードを書きます。

class_ APIの2つ目に親クラスを設定することで

継承クラスをバインディングできます。

/**
 *  @brief sample code of pybind11
 */

#include "Dog.h"
#include <pybind11/pybind11.h>

namespace py = pybind11;

PYBIND11_PLUGIN(cpplib) {
  py::module m("cpplib", "pybind11 example plugin");

  py::class_<Pet>(m, "Pet")
      .def(py::init<const std::string &>())
      .def_readwrite("name", &Pet::name);

  py::class_<Dog, Pet>(m, "Dog")
      .def(py::init<const std::string &>())
      .def("Bark", &Dog::Bark);

  return m.ptr();
}

上記のコードをライブラリ化すると、

下記のようにPythonからライブラリを利用することができます。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import cpplib
pet = cpplib.Pet("pochi")
print(pet.name)

pet2 = cpplib.Dog("shiro")
print(pet2.name)
print(pet2.Bark())

 

関数のオーバーロード

PythonにはC++で言う所の関数のオーバーロードは難しいですが、

pybind11を使えば、C++コードの関数オーバーロードを、

pythonで利用することが可能です。

 

下記のような、

関数オーバーロードがあるC++クラスがあったとして、

#include <string>

class Pet {
public:
  Pet(const std::string &name, int age) : name_(name), age_(age) {}

  void Set(const std::string &name) { name_ = name; }
  void Set(int age) { age_ = age; }

  std::string name_;
  int age_;
};

上記のコードを、

下記のコードのようにbindingすると、

同じSetという名前で関数をbindingすることができます。

#include "Pet.h"
#include <pybind11/pybind11.h>

namespace py = pybind11;

PYBIND11_PLUGIN(cpplib) {
  py::module m("cpplib", "pybind11 example plugin");

  py::class_<Pet>(m, "Pet")
      .def(py::init<const std::string &, int>())
      .def("Set", (void (Pet::*)(int)) & Pet::Set)
      .def("Set", (void (Pet::*)(const std::string &)) & Pet::Set)
      .def_readwrite("name", &Pet::name_)
      .def_readwrite("age", &Pet::age_);

  return m.ptr();
}

上記のオーバーロード関数は

Python側から下記のように利用することができます。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import cpplib
pet = cpplib.Pet("pochi", 10)
print(pet.name)
print(pet.age)
pet.Set("shiro")
pet.Set(20)
print(pet.name)
print(pet.age)

上記のように、関数オーバーロードが簡単にできるため、

関数オーバーロードを使いたい場合は、

普通にPythonをそのまま使うよりも便利かもしれません。

 

Enum

C++のEnumのような特別な型も

pybind11を使うことで、

pythonコードにbindすることができます。

 

下記のようなEnumを含むクラスがあった場合、

#include <string>

class Pet {
public:
  enum Kind { Dog = 0, Cat };

  Pet(const std::string &name, Kind type) : name_(name), type_(type) {}

  std::string name_;
  Kind type_;
};

下記のコードでbindingできます。

class_のオブジェクトを作成し、

関数の登録と共に、

enum_ APIを元にクラスオブジェクトに登録します。

/**
 *  @brief sample code of pybind11
 */
#include "Pet.h"
#include <pybind11/pybind11.h>

namespace py = pybind11;

PYBIND11_PLUGIN(cpplib) {
  py::module m("cpplib", "pybind11 example plugin");

  py::class_<Pet> pet(m, "Pet");

  pet.def(py::init<const std::string &, Pet::Kind>())
      .def_readwrite("name", &Pet::name_)
      .def_readwrite("type", &Pet::type_);

  py::enum_<Pet::Kind>(pet, "Kind")
      .value("Dog", Pet::Kind::Dog)
      .value("Cat", Pet::Kind::Cat)
      .export_values();

  return m.ptr();
}

すると、下記のようにPythonからC++のEnumを使うことができます。

intでキャストすると、C++のEnumの数値も取得できます。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import cpplib
pet = cpplib.Pet("pochi", cpplib.Pet.Dog)
print(pet.type)

pet2 = cpplib.Pet("pochi", cpplib.Pet.Cat)
print(pet2.type)
print(int(pet2.type))

 

STLのコンテナのやり取り

C++の関数に入力や出力にSTLのコンテナを使うことは多いですが、

pybind11を使うことで、C++のSTLのコンテナの変数を

pythonのリストやmapに変換し、

データのやり取りをすることができます。

 

例えば、下記のようにC++のvectorを入力にする関数があるとします。

ちなみにpybind11の注意点として、

bindingする関数は、コンテナデータをポインタや参照で与えられても、

そのデータを更新することが出来ないので、

返り値として、コンテナの変数を返す必要があります。

#include <vector>

std::vector<int> append_1(std::vector<int> &v) {
  v.push_back(1);
  return v;
}

上記のコードをbindingする関数は下記のようになります。

stlのデータをpythonとやり取りする場合は、

pybind11/stl.hをインクルードする必要があります。

このファイルをインクルードすることで、

stlのデータをpythonのlistやmapに自動変換してくれます。

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

namespace py = pybind11;

PYBIND11_PLUGIN(cpplib) {
  py::module m("cpplib", "pybind11 example plugin");

  m.def("append_1", &append_1);

  return m.ptr();
}

あとは、

下記のようにPythonのlistを渡すと、

その結果を処理し、結果のvectorを返す関数から、

再びpythonのlistを取得できます。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import cpplib
li = []
li.append(1)
li.append(2)
print(li)
li = cpplib.append_1(li)
print(li)

下記が、上記のコードの処理結果です。

f:id:meison_amsl:20161214074209p:plain

バインディングする関数から、

かならずデータのコピーを実施する必要がありますが、

これでSTLを使った関数を処理できます。

 

pybind11がサポートしているコンパイラ

現時点でpybind11は、下記のコンパイラに対応しています。

  • Clang/LLVM (any non-ancient version with C++11 support)

  • GCC (any non-ancient version with C++11 support)

  • Microsoft Visual Studio 2015 or newer

  • Intel C++ compiler 16 or newer (15 with a workaround)

  • Cygwin/GCC (tested on 2.5.1)

 

サンプルコード

上記のサンプルコードは

すべて下記のGithubリポジトリで公開されています。

github.com

 

より詳しいPythonとCコードの連携方法を学びたい人は

下記の書籍がおすすめです。

Cython ―Cとの融合によるPythonの高速化

Cython ―Cとの融合によるPythonの高速化

 

参考資料

pybind11でC++の関数をpythonから使う - Qiita

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

MyEnigma Supporters

もしこの記事が参考になり、

ブログをサポートしたいと思われた方は、

こちらからよろしくお願いします。

myenigma.hatenablog.com