pythonからctypesを使ってc++の関数を呼ぶ(Hello World編)

目次

はじめに

そもそも何故pythonを使っているのに,c++やあるいはfortranを使いたくなるのか.それはみなさんご存知の通り,pythonではfor文などの繰り返し文の処理が非常に遅いからです.numpyなどのライブラリを使うと,かなり早くなりますがnumpy配列をfor文にいれるとこれまた激遅になってしまうという問題があります.numpy配列のブロードキャストルールを用いてfor文を回避できれば最高ですが,それができない状況がたびたび見られます.

このように,pythonではいかにしてコードを速くするかが求められがちです.

pythonにはPyPyNumbaCythonがあり,並列計算を行うmultiprocessingモジュールなど,様々な高速化手法があります.

しかし,別の言語(CやC++)の関数を呼ぶctypesを使った高速化に関する情報はすごく乏しいように感じましたので,今回この記事を書こうと思い立ちました.

実は私は,f2pyを使ってfortranで作った関数をすでに実装済みですが,こちらはまた別の機会に記事を書きたいと思います.

私の現在のバックグラウンド

私のプログラミング歴は次の通りです.

今回のプログラムの実行環境は次の通りです.

今回のゴール

今回のゴールは "c++の簡単な自作関数をctypesを使ってpythonから呼び出す"です. この記事では実際に私がつまづいてteratailにて質問・自己解決したものをまとめました.(有意義なコメントもいただいています.) teratail.com

それではよろしくお願いします.

コードの実装

まず,c++で"Hello"を標準出力するために,以下のプログラムhello.cppを作成しました.

#include <iostream>
using namespace ::std;

void hello(){
cout << "Hello" << endl;
}
int main(){
hello();
}

これをmacの動的ライブラリ.dylibとして以下の通りコンパイルしました.

$ clang++ -dynamiclib -o hello.dylib hello.cpp -std=gnu++14
$ ls
hello.cpp hello.dylib

コンパイルは問題なくできました.

このc++で作成したライブラリのmain関数を呼び出すpythonコードとして,ctypes_test.py作成しました.(参考: ctypes --- Pythonのための外部関数ライブラリ — Python 3.8.1 ドキュメント )

import ctypes
libc = ctypes.cdll.LoadLibrary("hello.dylib")
libc.main()

これを実行した結果は次の通りです.

$ python ctypes_test.py
$ Hello

バッチリmain関数を呼び出せました. では,hello関数を呼び出すためにctypes_test.pyを編集します.

import ctypes
libc = ctypes.cdll.LoadLibrary("hello.dylib")
libc.hello()  #libc.main()からの変更点

問題点

これを実行すると以下のエラーが出ちゃいました.

$ python ctypes_test.py
$ Traceback (most recent call last):
File "ctypes_test.py", line 6, in <module>
libc.hello()
File "**私のホームディレクトリ**/anaconda3/lib/python3.7/ctypes/__init__.py", line 369, in __getattr__
func = self.__getitem__(name)
File "**私のホームディレクトリ**/anaconda3/lib/python3.7/ctypes/__init__.py", line 374, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
AttributeError: dlsym(0x7fc181500f10, hello): symbol not found

このエラー(AttributeError: dlsym(0x7fc181500f10, hello): symbol not found)は,hello関数が認識されていないという意味です. ちなみにhello関数のみを含んだものをコンパイルしても同じようなエラーが出てしまいました.

(余談)なんでmain関数を呼ぶだけじゃダメなんですか

これは私個人の事情なのですが,最終的には関数に配列を渡して計算させたいからです. 冒頭でも述べましたが,c++等の関数を呼ぶ一番の理由は計算速度です.計算速度に時間がかかる計算はほとんどが配列の計算なので,引数を持てないmain関数を読んでもあまり意味はないのです.

解決策

このエラーの理由はC++の関数をCで呼び出すときに必要な,extern "C"がないことによるということがわかりました. どうやらctypesでpythonからC+の関数を呼び出すときも同様のことが起こるようで,ただしmain関数は自動的にextern "C"があると思ってくれるようです. 実際に直したコード(hello.cpp`)はこちらです.

#include <iostream>
using namespace ::std;

extern "C" int hello(){                //extern "C" を関数の前に追加
    cout << "Hello"<<endl;
    return 0;
}

int main(){
    hello();
}

これをコンパイルして,先ほどのhello関数を呼び出すctypes_test.pyを実行すると

$ python ctypes_test.py
$ Hello

となり,無事にコンパイルできました.無事に関数を呼び出せて何よりです.

さいごに

今回は無事に問題解決できてよかったです.しかし,ctypes公式のチュートリアルにはextern "C"に関する記述がないので,CやC++をあまり知らない(C++歴2週間の私のような)人にはこの解にたどり着くのはなかなか難しいなと感じました.(意地悪だな〜と思いました.)

この記事が,同じ境遇の方の助けとなれば幸いです.

次回は簡単な配列をctypesで扱いたいと思います. また,最終的に実装したい関数についての記事も別途書いていきたいと思います.

ではまた(✿╹◡╹)