Pythonのctypesモジュールでネイティブライブラリを呼び出す

「このライブラリを使えば簡単に実装できるのにバインディング/ポーティングがないじゃん。。」といった経験がありませんか? この記事ではPythonのctypesモジュールでネイティブライブラリを使う方法を紹介します。
2018.06.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

普段の業務で色々なプログラミング言語を使う機会があると思いますが、「このライブラリを使えば簡単に実装できるのにバインディング/ポーティングがないじゃん。。」といった経験はありませんか? この問題を解決するために、多くの言語がFFIの仕組みを用意しています。

Pythonは標準で ctypesモジュール を提供していて、簡単にネイティブライブラリを使用できるようになっています。 今回はこの ctypesモジュール 経由で Zstandard 圧縮ライブラリを試してみます。

今回はctypesモジュールの紹介のためこの方法でZstandardを利用していますが、PythonでZstandardを使用する場合はPython用のライブラリの使用をおすすめします。詳細は公式の Bindings for other languagesを参照してください。

検証環境

  • macOS: 10.12.6
  • Homebrew: 1.6.6
  • Python 3.6.5
  • Zstandard: 1.3.4

ctypesモジュールの概要

ctypesはPythonからネイティブライブラリを呼び出すためのモジュールです。ネイティブライブラリを使用する場合、通常は下記どちらかの方法でリンクします。

  • 静的リンク : プログラムをビルドするときにライブラリをリンクする
  • 動的リンク : プログラム起動後にライブラリをリンクする

静的リンクはプログラムをビルドするときにライブラリをリンクします。gccやclangのログによく出てくる -lm-lz といった指定が静的リンクの指定です。静的リンクの場合は依存ライブラリが読み込めないとプログラムを実行できません。

動的リンクはプログラム起動後にライブラリをリンクします。実行環境によりリンク方法が異なり、Windows系は LoadLibrary 系のAPIを、 Unix系は dlopen を使うことが多いと思います。

ctypesモジュールは動的にライブラリをリンクするので、自分の使いたいライブラリをPythonから呼び出せるようになります。

検証内容

下記の操作で、ネイティブライブラリを正しく呼び出せていることを検証します。

  1. ZstandardのCLIツールzstdコマンドを使って圧縮データを用意する
  2. ctypesモジュールでネイティブライブラリを呼び出しデータを伸張する
  3. 伸張したデータと元のデータが一致するか検証する

前準備

Zstandardをインストール

Zstandardをインストールします。macOSの場合、Homebrewでインストールできます。

$ brew install zstd
$ zstd --version
*** zstd command line interface 64-bits v1.3.4, by Yann Collet ***

Windows・Linuxで試す場合は、各環境のパッケージマネージャを使ってインストールしましょう。パッケージマネージャに登録されていない場合は、 リポジトリをクローンして、ビルド手順 に従いビルドしましょう。cmake・Makefile・Visual Studioのプロジェクトと用意されているので、好みの方法でビルドできます。

圧縮データを用意

以前に投稿した Mimesisを活用してモックデータを手軽に生成する で紹介した方法でjsonデータを作成します。作成が完了したら、zstdコマンドで圧縮します。

# モックデータ生成
$ python generate_books.py > books.json

# データサイズを確認
$ ls -l
total 444312
-rw-r--r--  1 yoshihitoh  staff  227485681  5 31 20:13 books.json

# データを圧縮
$ zstd books.json
books.json           : 13.65%   (227485681 => 31051282 bytes, books.json.zst)

およそ217MiBのファイルを約30MiBまで圧縮できました。以降の操作で、ctypesモジュールを使って 圧縮データ books.json.zst を伸長します。

ctypesモジュールを使う

以下の流れで処理していきます。

概要

  1. ctypes.LoadLibrary でネイティブライブラリを読み込む
  2. ネイティブライブラリの関数に戻り値・および引数の型を設定する
  3. メモリ入出力を受け取る場合、構造体やメモリ配列を用意する。
  4. メモリ入力がある場合、pythonのデータを構造体やメモリに設定する
  5. ネイティブライブラリの関数を呼び出す

データの伸長にあたって以下の関数を使用します。各関数の定義は Emscriptenで試した記事 と同じです。

  • ZSTD_isError : エラー判定
  • ZSTD_getFrameContentSize : 伸長後のデータ長取得
  • ZSTD_decompress : データ伸長

試してみる

まず、ライブラリを読み込みます。ライブラリの配置場所や拡張子など、環境にあわせたものを指定します。macOSの場合、共有ライブラリの拡張子は .dylib を指定します。

zstd = cdll.LoadLibrary('libzstd.dylib')

次に、使用する関数の戻り値・引数の型を設定します。

zstd.ZSTD_isError.restype = c_uint32
zstd.ZSTD_isError.argtypes = (c_size_t,)

zstd.ZSTD_getFrameContentSize.restype = c_ulonglong
zstd.ZSTD_getFrameContentSize.argtypes = (c_void_p, c_size_t)

zstd.ZSTD_decompress.restype = c_size_t
zstd.ZSTD_decompress.argtypes = (c_void_p, c_size_t, c_void_p, c_size_t)

入力バッファ・出力バッファを割り当てます。出力バッファのサイズは伸張後のデータ長と同じサイズが必要なので、 ZSTD_getFrameContentSize を呼び出して取得します。

# バッファ作成用のヘルパー関数
def create_buffer(buffer_size):
    return (c_ubyte * buffer_size)()

# 入力バッファ
compressed_bytes = open('books.json.zst', 'rb').read()
src_buffer = create_buffer(len(compressed_bytes))
for i, b in enumerate(compressed_bytes):
    src_buffer[i] = b

# 出力バッファ、 入力バッファの内容をもとに算出する
original_size = zstd.ZSTD_getFrameContentSize(src_buffer, len(src_buffer))
if zstd.ZSTD_isError(original_size):
    raise ValueError('圧縮データが不正、伸長後のデータ長を取得できない')

dst_buffer = create_buffer(original_size)

ここまででデータを伸張する準備ができました。 ZSTD_decompress 関数を呼び出して伸張してみます。

rc = zstd.ZSTD_decompress(dst_buffer, len(dst_buffer), src_buffer, len(src_buffer))
if zstd.ZSTD_isError(rc):
    raise ValueError('圧縮データが不正、データを伸長できない')

# 戻り値=伸張後のサイズが、事前に取得した伸張後のデータ長と一致するか検証する
assert rc == original_size

最後に、ファイルに出力して元のデータと一致するか検証します。

with open('books_decompressed.json', 'wb') as wf:
    wf.write(dst_buffer)
$ diff books.json books_decompressed.json
$ echo $?
0

$ head -n 2 books.json                                                              金  6/ 1 18:06:40 2018
{"id": 1, "title": "The arguments can be primitive data types or compound data types.", "author": {"id": 4955, "first_name": "Jospeh", "last_name": "Kidd"}, "isbn": "021-1-74482-687-5", "publish_date": "02/06/1856"}
{"id": 2, "title": "The syntax {D1,D2,...,Dn} denotes a tuple whose arguments are D1, D2, ... Dn.", "author": {"id": 8827, "first_name": "Catherina", "last_name": "Riggs"}, "isbn": "1-38558-473-3", "publish_date": "12/18/1925"}

$ head -n 2 books_decompressed.json
{"id": 1, "title": "The arguments can be primitive data types or compound data types.", "author": {"id": 4955, "first_name": "Jospeh", "last_name": "Kidd"}, "isbn": "021-1-74482-687-5", "publish_date": "02/06/1856"}
{"id": 2, "title": "The syntax {D1,D2,...,Dn} denotes a tuple whose arguments are D1, D2, ... Dn.", "author": {"id": 8827, "first_name": "Catherina", "last_name": "Riggs"}, "isbn": "1-38558-473-3", "publish_date": "12/18/1925"}

中身が完全一致で、問題なく伸張できていますね!

おわりに

今回はctypesモジュールを使って、Pythonからネイティブライブラリを扱えることを確認できました。 バインディングやポーティングが用意されていない場合は積極的に活用して、新しいライブラリを試していきたいですね!