Python – ctypes で C++ メソッドを呼び出す

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.pylibhello.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.pylibhello.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 がその親ディレクトリを返却します。
最後に joinpathlibhello.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!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次