LoginSignup
16
21

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-06-19

内容

PythonからGoを、GoからPythonを呼び出してみようという試みです。
前回の記事にも書きましたが、PythonとGoはCを介して連携ができますので、cgo使えばやれそうです!

cgoとは

概要

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

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

main.go
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の関数を呼び出すためのコードを自動生成します。

Go言語からC言語の関数を呼び出す

基本的な流れは、

  • import "C"を行う
  • import "C"の直前のコメント(preamble)2に呼び出したい関数や変数の宣言を書く
  • go buildを実行

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

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

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

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

代替テキスト
cgoを使ったCとGoのリンクの裏側 (1)

ちなみになんで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コードがこちらです。

export.go
package main

import (
    "C"
    "fmt"
)

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

func main() {}

ポイントは、

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

です。

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

host.py
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を使います。

まずは完成コードです。

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)
}

実行結果はこちらです。

# ビルド
$ 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と拡張子を除いたライブラリ名を指定してあげる必要があります

LDFLAGS、CFLAGSについて

今回の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"

といった形で、LDFLAGSCFLAGSを用いずスマートに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

先ほどLDFLAGSCFLAGSで指定したのと全く同じ内容が出力されています!

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

まとめ

全く実用的なお話ではないですが、PythonからGo、GoからPythonを呼び出し合うことができました。
次はDatadogがgo-pythonなるパッケージを作ってるっぽいので使ってみたいです。

参考リンク

cgo

pkg-config

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


  1. Goにとって唯一のFFIツールというわけでなくSWIG等も利用できるらしいです。C++連携だとSWIGの方が良いとの声も 

  2. import "C"とコメントの間に改行があると動作しません 

  3. このあたり公式にまとまっています 

  4. 共有ライブラリの拡張子はOSによって異なります。*.dylibの他に*.so*.dllなどがあります 

  5. ちなみになぜpython-3.9-embed.pcを選んだのか、何故python-3.9.pcpython3.pcではないのかと申しますと、後者の場合はLibsのフィールドが空だったため 

16
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
21