Pythonで作成したプログラムを、Codonを使いダイナミックリンクライブラリへコンパイルし、C/C++から呼び出す方法についてのメモ
Codonとは
Codonは高性能なPythonコンパイラです。実行時のオーバーヘッドなしにPythonコードをネイティブなマシンコードにコンパイルし、シングルスレッドで10-100倍以上の高速化が実現できます。Codonの開発はGithub上で行われており、2021年頃から現在まで様々な機能追加が行われています。
出典: あなたのPythonを100倍高速にする技術 / Codon入門
https://zenn.dev/turing_motors/articles/e23973714c3ecf
重要なのは、Pythonをネイティブなマシンコードにコンパイルができる点。
セットアップ
/bin/bash -c "$(curl -fsSL https://exaloop.io/install.sh)"
Pythonをコンパイルする
検証環境
macOS Monterey 12.6.3
pyenv 2.3.9
Python 3.9.7
次のプログラムをサンプルに用意した。
def foo(n: int):
for i in range(n):
print(i * i)
return n * n
foo(10)
コンパイルには、 codon build
を使う。 -o
オプションで出力ファイル名を指定できる
codon build -o foo foo.py
出力した結果を file
コマンドで確認すると、ちゃんとMacの実行可能ファイルになっている。
そして、実行もできる。
% file foo
foo: Mach-O 64-bit executable arm64
% ./foo
0
1
4
9
16
25
36
49
64
81
ダイナミックリンクライブラリの作成
Codonを使って、Pythonで書かれたコードからダイナミックリンクライブラリを作成する。
Webでオプションの一覧が見つからなかったので、 codon build --help
の中から探す。
% codon build --help | grep lib
--lib - Generate shared library
-l <string> - Link the specified library (only for executables)
--libdevice=<string> - libdevice path for GPU kernels
--lib
オプションをつける とライブラリを作成できるようだ。
ライブラリを作るにあたってPythonのプログラムの修正が必要である。
C/C++から呼びたい関数の上に @export
と書かなければならない。
※ また、引数の型の定義が必要である。
@export
def foo(n: int):
for i in range(n):
print(i * i)
return n * n
プログラムを修正したら以下のようにライブラリを作成する。
codon build --lib -o libfoo.dylib foo.py
OSがMacなので、ダイナミックリンクライブラリの名前は、libXXX.dylib の形式が好ましい。
ちゃんとダイナミックリンクライブラリとして出力されるのが確認できる。
% file libfoo.dylib
libfoo.dylib: Mach-O 64-bit dynamically linked shared library arm64
ダイナミックリンクライブラリのリンク
「ダイナミックリンクライブラリにコンパイルしたPythonプログラム」を呼び出す方も用意する。
#include <stdint.h>
#include <stdio.h>
extern int64_t foo(int64_t);
int main() {
foo(10);
}
extern int64_t foo(int64_t)
のように、関数定義を用意する。
型の対応については公式ドキュメント参照。
あとは、通常のC/C++のコンパイルと同様にできる。
gcc -o call -L. -lfoo call.c
libfoo.dylib
をリンクしてることは otool -L
で確認できる。
% otool -L call
call:
libfoo.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.100.3)
実行
あとは通常のプログラム同様に実行できる。
% ./call
0
1
4
9
16
25
36
49
64
81
モジュールを使う
Pythonといえば、便利なモジュールが豊富である。
Codonでは、書き方に修正こそいるが import
で様々なライブラリを使うことができる。
Numpyを使ったPythonプログラムをコンパイルする。
from python import numpy as np
@export
def numpy_test():
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
c = a * b
print(c)
numpyを含む部分はCodonによるコンパイルはされず、Pythonの共有ライブラリから動的に呼び出されます
出典: あなたのPythonを100倍高速にする技術 / Codon入門
import
の前に from python
を追加する のがポイント。
#include <stdint.h>
#include <stdio.h>
extern void numpy_test();
int main() {
numpy_test();
}
先程と同じように、コンパイルしてリンクする。
% codon build --lib -o libnum.dylib num.py
% gcc -o call_numpy -L. -lnum call_numpy.c
最後に、 libpython.dylib
の場所を環境変数 CODON_PYTHON
に記述 して実行できる。
% CODON_PYTHON=~/.pyenv/versions/3.9.7/lib/lib/libpython3.9.dylib ./call_numpy
[[ 5 12]
[21 32]]
%
libpython.dylibの場所は、Pythonで調べられる。 参考
>>> from distutils.sysconfig import get_config_var
>>> print(get_config_var("LIBDIR"))
~/.pyenv/versions/3.9.7/lib
CODON_PYTHON
の設定を忘れた場合、次のようなエラーが出る。
% ./call_numpy
CError: dlopen(libpython.dylib, 0x0002): tried: '/Users/xxxxxxxxxxxx/.codon/lib/libpython.dylib' (no such file), '/Users/xxxxxxxxxxxx/.codon/lib/codon/libpython.dylib' (no such file), 'libpython.dylib' (no such file), '/usr/local/lib/libpython.dylib' (no such file), '/usr/lib/libpython.dylib' (no such file), '/Users/xxxxxxxxxxxx/xxxxxxxxx/xxxxxxxxxxx/xxxxxxx/libpython.dylib' (no such file)
Raised from: std.internal.dlopen.dlopen.2:0
/Users/xxxxxxxxxxxx/.codon/lib/codon/stdlib/internal/dlopen.codon:31:9
Backtrace:
[0x104c230f3] std.internal.dlopen.dlopen.2:0[str,int].240 at /Users/xxxxxxxxxxxx/.codon/lib/codon/stdlib/internal/dlopen.codon:31
[0x104c26fd7] std.internal.python.setup_python:0[bool].654 at /Users/xxxxxxxxxxxx/.codon/lib/codon/stdlib/internal/python.codon
[0x104c27067] std.internal.python.ensure_initialized:0[bool].664 at /Users/xxxxxxxxxxxx/.codon/lib/codon/stdlib/internal/python.codon:886
[0x104c27ea7] pyobj._import:0[str].773 at /Users/xxxxxxxxxxxx/.codon/lib/codon/stdlib/internal/python.codon:881
[0x104c298bb] main.0 at /Users/xxxxxxxxxxxx/xxxxxxxxx/xxxxxxxxxxx/xxxxxxx/num.py:1
zsh: abort ./call_numpy
pyenvでPythonをインストールしている場合、 libpython.dylib
がディレクトリに無いことがある。
その時は、 --enable-framework
を有効にして、Pythonを再インストールする。
env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -v 3.9.7
その他
@export
を忘れた場合、シンボルが見つからなくなる。
Undefined symbols for architecture arm64:
"_foo", referenced from:
_main in call-4aedb6.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
雑記
- 公式ドキュメントのinteroperabilityの内容を補完して日本語でまとめたものではあるが、試したことはまとめておくとよいので。
- Codonのコンパイルオプションはそれなりにあるが、すべてをまとめたドキュメントをまだ見つけられてないので、試せることはまだまだありそう。
- C/C++側でPythonのオブジェクトを取り回す方法については、まだ未検証なので検証したい。
参考にしたサイト
https://docs.exaloop.io/codon/interoperability/cpp
https://zenn.dev/turing_motors/articles/e23973714c3ecf
https://chacha-py.hatenablog.com/entry/2023/04/01/052243