株式会社クリアコード > ククログ

ククログ

タグ:

Python/CFFIを利用してC拡張を作成する (2/2)

本記事は全2回の連載の後編です。前回の記事はこちらから読めます。

前回の記事ではCFFIライブラリを使うことで、C関数のプロトタイプ宣言から Pythonの拡張モジュールを自動的に生成できることを見ました。

この後編ではCFFIライブラリの実務的な使い方を解説したいと思います。

(以下の内容は Ubuntu 16.04 / Python 3.5.1 で動作を確認しています)

1. C拡張作成の基本的な流れ

本節ではCFFIでC拡張モジュールを作成する際の基本的なフローを解説します。 前回に引き続いて、暗号ライブラリのlibsodiumを題材として具体的に手順を追っていきます。

1.1. CFFIをインストールする

まずはCFFIをインストールします。 たいていのディストリビューションでパッケージが用意されているので、 これを利用するのが最も手っ取り早いです。

$ sudo apt install python3-cffi

あわせて、C拡張のコンパイルに必要になるので、 gccとPythonのヘッダファイルもインストールしておいてください。

$ sudo apt install gcc python3-dev

なおCFFIそのものはPyPI経由でもインストール可能です。

$ pip install cffi
1.2. 対象のライブラリをインストールする

続いて、呼び出しの対象となるCライブラリをインストールします。 ここでは、共有ライブラリ本体だけではなく、ヘッダファイルも一緒にインストールします。

例えば、libsodiumの場合は次のようにインストールします:

# 次のパッケージをインストールする
# - libsodium18   ... 共有ライブラリ本体 (libsodium.so)
# - libsodium-dev ... ヘッダファイル (sodium.h)
$ sudo apt install libsodium18 libsodium-dev

他のライブラリも多くのケースで同様のパッケージングがされているので、 利用したいライブラリをapt searchコマンドで探してみてください。

1.3. CFFIにライブラリの情報を渡す

以下のようなビルド定義を作成し、builder.pyという名前で保存します。

from cffi import FFI

ffibuilder = FFI()

ffibuilder.cdef("""
    size_t crypto_stream_keybytes(void);
    size_t crypto_stream_noncebytes(void);
""")

ffibuilder.set_source("_sodium", """
    #include <sodium.h>
""", libraries=["sodium"])

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)

このスクリプトはlibsodium前提の内容になってますが、 cdef()set_source()の二つのメソッドの呼び出しを書き換えることで、 他のライブラリに応用することができます。 この二つのメソッドの違いは一見すると分かりにくいのですが、 次のような住み分けがなされています。

ffi.cdef()

  • Pythonにエクスポートする関数のプロトタイプ宣言を渡します。
  • CFFIはここで定義された各関数についてPython向けの実装を自動生成します。

ffi.set_source()

  • cdefで渡した以外の、ビルドに必要なあらゆる情報を渡します。
  • それぞれの引数はdistutilsやコードジェネレータに引き継がれます。

各メソッドの詳しい使い方は2節に譲ります。

1.4. C拡張モジュールを生成する

定義したスクリプトをPythonに渡すと一連のビルド処理が走り、C拡張が生成されます。

$ python3 builder.py

カレントディレクトリにC拡張モジュール (*.so) が生成されていることが確認できたら、 実際にPythonから処理を呼び出してみましょう。

>>> from _sodium import ffi, lib
>>> lib.crypto_stream_keybytes()
32
>>> lib.crypto_stream_noncebytes()
24

これでPythonから共有ライブラリの処理を呼び出すことができるようになりました。

2. ライブラリの情報の渡し方

C拡張を生成する際にはcdef()set_source()の二つのインターフェイスを通じて、 必要な情報を渡すことになります。 各メソッドの取扱いには多少分かりづらい部分があるので、本節で要点を解説します。

2.1. cdef: 関数のプロトタイプ宣言を渡す

cdef()メソッドには、Pythonにエクスポートしたい関数のプロトタイプ宣言を渡します。 ここで渡した定義(関数名・型情報)をもとにC拡張のコードが自動的に生成されます。

# 例: libsodiumのストリーム暗号処理をエクスポートする
ffibuilder.cdef("""
  size_t crypto_stream_keybytes(void);
  size_t crypto_stream_noncebytes(void);
  int crypto_stream(unsigned char *c, unsigned long long clen,
                    const unsigned char *n, const unsigned char *k);
""")

定義を渡さなかった関数については、実装が生成されない点に注意してください。

作業時のポイント

cdef()に渡す関数名と入出力型はライブラリ側の定義と厳密にマッチする必要があります。 このため、実務的な作業としては、ライブラリ本体のヘッダファイルからプロトタイプ宣言を 一つ一つコピペしていくことになるのですが、作業にあたってはいくつかの注意点があります:

  1. この定義の中ではCの任意の文法が使えるわけではありません。

    • 例えば、#includeはサポートされていませんし、マクロも原則として使えません。
    • これはCFFI独自の処理系によって解析されることに由来する制約です。
  2. 定義の解釈にあたって、ライブラリ本体のヘッダファイルは参照されません。

    • 従って、定義が自己完結するように配慮する必要があります。
    • 例えば、関数定義にマクロが利用されている場合、手で展開する必要があります。

特殊な記法を使えば多少は融通を聞かせることもできるのですが、実際の取扱いでは、 ライブラリの定義に準拠した、単純な関数宣言のみで構成するのが最も障害が少ないです。

補足として、この中で構造体や型を定義することもできます。 本記事では取り扱わないので、これに関心のある方はcdef()のドキュメントを参照してください。

2.2. set_source: ビルドに必要な情報を渡す

set_source()メソッドにはビルドに必要なその他の情報を渡します。 名前からは非常に分かりづらいのですが、このメソッドを通じてコード生成からコンパイル までの一連のフローを制御できる設計になっています。

以下に主要な引数についてインラインで解説を加えます:

ffibuilder.set_source(
    # module_name: 生成されるC拡張モジュールの名称
    # 例えば'foo'とすると`import foo`でインポート可能になります。
    module_name="_sodium",

    # source: 自動生成時に埋め込むコード
    # この引数を通じて任意のC言語の処理を自動生成コードに埋め込むことができます。
    # (ただ現実の大半のケースではライブラリのヘッダをincludeするだけです)
    source="""#include <sodium.h>""",

    # source_extension: 生成されるソースファイルの拡張子
    # 具体的な利用例としては、C++の拡張を生成する場合に'.cpp'を指定します。
    source_extension='.c',

    # libraries: リンカに渡されるライブラリ情報
    # 以下の例ではリンカの実行オプションに`-lsodium`を追加しています。
    # この指定を省くとインポート時に未定義シンボルエラーが発生します。
    libraries=["sodium"]
)

このメソッドは実質的に「distutilsのプロキシ」という性格が強いです。 キーワード引数は原則としてdistutils.core.Extensionsにそのまま引き継がれる作りになっているので、 ビルドの細かい制御を行いたい場合は distutilsのリファレンス を参照して引数を調整してください。

3. より複雑な関数に対応する

CとPythonは基本的なセマンティクスが異なっているので、 単純にCの関数をPythonに機械的にエクスポートしただけでは、扱いづらい場合が少なくありません。

例えば、ランダムなバイト列を生成するrandombytes_buf()関数を考えてみましょう。

void randombytes_buf(void * const buf, const size_t size);

これまでの解説を用いれば、この関数をエクスポートすること自体は容易にできます。 問題は、具体的にどのようにこの関数をPythonからコールするかです。 例えば、単純にPythonのオブジェクトを引数に与えると、 Pythonオブジェクトの内部構造を上書きしてしまい、予期しない動作を引き起こします。

>>> from _sodium import ffi, lib
>>> buf = b'x' * 64
>>> lib.randombytes_buf(buf, 64)  # ???

この問題を解決するには、CFFIのffiというインターフェイスを利用する必要があります。

3.1. FFIインターフェイス

ffiインターフェイスの役割は、Python上でC言語のセマンティクスを部分的に再現することです。 提供されている主要なメソッドを以下に示します:

名称 機能
ffi.new() メモリ領域を確保する
ffi.cast() 型変換(キャスト)を行う
ffi.sizeof() データ型のサイズを取得する
ffi.memmove() メモリ領域をコピーする
ffi.string() メモリ領域をPythonのバイト列に変換する

メソッドの一覧はリファレンスマニュアルを参照してください。

具体的な利用例を以下に示します:

>>> from _sodium import ffi, lib
>>> buf = ffi.new('char[]', 64)   # メモリ領域を確保する
>>> lib.randombytes_buf(buf, 64)  # 関数に引き渡す
0
>>> ffi.string(buf, 64)           # バッファをPythonのバイト列に変換する
b'\x1d\xedw+\xf9}\x8d!\xa3...'

Cのポインタ操作と同等の処理がPythonでできるようになっている事が見て取れると思います。

3.2. メモリ管理について

確保したメモリ領域はPython上ではcdataというオブジェクトとして表現されます。

>>> buf = ffi.new('char[]', 64)
>>> print(buf)
<cdata 'char[]' owning 10 bytes>

重要なポイントとして、確保したメモリ領域は、 対応するcdataオブジェクトのライフサイクルに紐付けて自動的に管理されます。 オブジェクトがGCによって回収されるとメモリ領域も解放されるので、 C言語のように開発者の側で手動で解放する必要がなくなっています。

# Pythonオブジェクトが回収されると、確保したメモリ領域も自動的に解放される。
# 例えば、次の関数は`intp`を明示的に解放していないが、メモリリークは起きない。
del foo():
    intp = ffi.new('int *')
    return lib.somefunc(intp)
3.3. モジュール作成のヒント

モジュールを使うために毎回ffiインターフェイスを操作する必要があるのは非常に面倒です。 そのため、CFFIでC拡張モジュールを作成する時は、一緒にPythonのラッパ実装を作成しておくと、 Pythonモジュールとしての使い勝手がぐんと向上します。

一般的に使われるテクニックは、C拡張のモジュールをアンダースコア付きの名前で生成しておいて、 その上にPythonの実装をかぶせるという方式です。

_mymodule ... CFFIで生成した素のC拡張モジュール
mymodule  ... Pythonで作成したラッパモジュール

前節のコードを例にとると、次のようなラッパ実装を sodium.py という名前で保存します。

from _sodium import ffi,lib

def get_randombytes(size):
    buf = ffi.new('char[]', size)
    lib.randombytes_buf(buf, size)
    return ffi.string(buf)

これでモジュールの利用者はffiインターフェイスを意識せずに、 ライブラリの機能を利用できるようになります。

>>> from sodium import get_randombytes
>>> get_randombytes(10)
b'\x93\x13\xf9z\xaaE\xf8gb\x01'

4. まとめ

本記事では、実務面に焦点をあててCFFIライブラリの使い方を解説しました。 PythonからCの共有ライブラリを扱う場合の参考になりましたら幸いです。

タグ: Python
2018-04-05

Python/CFFIを利用してC拡張を作成する (1/2)

クリアコードの藤本です。皆さん、Pythonでプログラムを書いていますか?

本記事から始まる前後編2回の連載では、Pythonの CFFIライブラリ — Cで実装された処理をPythonから呼び出すための仕組み — について解説したいと思います。

ここで "Cで実装された処理を呼び出す" とは、 例えば /usr/lib/libc.so に格納されているCのルーチンを、 Pythonプログラムから直接コールすることを指しています。 前編にあたる本記事では、 まず最初にCFFIライブラリの大きな仕組みについて簡単な説明を加えた上で、 実際にこのライブラリを動かしてみたいと思います。

CFFIは何を解決するのか

Pythonで会員制のWebサービスを開発しているとしましょう。

ユーザーの認証処理をセキュアに実装するために、 暗号ライブラリ libsodium のパスワードハッシュ関数を使いたいと考えたとします。 この暗号ライブラリは、もともと C(++) 向けに提供されているものなので、 何とか工夫してPythonから必要な関数を呼び出せるようにする必要があります。 どうすれば、これを実現できるでしょうか?

この問題に対する伝統的な戦略は、CPythonが提供しているC言語向けのAPIを利用して、 拡張モジュールを作成することです。具体的には次のようにヘッダ定義を読み込んで、 ラッパ関数を一つ一つ作成していくことになります:

#include <Python.h>
#include <sodium.h>

/*
 * PythonのC/APIで crypto_pwhash_str_verify() 関数をラップする
 */
static PyObject* sodium_pwhash_str_verify(PyObject *self, PyObject *args)
{
    char *hash, *password;
    Py_ssize_t len;
    int res;

    // 引数をCの型に変換する (Python -> C)
    if (!PyArg_ParseTuple(args, "yyn", &hash, &password, &len))
        return NULL;

    // 対象の関数をコールする
    res = crypto_pwhash_str_verify(hash, password, len);

    // 返り値をPythonのオブジェクトに変換する (C -> Python)
    return PyLong_FromLong(res);
}

// 他の関数も定義してモジュールとしてエクスポートする

この戦略は20年以上に渡って用いられており、 2018年現在も多くのPythonモジュールがこの方式を採用しています。 したがって、この方式が現実に(それもかなり有効に)機能することにはほとんど異論がありません。

一方で、この方式で拡張モジュールを作成するのには、 それなりの背景知識が要求されるというのもまた事実です。 およそC/APIは実行系/ランタイムの内部実装と表裏一体なので、 CPythonに特有の仕組みや細かい決まりごと(たとえば参照カウントの扱いや例外をめぐる処理) をおさえておく必要があるからです。

CFFIはこのようなPythonとCの間の橋渡しを楽にするために開発されたFFI (Foreign Function Interface) ライブラリです。 LuaJITのFFI実装を参考に、2012年に開発がスタートしました。 2015年にメジャーバージョンの1.0.1がリリースされており、 既に2Dグラフィック処理の cairo や、 サウンドサーバーの JACK との連携などに幅広く利用されています。

CFFIはどのように問題を解決するのか

最初の例に戻りましょう。

そもそも、Cで提供される関数は(原則として)入出力が型によってきっちり定義されるので、 Pythonとの結合部分はある程度自動で生成できるんじゃないか、というのはごく自然な発想です。 例えば、int foo(const char *)という関数定義が与えられたとすると、 引数の char* をPythonのバイト列に、 返り値の int をPythonの整数オブジェクトに機械的に対応させるのは、 それほど難しいことではなさそうです。

CFFIライブラリはまさにこの"つなぎ"の部分の自動生成を行ってくれます。 実例で見てみましょう。まずは、次の内容をbuild.pyというファイルに保存します:

from cffi import FFI

ffibuilder = FFI()

# libsodiumのヘッダ情報をincludeする
ffibuilder.set_source("sodium", """
    #include <sodium.h>
""", libraries=["sodium"])

# Python化したいC関数の定義を記述する
ffibuilder.cdef("""
    int crypto_pwhash_str_verify(const char * hash,
                                 const char * const passwd,
                                 unsigned long long passwdlen);
""")

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)

この例では、libsodiumの関数 crypto_pwhash_str_verify() を移植しています (上のC/APIの例で用いたのと同じ関数です)。 set_source()で読み込むライブラリを指定し、 cdef() で関数定義を与えているのが見て取れると思います。

依存関係をインストールした上で、このスクリプトを実行しましょう。 すると、CFFIが定義情報からPython向けのラッパ実装を自動的に生成し、 コンパイルまで行ってくれます:

# 必要なライブラリのインストール (Ubuntu/Debian)
$ sudo apt install python3-cffi libsodium-dev
...
# C拡張コード生成 + コンパイル
$ python3 build.py
...
# 生成結果
$ ls
build.py sodium.c sodium.cpython-35m-x86_64-linux-gnu.so sodium.o

このsodium.cがC拡張のソースコードで、 sodium.*.soというのがコンパイル済みのモジュールです。

このモジュールはそのままPythonから呼び出すことができます。 試しに、適当な入力を与えてみましょう:

>>> from sodium import lib
>>> passwd = b'secret'
>>> pwhash = (b'$argon2i$v=19$m=131072,t=6,p=1$r/g1+z50+W9RWUMRy4xu+g$'
...           b'BNWoK+o6Hlcu98scoCxlNrYGo8hacShQ2nkc4RS5wZk')
>>> lib.crypto_pwhash_str_verify(pwhash, passwd, len(passwd))
0

この例で、C言語の型とPythonのオブジェクトが透過的に接続されているのが 確認いただけると思います。

残された課題/後編へのつなぎ

連載の前編を締めるにあたって、大急ぎで次の二つの点を指摘しておきたいと思います。

まず第一は、このコードジェネレーティングの戦略は万能の解決策ではないということです。 前節で見たとおり、簡単な関数であれば簡単に移植できます。 しかし、真面目にライブラリを移植しはじめると、 ほぼ間違いなく自動生成だけでは対応できないケースに出くわすことになります。

この代表例がポインタです。とくに、Cのコードでよく使われるテクニックとして 「呼び出し側でデータ領域を確保して、ポインタ経由で関数に中身を書き換えてもらう」 というコードパターンがありますが、これをどうPythonのコードに置き換えるかは、 決して自明ではありません。例えば、次の関数を移植する方法を考察してみてください:

// `size`バイト分のランダムデータで`buf`を埋める
void randombytes_buf(void * const buf, const size_t size);

この問題の解決は後編に回したいと思います。

もう一つは、CFFIライブラリの技術的な位置づけについてです。 実はここまでの「CFFI = C拡張の自動生成ライブラリ」という定式化はかなり話を端折ったものです。 この点については、他の競合する技術(Cython/ctypes/SWIG)との関係で説明する必要があるので、 後編で一節をまるまる割く予定です。

つづき: 2018-04-05
タグ: Python
2018-01-17

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|
タグ: