はじめに
Pythonはグルー言語だと称されますが、その心は、他言語(C等)で書かれたライブラリを簡単に利用できる特徴を指しています。
他言語で実装されたステキライブラリ(データ分析周りの例ばかりになって恐縮ですが、NumPy
/Pandas
/Tensorflow
...)を、平易に記述できるPythonコードから利用できるのは開発者観点からもステキですね。
PythonからCをどう呼び出すのかと言いますと、SciPy公式ドキュメントにそのままズバリ「Using Python as glue」との章があり、こちらに2つの方法が書いてあります。
拡張モジュールを作成し、importコマンドを使用してPythonにインポートする方法
Pythonから直接共有ライブラリを呼び出す方法
前者については、CでPython処理系の拡張モジュールを作成する方法です。
Python処理系(CPython)はCで書かれていますから、Cでコードを書き、処理系に紐付けてあげることで、Pythonモジュールとして利用できるわけです。
**Python/C API**というPython処理系のAPIが公開されているので、こちらを利用して実装します。
後者についてはctypes
等のPythonモジュールを使ってCの共有ライブラリをロードし、ロードした共有ライブラリから直接関数を呼び出す方法です。
以降では、PythonとCを連携する2つの方法それぞれについてもう少し詳しく説明します。
方法①:拡張モジュールを作成しCと連携
拡張モジュールはPython/C APIを利用して作成するのは前述の通りです。
改めてPython/C APIの説明ですが、
Pythonインタプリタに対する様々なレベルでのアクセス手段をCやC++のプログラマに提供
するものです1。
愚直にPython/C APIを使って拡張モジュールを作成する手順としては、
-
python.h
をinclude
したCコードを書く(python.h
内にPython処理系のAPIに関する関数や型の定義) - できたCコードをビルドし、処理系が読み込めるように紐付ける
といった流れです。
基本的には、Cのステキなライブラリを包むラッパーコードをpython.h
使って作成して、Pythonから呼び出せるようにする、みたいなユースケースが多いかなと思います。
とはいえ、Python/C APIを直書きしてラッパーコードを書くことにはツラミもありまして、
- 手作業での開発だと作成や保守作業にかかるコストが大きいという点2
- C/C++を書く必要がある点(Pythonプログラマには辛い)
があげられるかと思います。
前者については、ラッパーを自動生成するツール、例えばSWIG
等の解決方法があります。
SWIG
では、簡易な定義ファイルをもとにラッパーコードが自動生成されます。
後者についてはPythonライクなCython
でコードを書くことでツラミを軽減する方法等があるかなと思われます。
方法②:直接共有ライブラリを呼び出す(FFI)
FFI(Foreign function interface)とは
Wikipedia引用。
Foreign function interface (FFI)とは、あるプログラミング言語から他のプログラミング言語で定義された関数などを利用するための機構
まさにPythonからCのライブラリを呼ぶことはFFIですね。
前述の「拡張モジュールを作成しCと連携」することもFFIと呼ぶとは思いますが、本稿では便宜上、直接共有ライブラリを呼び出すような言語間連携を特にFFIと表現します。
FFIの流れについては、こちらの記事が圧倒的に分かりやすかったです。
流れを簡単にまとめると、
- Cの関数が入ったライブラリをロードする
- Cの関数(関数ポインタ)を呼び出す
- 関数の引数として渡すために、Pythonの型をCの型に変換する
- 関数の戻り値を受け取るために、Cの型をPythonの型に変換する
といった感じです。
ライブラリのロードも関数の呼び出しも、全てPythonコード上で行われます。
ライブラリのロードや言語間の型変換3といった操作はFFIでは頻出ですが、そうした機能をまとめたライブラリとして、例えばlibffi
が挙げられます。
libffi
をもとにしたライブラリは多くのスクリプト言語に存在し、Pythonならctypes
、Rubyだとruby-ffi
等のライブラリ(モジュール)として利用できます。
ctypes
はPythonの標準モジュールです。
libffi
のラッパーであり、Cと互換性のあるデータ型を提供し、共有ライブラリ内の関数呼び出しを可能にします。
NumPy
の内部でも使われているようです。
ctypes、なんでCの関数を呼べるの?
実際にPythonからCを呼び出すFFIはどのように動作しているのでしょうか。
C等をコンパイルした機械語(バイナリ。ctypes
が呼び出す共有ライブラリももちろんバイナリです)の仕様のことをABIと呼びます。
ABIでは、
- 関数呼び出しで変更されるレジスタとされないレジスタ
- intやlongなどの型のサイズ
- 構造体のレイアウトのルール
- ビットフィールドのレイアウトのルール
等が規定されますが、今回の例で特に重要なのが、関数の呼び出し方(呼び出し規約)が定まっている点です。
ABIの通りに、引数を格納する/戻り値がセットされる場所(レジスタ等)の把握等ができていれば、関数を呼び出すことができるわけです。
C/C++ともに標準化されたABIはない(異なるコンパイラでは互換性のないバイナリを生成する可能性がある)のですが、Cは古く安定した言語なので、異なるコンパイラ間でも(基本的に)同じABIを満たしたバイナリが得られます。
ctypes
ではCのABIを満たした機械語を受け取ることを想定しています。
CのABI通りに共有ライブラリを解釈することで、関数呼び出しが可能になるというスンポーです。
例えばGoでは-buildmode=c-shared
をつけたビルドでCのABIを持ったバイナリを作れるので、こいつをctypes
に食わせればPythonから呼び出せます。
このように、CのABIを通せばさまざまな言語間を連携することができます。
まとめ
Pythonは拡張モジュールやFFIを利用する仕組みが整っているので、他言語で作られた資産を利用しやすいグルー言語であることが分かりました。
普段はほとんどPython書かないので誤り等あればご指摘お願いいたします🙇♂️
用語整理など
ラッパー、バインディング、ポーティング
FFI等について調べているとよく出てくる用語ですが、上記の記事を参考にまとめました。
- ラッパー
- ライブラリの機能を同じ言語でラッピング
- バインディング
- ライブラリの機能を別の言語でラッピング
- ポーティング
- ライブラリを別の言語で書き換え
とはいえ原理原則ということではなく、原義的にはこんな感じって認識で良いと思います。
Pyhonでは他にどんな言語間連携がある?
こちらの記事にまとまっています。
-
余談ですが、C以外で書かれた処理系、PyPyとかではC APIをエミュレートしたりしてるようです(詳しくは読んでない) ↩
-
「Web技術文書からのFFI自動生成に関する実践」参照。それこそCライブラリの変更を追従する必要があったりは辛そう ↩
-
FFIでは型の変換が頻出ですが「そもそも型のメモリ上の表現が違うから変換する必要がある」みたいな理解で良いんですかね ↩