Python から C++ のメソッドを呼び出すためには、いろいろな手段があります。
ctypes, CFFI, SWIG, boost.python, WASM とありますが、今回は python の標準ライブラリ ctypes を利用する例を紹介します。
動作環境
- Windows 11
- WSL – Ubuntu 22.04
- g++ 11.3.0
- Python 3.10.6
事前準備
C++ ライブラリ生成のため、g++ をインストールしておきます。
$ sudo apt install g++
Hello World!サンプル
動的リンクライブラリの作成
まず、C++ のメソッドを他言語から呼び出すために .so ファイルを作成します。
#include <iostream>
extern "C" void hello()
{
std::cout << "Hello World!" << std::endl;
}
$ g++ -fPIC --shared hello.cc -o libhello.so
libhello.so
ファイルが生成されます。
$ ls libhello.so
libhello.so
ctypes で呼び出し
次に python から libhello.so
を呼び出すための実装です。main.py
と libhello.so
を同じフォルダに配置する必要があります。
import ctypes
lib = ctypes.cdll.LoadLibrary("./libhello.so")
lib.hello()
$ python3 ./main.py
Hello World!
ポイント解説 – extern “C”
extern “C” の必要性
C++ のメソッドを他言語から呼び出す際には、extern "C"
を付与する必要があります。
C++ ではオーバーロードの機構(同じメソッド名の引数違いを宣言できる)があるため、コンパイラによってメソッド名が改変されてしまいます。
メソッド名が改変されると python から呼び出すためのメソッド名がわからなくなってしまいます。
これを抑制するのが extern "C"
です。
例えば、以下のように extern "C"
を記述しなくてもライブラリの生成そのものは正常終了します。
#include <iostream>
void hello()
{
std::cout << "Hello World!" << std::endl;
}
しかし、python コードを実行すると、「hello メソッドなんてないよー」とエラーが発生してしまいます。
$ python3 main.py
Traceback (most recent call last):
File "/home/shrh/sandbox/ctypes-sample/main.py", line 4, in <module>
lib.hello()
File "/usr/lib/python3.10/ctypes/__init__.py", line 387, in __getattr__
func = self.__getitem__(name)
File "/usr/lib/python3.10/ctypes/__init__.py", line 392, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
AttributeError: ./libhello.so: undefined symbol: hello
extern “C” がない場合のメソッド名
少し踏み込んで、nm
コマンドで解析すると、libhello.so
の中に _Z5hellov
というシンボルが生成されているのが確認できます。
これが g++ コンパイラによって勝手に改変されたメソッド名と推測されます。
$ nm libhello.so | grep hello
0000000000001205 t _GLOBAL__sub_I_hello.cc
0000000000001179 T _Z5hellov
試しに python からのメソッド呼び出しを _Z5hellov
に書き換えてみます。
import ctypes
lib = ctypes.cdll.LoadLibrary("./libhello.so")
lib._Z5hellov()
$ python3 main.py
Hello World!
今回は無事メソッド呼び出しが成功しました。
ポイント解説 – ライブラリパスの指定
パス探索がうまくいかない例
サンプルでは雑に ./libhello.so
と指定しましたが、ライブラリパスの指定は繊細です。
例えば main.py
の呼び出しを別ディレクトリから実行するだけで、エラーが発生してしまいます。
(今回の作業ディレクトリには hello-ctypes
と命名しています)
$ cd ..
$ pwd
/path_to_project
$ python3 hello-ctypes/main.py
Traceback (most recent call last):
File "/home/shrh/sandbox/hello-ctypes/main.py", line 3, in <module>
lib = ctypes.cdll.LoadLibrary("./libhello.so")
File "/usr/lib/python3.10/ctypes/__init__.py", line 452, in LoadLibrary
return self._dlltype(name)
File "/usr/lib/python3.10/ctypes/__init__.py", line 374, in __init__
self._handle = _dlopen(self._name, mode)
OSError: ./libhello.so: cannot open shared object file: No such file or directory
解決策1:LD_LIBRARY_PATH 環境変数を利用する
1つの手段として、LD_LIBRARY_PATH 環境変数を利用します。
まず、ライブラリのパスから、./
を除去します。
これだけだと、どこのディレクトリから実行してもエラーが発生することになってしまいます。
import ctypes
lib = ctypes.cdll.LoadLibrary("libhello.so")
lib.hello()
その代わり、LD_LIBRARY_PATH
環境変数を変更して、当パスの配下に libhello.so
が存在するようにするとどこからでも実行できるようになります。
$ cd ..
$ pwd
/path_to_project
$ LD_LIBRARY_PATH=hello-ctypes python3 hello-ctypes/main.py
Hello World!
上記の例は相対パスを指定していますが、もちろん絶対パスでも問題ないです。
むしろ実用上は絶対パスを指定するのが適切です。
解決策2:プログラム中で絶対パスを指定する
プログラム中で絶対パスを生成するようにすることで、常にパスを有効にすることができます。
下記スクリプトでは、main.py
と libhello.so
が同ディレクトリに存在することを前提に、__file__
を起点に libhello.so
の絶対パスを生成しています。
import ctypes
import pathlib
dirname = pathlib.Path(__file__).parent
libpath = dirname.joinpath("libhello.so")
lib = ctypes.cdll.LoadLibrary(str(libpath))
lib.hello()
__file__
が main.py
のパスを表します。parent
がその親ディレクトリを返却します。
最後に joinpath
で libhello.so
の絶対パスを生成しています。
変数 | パスの例 |
---|---|
__file__ | /path_to_project/hello-ctypes/main.py |
dirname | /path_to_project/hello-ctypes |
libpath | /path_to_project/hello-ctypes/libhello.so |
こうすることで、LD_LIBRARY_PATH
を気にせずとも、どこからでもスクリプトを実行することができるようになります。
$ pwd
/path_to_project/hello-ctypes
$ python3 main.py
Hello World!
$ cd ..
$ pwd
/path_to_project
$ python3 hello-ctypes/main.py
Hello World!
コメント