PythonからGo、GoからPythonを呼び出し合う

# 内容

PythonからGoを、GoからPythonを呼び出してみようという試みです。
[前回の記事](https://qiita.com/kawamou/items/05028c1a871d42119473)にも書きましたが、PythonとGoはCを介して連携ができますので、[cgo](https://golang.org/cmd/cgo/)使えばやれそうです！

https://qiita.com/kawamou/items/05028c1a871d42119473

# cgoとは
## 概要

GoからCを呼ぶためのツールで、CライブラリにアクセスするGoコードを作るために使われたりします[^1]。

https://golang.org/cmd/cgo/

まずはシンプルに試してみます。

```go:main.go
package main

/*
#include <stdio.h>

void my_print(void)
{
    printf("Hello, World!\n");
}
*/
import "C"

func main() {
	C.my_print()
}

```

結果がこちらです。

```sh
# ビルド
$ go build -o main

# 実行
$ ./main
Hello, World!
```

## 仕組み

>cgoは実に面白い仕組みになっています。import "C"がcgoを使う合図になっていますが、その上のコメントはただのコメントではありません。import "C"のすぐ上に書いたコメントはCのコードとして認識され、それに従いcgoはCの関数を呼び出すためのコードを自動生成します。

[Go言語からC言語の関数を呼び出す](https://schima.hatenablog.com/entry/20100523/1274620423)

基本的な流れは、

- `import "C"`を行う
- `import "C"`の直前のコメント（preamble）[^2]に呼び出したい関数や変数の宣言を書く
- `go build`を実行

です。
`go build`実行時、`import "C"`を使用したGoファイルがひとつでもあれば配下のCファイルのコンパイル等を開始します[^3]。
cgoを伴う`go build`では、

- Go/C間のギャップ（型など）を吸収するためのグルーコード生成
- Go/Cコードのコンパイル
- 各種ライブラリのリンク

等を行なったのちに成果物であるバイナリを吐き出します。

一連の流れについては[こちらの記事](https://qiita.com/yugui/items/e71d3d0b3d654a110188)に記載の図が圧倒的に分かりやすかったです。
グルーコードの生成、Cコードのコンパイル、ライブラリとのリンク等、裏側で行われる処理が可視化されています。

<img width="640" alt="代替テキスト" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F1806%2Fde5a7a2b-5f23-cfd9-acf3-e42ac52ea810.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=471922e0b2d38f7603e5d06ebcd0af42">
[cgoを使ったCとGoのリンクの裏側 (1)](https://qiita.com/yugui/items/e71d3d0b3d654a110188)

ちなみになんでGoの世界なのにCファイルをコンパイルできるのかと申しますと、裏でCコンパイラが[普通に呼び出されている](https://stackoverflow.com/questions/44856124/can-i-change-default-compiler-used-by-cgo)ようです。

# PythonからGo、GoからPythonを呼び出し合ってみる

ここからが本題です。
先に言っておきますと、今回は怠慢極まれり、コンテナ等使わずローカルで実行しています。
（本記事の目的がPythonとGoの連携を「ばっくり」理解するためだからです）
環境差異によっては実行できない場合もありますのであらかじめご留意ください。
とはいえ大枠のソースコードやビルドの流れは一緒だと思います。

自分の環境は下記のような感じです。

- MacBook Pro (16-inch, 2019)
- macOS11.2.3
- Go1.16.2
- Python3.9.5

## PythonからGoを呼ぶ場合

Pythonから呼び出されるGoコードがこちらです。

```go:export.go
package main

import (
	"C"
	"fmt"
)

//export my_print
func my_print() {
	fmt.Println("Hello, World!")
}

func main() {}
```

[ポイント](https://qiita.com/yanolab/items/1e0dd7fd27f19f697285)は、

- パッケージは`main`である必要がある
- `main`関数は実行されないが宣言する必要がある
- エクスポートする関数に`//export funcname`を付与（exportとスラッシュの間は空けない）

です。

呼び出し側であるPythonコードです。
こちらはやたらシンプルになりました。

```python:host.py
from ctypes import *
import ctypes

lib = cdll.LoadLibrary("./export.so")
lib.my_print()
```

ビルドと実行をやってみましょう。
`go build`の際に`c-shared`をつけることでC ABIを持った共有ライブラリが作成できます。

```sh
# ビルド
$ go build -buildmode=c-shared -o export.so

# 確認
$ ls
export.go export.so

# 実行（export.soをPythonコードのディレクトリに移し替えてください）
$ python3 host.py
Hello, World!
```

Pythonを実行し、Goで記述した「Hello, World!」が確認できました。
[前回の記事](https://qiita.com/kawamou/items/05028c1a871d42119473)でも書いたようにctypesは**C ABIを持った共有ライブラリを読み込めるモジュール**です。
なので`c-shared`な共有ライブラリを作って食わせてやることでうまく呼び出せた次第です。

## GoからPythonを呼ぶ場合

今度はGoからPythonを呼びます。
PythonからGoを呼ぶ場合同様、Cを介さなくては相互にやり取りできないので、cgoを使います。

まずは完成コードです。

```go:main.go
package main

// #cgo CFLAGS: -I/Library/Frameworks/Python.framework/Versions/3.9/include/python3.9
// #cgo LDFLAGS: -L/Library/Frameworks/Python.framework/Versions/3.9/lib -lpython3.9
// #include <Python.h>
import "C"

func main() {
	//  最後にPythonインタプリタ終了
	defer C.Py_Finalize()
	// Pythonインタプリタの初期化
	C.Py_Initialize()
	// GoのstringをCのcharに型変換（変換しないとPyRun_SimpleStringに型合ってないよって怒られる）
	// cannot use "print(\"Hello, World!\")" (type string) as type *_Ctype_char in argument to _Cfunc_PyRun_SimpleString
	pyCodeStr := `print("Hello, World!")`
	pyCodeChar := C.CString(pyCodeStr)
	// Pythonコードを文字列として受け取ってインタプリタ上で実行
	C.PyRun_SimpleString(pyCodeChar)
}
```

実行結果はこちらです。

```sh
# ビルド
$ go build -o main

# 実行
$ ./main
Hello, World!
```

Python（CPython）には[Python/C API](https://docs.python.org/ja/3/c-api/intro.html)があるので、C上でPythonインタプリタを動かせます。
今回の場合、Goからcgoを介してCにアクセスし、CからPython/C APIを使ってPythonインタプリタを動かしているイメージです。

Pythonコード自体は文字列としてインタプリタに渡し実行しています。
かなり短いですが`print("Hello, World!")`がPythonコードに該当する部分です。

**確かにGoの上でもPythonが動きました！**
これでPythonからGo、GoからPythonを呼び出す試みはすべて達成されました。
やったね！
ここからはただの余談です。

### 余談：cgoのコメントについて

`#include <Python.h>`の上に、

```go
// #cgo CFLAGS: -I/Library/Frameworks/Python.framework/Versions/3.9/include/python3.9
// #cgo LDFLAGS: -L/Library/Frameworks/Python.framework/Versions/3.9/lib -lpython3.9
```

とのコメントがあるかと思います。
コメントを取り除いて実行してみると、

```sh
fatal error: 'Python.h' file not found
```

のように`Python.h`が見つからんと怒られます。
cgoでは、Cコンパイラに、**使用するCライブラリ（及びそのヘッダファイル）の位置を教えてあげる必要**があります。
ヘッダファイル`Python.h`やその実体であるCライブラリはPythonを落としたディレクトリに存在しますので、その位置を伝える必要があるというわけです。

誤解を恐れずばっくり言うと、

- `#cgo CFLAGS:`でヘッダファイルが存在する位置等をコンパイラに伝えることができます
- `#cgo LDFLAGS:`でCライブラリが存在する位置等をコンパイラに伝えることができます。`-L`でCライブラリのディレクトリを指定し、`-l`で使用するライブラリを指定します。Cライブラリは`libfoo.dylib`みたいなファイル名になるのですが[^4]、先頭の`lib`と拡張子を除いたライブラリ名を指定してあげる必要があります

[LDFLAGS、CFLAGSについて](http://nalab.mind.meiji.ac.jp/~mk/labo/studying-C/Programing-in-C/node13.html)

今回のPythonは`python.org`の[公式インストーラ](https://www.python.org/downloads/)から入れたものなので、

```sh
# Pythonのバージョンは落としたPythonのバージョンで読み替える

# ヘッダファイル
/Library/Frameworks/Python.framework/Versions/3.9/include/python3.9

# Cライブラリ
/Library/Frameworks/Python.framework/Versions/3.9/lib
```

あたりに（Macの場合だと）ヘッダファイルやCライブラリがいます。

以上のように、cgoでは使用するCライブラリやヘッダファイルの位置について、コメントを介してCコンパイラに渡しています。

### 余談：pkg-config

https://poweruser.blog/embedding-python-in-go-338c0399f3d5

「Embedding Python in Go」では、

```go
// #cgo pkg-config: python3
// #include <Python.h>
import "C"
```

といった形で、`LDFLAGS`や`CFLAGS`を用いずスマートにcgoのコメントが書けています。

知見が無なので調査すると`pkg-config`とは、`*.pc`ファイルを元に、ビルドの際に必要な情報を返すツールだそうです。
Cライブラリをリンク等する際に必要な情報を`*.pc`形式にまとめておくからみんな利用してくれよな、みたいな世界観？
PythonのCライブラリ自体も`*.pc`を準備しているので、「Embedding Python in Go」ではソイツを読み込んで利用しているわけですね。

同じようにやってみましょう。

`pkg-config`は、

```sh
$ brew install pkg-config
```

で入れられました。

自分のMac環境（何度も言いますが`python.org`の[公式インストーラ](https://www.python.org/downloads/)から入れたPython）の場合、

```sh
# Pythonのバージョンは落としたPythonのバージョンで読み替える
$ cd /Library/Frameworks/Python.framework/Versions/3.9/lib/pkgconfig/

$ ls
python-3.9-embed.pc python3-embed.pc    tcl.pc
python-3.9.pc       python3.pc          tk.pc
```

あたりにPythonの`*.pc`ファイル群がいました。
複数ある`*.pc`ファイルの中から`python-3.9-embed.pc`をピックアップし`pkg-config`コマンドを試してみます[^5]。

```sh
# pkg-configはPKG_CONFIG_PATH環境変数で指定したディレクトリを探すため実行前には事前に設定
# cgoで使う前にも設定する
$ export PKG_CONFIG_PATH=/Library/Frameworks/Python.framework/Versions/3.9/lib/pkgconfig/


# Cライブラリの位置を出力
$ pkg-config --libs python-3.9-embed
-L/Library/Frameworks/Python.framework/Versions/3.9/lib -lpython3.9

# ヘッダファイルの位置を出力
$ pkg-config --cflags python-3.9-embed
-I/Library/Frameworks/Python.framework/Versions/3.9/include/python3.9
```

先ほど`LDFLAGS`や`CFLAGS`で指定したのと全く同じ内容が出力されています！

cgoでは`cgo pkg-config: foo`とコメントを書くことで、`pkg-config`が`foo.pc`から取得できるディレクトリ等の情報がCコンパイラに渡されているようです。

# まとめ

全く実用的なお話ではないですが、PythonからGo、GoからPythonを呼び出し合うことができました。
次はDatadogが[go-python](https://www.datadoghq.com/ja/blog/engineering/cgo-and-python/)なるパッケージを作ってるっぽいので使ってみたいです。

# 参考リンク
### cgo

- https://hnakamur.github.io/blog/2019/12/29/cgo-and-unsafe/
- https://dave.cheney.net/tag/cgo
- https://speakerdeck.com/rajeshr/cgo-go-under-the-hood
- https://www.slideshare.net/AllThingsOpen/hidden-dragons-of-cgo

### pkg-config

Wikipediaがかなり分かりやすいです

- https://ja.wikipedia.org/wiki/Pkg-config


[^1]: Goにとって唯一のFFIツールというわけでなくSWIG等も利用できるらしいです。[C++連携だとSWIGの方が良いとの声](https://speakerdeck.com/clsung/interfacing-c-libraries-with-go?slide=8)も
[^2]: `import "C"`とコメントの間に改行があると動作しません
[^3]: このあたり[公式](https://golang.org/cmd/cgo)にまとまっています
[^4]: 共有ライブラリの拡張子はOSによって異なります。`*.dylib`の他に`*.so`や`*.dll`などがあります
[^5]: ちなみになぜ`python-3.9-embed.pc`を選んだのか、何故`python-3.9.pc`や`python3.pc`ではないのかと申しますと、後者の場合はLibsのフィールドが空だったため