バイナリ拡張

Page Status:Incomplete
Last Reviewed:2013-12-08

CPython リファレンスインタプリタの一つの特徴は、Python コードを実行するだけでなく、他のソフトウェアから利用できる豊富な C API を提供していることだ。この C API の最も一般的な使い方の一つは、インポート可能な C 拡張を作り、pure Python コードでは容易にはできないことを可能にすることだ。

バイナリ拡張の概要

ユースケース

バイナリ拡張の典型的なユースケースは、慣例的には 3 つのカテゴリに分かれる:

  • 高速化モジュール: これらのモジュールは完全に自己完結していて、単に CPython で動く同等な pure Python コードより高速に動作するために作られる。理想的には、高速化モジュールは同等な pure Python モジュールを常に備えており、あるシステムで高速版が利用できないときの fallback として使えるようになっているべきだ。CPython 標準ライブラリでは高速化モジュールが広く使われている。

  • ラッパーモジュール: これらのモジュールは既存の C インターフェースを Python コードから利用するために作られる。これらは基礎となる C インターフェースを直接公開するかもしれないし、あるいは API を使いやすくするために Python 言語機能を利用し、より “Pythonic” な API を公開するかもしれない。CPython 標準ライブラリではラッパーモジュールが広く使われている。

  • 低レベルシステムアクセス: これらのモジュールは CPython ランタイムのより低レベルな機能や、オペレーティングシステム、または基礎となるハードウェアにアクセスするために作られる。プラットフォーム固有のコードにより、拡張モジュールは pure Python コードでは不可能なことができるかもしれない。いくつかの CPython 標準ライブラリは C で書かれており、言語レベルでは公開されていないインタプリタの内部機能にアクセスしている。

    C 拡張の特筆すべき機能の一つは、インタプリタランタイムへコールバックする必要がない場合、時間のかかる操作(CPU bound か IO bound かによるが)の間、CPython の GIL (global interpreter lock) を解放できることだ。

全ての拡張モジュールが以上のカテゴリにきっちり当てはまるわけではない。例えば、NumPy に含まれる拡張モジュールはこれら 3 つのユースケース全てにわたっている - 速度上の理由で内部ループを C に移し、C, FORTRAN その他の言語で書かれた外部ライブラリをラップし、また CPython および基礎となるオペレーティングシステムの低レベルインターフェースを使うことで、ベクトル化演算の並列実行をサポートし、作成されるオブジェクトのメモリレイアウトを厳密に制御している。

欠点

バイナリ拡張を使う主な欠点は、その後のソフトウェア配布がより難しくなることだ。Python を使う利点の一つはその大部分がクロスプラットフォームであることだが、拡張モジュールを書くのに使われる言語(普通は C か C++ だが、実際は CPython C API にバインドできるなら何でもよい)は通常、プラットフォームごとにカスタムバイナリを作る必要がある。

つまり、バイナリ拡張は:

  • エンドユーザがソースからビルドできるか、または誰かが共通のプラットフォーム用のビルド済みバイナリを公開している必要がある。
  • CPython リファレンスインタプリタの異なるビルドに対して互換性がないかもしれない。
  • PyPy, IronPython, Jython などの代替インタプリタではしばしば正しく動かない。
  • 手で書かれている場合、メンテナンスがより難しくなる。なぜならメンテナは Python だけでなく、バイナリ拡張の作成に使われる言語や CPython C API の詳細にも精通していなければならないからだ。
  • Pure Python の fallback 実装が提供されている場合、メンテナンスがより難しくなる。なぜなら変更点は 2 箇所で実装する必要があり、両方のバージョンが常に動作することを保証するためのテストスイートはさらに複雑化するからだ。

バイナリ拡張に頼ることのもう一つの欠点は、代替インポートメカニズム(例えば zip ファイルからモジュールを直接インポートする機能)が拡張モジュールではしばしば動作しないことだ(ほとんどのプラットフォームにおける動的ロードメカニズムは、ディスクからライブラリをロードすることしかできないので)。

手書きの高速化モジュールに対する代案

拡張モジュールが単にコードを速く実行するために使われているだけなら(プロファイリングで速度向上がメンテナンスコストに見合うコードを見分けた後で)、他の選択肢もいくつか考慮すべきだ:

  • 既に最適化されたものがないか探す。CPython 標準ライブラリは多くの最適化されたデータ構造とアルゴリズムを含む(特に、組み込み関数と collections および itertools モジュールにおいて)。Python Package Index にはさらなる選択肢がある。場合によっては、標準ライブラリやサードパーティモジュールを適切に選ぶことで、独自の高速化モジュールを作る必要がなくなる。
  • 長時間動作するアプリケーションでは、JIT コンパイル機能を持つ PyPy インタプリタ を標準 CPython ランタイムの代わりに使うのが適切かもしれない。PyPy を採用する上で主な障害となるのは、他のバイナリ拡張モジュールへの依存だ - PyPy は確かに CPython C API をエミュレートするのだが、それに依存するモジュールは PyPy JIT で問題を起こすことがあり、またエミュレーションレイヤはしばしば CPython が現在許容している拡張モジュールの潜在的欠陥の影響を受ける(よくあるのは参照カウント周りのエラーだ - オブジェクトの参照カウント 2 が 1 になったとしても問題はないだろうが、1 が 0 になるのは重大な問題だ)。
  • Cython は成熟した静的コンパイラで、ほとんどの Python コードを C 拡張モジュールへコンパイルできる。最初のコンパイルで (CPython インタプリタレイヤを介さないことで)いくらか速度が向上する。また、Cython のオプションの静的型付け機能によりさらなる速度向上が見込める。Cython を使うと完成したアプリケーションの配布が複雑化する欠点はあるものの、Python プログラマにとって参入障壁が低い利点がある(C や C++ のような他の言語に比べれば)。
  • Numba はより新しいツールで、科学 Python コミュニティのメンバーにより作られた。これは LLVM を活用し、実行時に Python アプリケーションをネイティブコードへ選択的にコンパイルできるようにするのが目的だ。コードが実行されるシステムで LLVM が利用可能でなければならないが、大幅な速度向上が見込める。ベクトル化に適した操作では特にそうだ。

手書きのラッパーモジュールに対する代案

C ABI (Application Binary Interface) は複数のアプリケーション間で機能を共有するための標準だ。CPython C API (Application Programming Interface) の強みの一つは、Python ユーザがこの機能を利用できることだ。しかし、ラッパーモジュールを手で書くのはかなり退屈な作業なので、他のアプローチをいくつか見当すべきだ。

以下で述べるアプローチは配布を簡単にするものではないが、ラッパーモジュールを最新に保つメンテナンス負担を大幅に軽減 できる

  • Cython は高速化モジュールだけでなく、ラッパーモジュールの作成にも役立つ。ただしインターフェースは依然として手でラップしなければならないので、大きな API をラップする場合はよい選択肢ではないかもしれない。

  • cffi は PyPy 開発者の一部が作ったプロジェクトで、Python と C の両方を知っているプログラマが C モジュールを Python アプリケーション向けに公開するのを容易にするのが目的だ。また、C を知らなくても、ヘッダファイルに基づいて C モジュールをラップするのを比較的容易にしてくれる。

    cffi の主な利点の一つは、PyPy JIT と互換性があることだ。これにより、CFFI ラッパーモジュールは PyPy のトレース JIT 最適化の完全な恩恵を受けられる。

  • SWIG はラッパーインターフェース生成器で、 Python を含む様々な言語が C および C++ コードと連携できるようにする。

  • 標準ライブラリの ctypes モジュールは、ヘッダファイルがないときに C レベルのインターフェースにアクセスするのに便利だが、C ABI レベルでしか動作しないのが難点だ。このため、実際にライブラリがエクスポートするインターフェースと Python コードで宣言されたそれとの間の一貫性の自動チェックは一切行われない。対照的に、上記の選択肢は全て C API レベルで動作でき、C ヘッダファイルを使うことで、ラップ対象のライブラリがエクスポートするインターフェースと Python ラッパーモジュールが期待するそれとの間の一貫性を保証できる。 cffi は直接 C ABI レベルでも動作 できる が、そのような使い方をすると ctypes と同様のインターフェースの一貫性に関する問題を抱えることになる。

低レベルシステムアクセスに対する代案

(理由を問わず)低レベルシステムアクセスを必要とするアプリケーションでは、バイナリ拡張モジュールがしばしば最善の方法 。CPython ランタイム自身への低レベルアクセスについては特にそうだ。なぜなら、ある種の操作 (GILの解放など)はインタプリタがコードを実行中だと単に無効化されるからだ。これはたとえ ctypescffi のようなモジュールを使って関連 C API インターフェースにアクセスしても同様だ。

拡張モジュールが(CPython ランタイムではなく)基礎となるオペレーティングシステムやハードウェアを操作している場合は、単に普通の C ライブラリ(または C++ や Rust のような C 互換 ABI をエクスポートできるシステムプログラミング言語のライブラリ)を書き、それから上記のラッピング手法のどれかを使って Python モジュールとしてインポートできるインターフェースを作る方がよい場合もあるかもしれない。

バイナリ拡張の実装

mention the stable ABI (3.2+, link to the CPython C API docs)
mention the module lifecycle
mention the challenges of shared static state and subinterpreters
mention the implications of the GIL for extension modules
mention the memory allocation APIs in 3.4+

mention again that all this is one of the reasons why you probably
*don't* want to handcode your extension modules :)

バイナリ拡張のビルド

Windows 上でのビルド環境構築

バイナリ拡張をビルドする前に、適切なコンパイラが利用できることを確認する必要がある。Windows では、Visual C が公式 CPython インタプリタのビルドに使われており、互換性のあるバイナリ拡張のビルドにもこれを使うべきだ。

Python 2.7 は Visual Studio 2008 を使っており、Python 3.3 と 3.4 は Visual Studio 2010 を使っており、Python 3.5+ は Visual Studio 2015 を使っている。残念ながら、旧バージョンの Visual Studio はもはや Microsoft から容易に入手できなくなった。よって、Python 3.5 より前のバージョンについては、別の方法でコンパイラを入手しなければならない(必要なバージョンの Visual Studio を既に持っているのでなければ)。

バイナリ拡張のビルド環境構築手順は以下の通り:

Python 2.7 の場合

  1. “Visual C++ Compiler Package for Python 2.7” をインストールする。これは Microsoft のウェブサイト から入手できる。
  2. setup.py で(最新バージョンの) setuptools を使うようにする (いずれにしろ、pip がこれを行ってくれる)。
  3. 完了。

Python 3.4 の場合

  1. “Windows SDK for Windows 7 and .NET Framework 4” (v7.1) をインストールする。これは Microsoft のウェブサイト から入手できる。
  2. SDK のコマンドプロンプトを使うようにする(環境変数が設定され、SDK が PATH に登録される)。
  3. 環境変数 DISTUTILS_USE_SDK=1 を設定する。
  4. 完了。

Python 3.5 の場合

  1. Visual Studio 2015 Community Edition (または、リリースされていればより新しいバージョン)をインストールする。
  2. 完了。

Python 3.5 以降では、Visual Studio は後方互換性のある方法で動作するため、今後の任意のバージョンの Visual Studio で Python 3.5 以降の全バージョンの Python 拡張をビルドできる。

FIXME

cover Windows binary compatibility requirements
cover Mac OS X binary compatibility requirements
cover the vagaries of Linux distros and other *nix systems

バイナリ拡張の公開

FIXME

cover publishing as wheel files on PyPI or a custom index server
cover creation of Windows and Mac OS X installers
mention the fact that Linux distros have a requirement to build from
source in their own build systems anyway, so pre-built binaries for
*nix systems currently aren't common