内容
PythonからGoを、GoからPythonを呼び出してみようという試みです。
前回の記事にも書きましたが、PythonとGoはCを介して連携ができますので、cgo使えばやれそうです!
cgoとは
概要
GoからCを呼ぶためのツールで、CライブラリにアクセスするGoコードを作るために使われたりします1。
まずはシンプルに試してみます。
package main
/*
#include <stdio.h>
void my_print(void)
{
printf("Hello, World!\n");
}
*/
import "C"
func main() {
C.my_print()
}
結果がこちらです。
# ビルド
$ go build -o main
# 実行
$ ./main
Hello, World!
仕組み
cgoは実に面白い仕組みになっています。import "C"がcgoを使う合図になっていますが、その上のコメントはただのコメントではありません。import "C"のすぐ上に書いたコメントはCのコードとして認識され、それに従いcgoはCの関数を呼び出すためのコードを自動生成します。
基本的な流れは、
-
import "C"
を行う -
import "C"
の直前のコメント(preamble)2に呼び出したい関数や変数の宣言を書く -
go build
を実行
です。
go build
実行時、import "C"
を使用したGoファイルがひとつでもあれば配下のCファイルのコンパイル等を開始します3。
cgoを伴うgo build
では、
- Go/C間のギャップ(型など)を吸収するためのグルーコード生成
- Go/Cコードのコンパイル
- 各種ライブラリのリンク
等を行なったのちに成果物であるバイナリを吐き出します。
一連の流れについてはこちらの記事に記載の図が圧倒的に分かりやすかったです。
グルーコードの生成、Cコードのコンパイル、ライブラリとのリンク等、裏側で行われる処理が可視化されています。
ちなみになんでGoの世界なのにCファイルをコンパイルできるのかと申しますと、裏でCコンパイラが普通に呼び出されているようです。
PythonからGo、GoからPythonを呼び出し合ってみる
ここからが本題です。
先に言っておきますと、今回は怠慢極まれり、コンテナ等使わずローカルで実行しています。
(本記事の目的がPythonとGoの連携を「ばっくり」理解するためだからです)
環境差異によっては実行できない場合もありますのであらかじめご留意ください。
とはいえ大枠のソースコードやビルドの流れは一緒だと思います。
自分の環境は下記のような感じです。
- MacBook Pro (16-inch, 2019)
- macOS11.2.3
- Go1.16.2
- Python3.9.5
PythonからGoを呼ぶ場合
Pythonから呼び出されるGoコードがこちらです。
package main
import (
"C"
"fmt"
)
//export my_print
func my_print() {
fmt.Println("Hello, World!")
}
func main() {}
ポイントは、
- パッケージは
main
である必要がある -
main
関数は実行されないが宣言する必要がある - エクスポートする関数に
//export funcname
を付与(exportとスラッシュの間は空けない)
です。
呼び出し側であるPythonコードです。
こちらはやたらシンプルになりました。
from ctypes import *
import ctypes
lib = cdll.LoadLibrary("./export.so")
lib.my_print()
ビルドと実行をやってみましょう。
go build
の際にc-shared
をつけることでC ABIを持った共有ライブラリが作成できます。
# ビルド
$ 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!」が確認できました。
前回の記事でも書いたようにctypesはC ABIを持った共有ライブラリを読み込めるモジュールです。
なのでc-shared
な共有ライブラリを作って食わせてやることでうまく呼び出せた次第です。
GoからPythonを呼ぶ場合
今度はGoからPythonを呼びます。
PythonからGoを呼ぶ場合同様、Cを介さなくては相互にやり取りできないので、cgoを使います。
まずは完成コードです。
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)
}
実行結果はこちらです。
# ビルド
$ go build -o main
# 実行
$ ./main
Hello, World!
Python(CPython)にはPython/C APIがあるので、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>
の上に、
// #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
とのコメントがあるかと思います。
コメントを取り除いて実行してみると、
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
と拡張子を除いたライブラリ名を指定してあげる必要があります
今回のPythonはpython.org
の公式インストーラから入れたものなので、
# 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
「Embedding Python in 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
は、
$ brew install pkg-config
で入れられました。
自分のMac環境(何度も言いますがpython.org
の公式インストーラから入れたPython)の場合、
# 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。
# 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なるパッケージを作ってるっぽいので使ってみたいです。
参考リンク
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がかなり分かりやすいです