Codonを使いPythonで作成したプログラムをC/C++で活用する

Pythonで作成したプログラムを、Codonを使いダイナミックリンクライブラリへコンパイルし、C/C++から呼び出す方法についてのメモ

# Codonとは

https://github.com/exaloop/codon

> Codonは高性能なPythonコンパイラです。実行時のオーバーヘッドなしにPythonコードをネイティブなマシンコードにコンパイルし、シングルスレッドで10-100倍以上の高速化が実現できます。Codonの開発はGithub上で行われており、2021年頃から現在まで様々な機能追加が行われています。

出典: あなたのPythonを100倍高速にする技術 / Codon入門
https://zenn.dev/turing_motors/articles/e23973714c3ecf

重要なのは、Pythonをネイティブなマシンコードにコンパイルができる点。

## セットアップ
```bash:Codonのインストール
/bin/bash -c "$(curl -fsSL https://exaloop.io/install.sh)"
```

# Pythonをコンパイルする
検証環境
macOS Monterey 12.6.3
pyenv 2.3.9
Python 3.9.7

次のプログラムをサンプルに用意した。
```Python: foo.py サンプルプログラム
def foo(n: int):
    for i in range(n):
        print(i * i)
    return n * n
foo(10)
```

コンパイルには、 `codon build` を使う。 `-o` オプションで出力ファイル名を指定できる
```bash:Pythonのコンパイル
codon build -o foo foo.py
```

出力した結果を `file` コマンドで確認すると、ちゃんとMacの実行可能ファイルになっている。
そして、実行もできる。
```bash:コンパイルした結果と実行
% 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` の中から探す。

```bash:オプションの検索
% 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` と書かなければならない。**
※ また、引数の型の定義が必要である。

```Python:foo.py ライブラリ用に修正したプログラム
@export
def foo(n: int):
    for i in range(n):
        print(i * i)
    return n * n
```

プログラムを修正したら以下のようにライブラリを作成する。

```bash:ダイナミックリンクライブラリの作成
codon build --lib -o libfoo.dylib foo.py
```

OSがMacなので、ダイナミックリンクライブラリの名前は、libXXX.dylib の形式が好ましい。
ちゃんとダイナミックリンクライブラリとして出力されるのが確認できる。

```bash:出力されたlibfoo.dylibの確認
% file libfoo.dylib
libfoo.dylib: Mach-O 64-bit dynamically linked shared library arm64
```


# ダイナミックリンクライブラリのリンク
「ダイナミックリンクライブラリにコンパイルしたPythonプログラム」を呼び出す方も用意する。

```C:call.c 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++のコンパイルと同様にできる。

```bash:ダイナミックリンクライブラリのリンク
gcc -o call -L. -lfoo call.c
```

`libfoo.dylib` をリンクしてることは `otool -L` で確認できる。
```bash:
% 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)
```

## 実行

あとは通常のプログラム同様に実行できる。

```bash:callの実行
% ./call
0
1
4
9
16
25
36
49
64
81
```

# モジュールを使う
Pythonといえば、便利なモジュールが豊富である。
Codonでは、書き方に修正こそいるが `import` で様々なライブラリを使うことができる。

Numpyを使ったPythonプログラムをコンパイルする。

```Python:num.py 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` を追加する** のがポイント。

```C:call_numpy.c Pythonプログラムを呼び出す側
#include <stdint.h>
#include <stdio.h>

extern void numpy_test();

int main() {
  numpy_test();
}
```

先程と同じように、コンパイルしてリンクする。

```bash:コンパイルとリンク
% codon build --lib -o libnum.dylib num.py
% gcc -o call_numpy -L. -lnum call_numpy.c
```

最後に、  **`libpython.dylib` の場所を環境変数 `CODON_PYTHON` に記述** して実行できる。
```bash:call_numpyの実行
% CODON_PYTHON=~/.pyenv/versions/3.9.7/lib/lib/libpython3.9.dylib ./call_numpy
[[ 5 12]
 [21 32]]
% 
```

libpython.dylibの場所は、Pythonで調べられる。 [参考](https://stackoverflow.com/questions/72240777/)
```Python:libpython.dylibの場所を調べる。
>>> from distutils.sysconfig import get_config_var
>>> print(get_config_var("LIBDIR"))
~/.pyenv/versions/3.9.7/lib
```

`CODON_PYTHON` の設定を忘れた場合、次のようなエラーが出る。
```bash: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を再インストールする。

```bash:libpython.dylibを含めたPythonのインストール
env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -v 3.9.7
```

# その他
`@export` を忘れた場合、シンボルが見つからなくなる。
```bash:@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