バイナリ拡張をパッケージングする¶
- ページステイタス
未完了
- 最終査読日
2013-12-08
CPython の参照インタープリタの機能の一つは、Python のコードを実行することに加えて、他のソフトウェアによる利用のために豊富な C の API を露出することです。この C の API の最もよくある利用方法は、純粋な Python のコードでは必ずしも簡単には達成できないようなことをインポート可能な C 拡張として作成することです。
内容
バイナリ拡張の概要¶
ユースケース¶
バイナリ拡張の典型的なユースケースは、たった3個の伝統的なカテゴリに分類されます:
アクセラレータモジュール: これらのモジュールは完全に自己完結型で、CPython で同機能の純粋なPython コードを走らせるよりも速く動作するためにだけ作成されます。アクセラレートされたバージョンが当該システム上で動作しなかった時のために、アクセラレータモジュールには同機能の純粋なPythonのコードを予備品として同梱していることが理想的です。CPython の標準ライブラリはアクセラレータモジュールを多用しています。例:
datetime
をインポートする際、C での実装 (_datetimemodule.c) が使えない時には、datetime.py にフォールバックします。ラッパモジュール: これらのモジュールは、既存の C のインタフェースを Python に見せるために作られます。それらは C のインタフェースを直接に見せるか、または、Python 言語の機能を使ってAPI をより使いやすくするようなもっと "Pythonic" なやり方で見せるかします。CPython の標準ライブラリでは、ラッパモジュールを多用しています。 例: functools.py は、 _functoolsmodule.c のためのラッパモジュールです。
低レベルシステムアクセス: これらのモジュールは、CPython ランタイムやオペレーティングシステム、あるいはさらに下層のハードウェアの低レベルの機能にアクセスするために作成されています。プラットフォームに固有のコードを通して、純粋な Python コードでは不可能なことを拡張モジュールが達成できるかもしれません。結構な数の CPython の標準ライブラリモジュールが C で書かれていて、言語のレベルには提供されていないようなインタープリタの内部にアクセスできるようになっています。 例:
sys
は sysmodule.c から来ています。C 言語拡張の特筆すべき機能は、インタプリタランタイムにコールバックする必要のない時には、 (CPU制約であろうとIO制約であろうと関わりなく) 長時間走り続ける操作を包む CPython グローバルインタプリタロックを解放することができるということです。
必ずしも全ての拡張モジュールが上記のカテゴリにぴたりと当て嵌まる訳ではありません。例えば NumPy に含まれる拡張モジュールは3個のカテゴリのすべてにまたがっています - 動作速度上の理由から内側のループを C 言語に移しているし、C 言語や FORTRAN 言語その他で書かれた外部のライブラリをラップしているし、また、ベクタ演算の並列実行や生成したオブジェクトのメモリレイアウトを正確な管理のために、 CPython とその下にあるオペレーティングシステムの両方の低レベルなシステムインターフェイスを利用しています。
欠点¶
バイナリ拡張を使うことの主たる欠点は、引き続くソフトウェア配布がより難しくなることです。Python を使う利点のひとつは非常に広範囲のクロスプラットフォームである (訳注、同一の Python コードが広範囲の異種プラットフォームで動作する) ことですが、拡張モジュールを書くのに使われる言語 (典型的には C 言語や C++ だが、実際にはどんな言語でも CPython の C 言語 API にバインドできる) は異なるプラットフォームには異なるバイナリを作成しなければならないのが普通なのです。
これが意味するところは、バイナリ拡張は:
エンドユーザがソースコードからビルドできるか、または、誰かが共通のプラットフォーム向けにビルド済みのバイナリを公開するか、のいずれかでなければなりません
CPython 参照インタプリタのビルドが異なると互換性がないかもしれません
PyPy ・ IronPython ・ Jython のような代替インタプリタ上では動作しないことがしばしばです
もしハードコードされているなら、メンテナンス担当者が Python のみならずそのバイナリ拡張を作成するために使われている言語についてもCPython の C 言語 API についても慣れ親しんでいることが要求されるので、メンテナンスがより難しくなるでしょう。
純粋な Python で書かれた退避先実装が用意されているなら、変更を実装するべき場所が2箇所になるのと、両方の実装が常に実行されることを保証するテストスイートを準備するために複雑さが増加することになるので、メンテナンスがより一層困難になります。
バイナリ拡張に依存することによるさらなる欠点は、 (例えば zip ファイルから直接にインポートする能力など) 通常とは異なるインポート機構が、しばしば拡張モジュールでは動作しない (ほとんどのプラットフォームでは動的ロード機構はディスクからライブラリを読み込むことしかできないため) ということです。
ハードコードされたアクセラレータモジュールの代わりとなるもの¶
拡張モジュールが単純にプログラムを速く走らせるためだけに使われてい (て、プロファイリングの結果、メンテナンス工数が増えることを甘受しても速度を上げることに価値があると判断され) る時には、他の複数の選択肢も検討しておくべきです:
既存の最適化された代替策を探す。CPython の標準ライブラリには、最適化されたデータ構造やアルゴリズム (特に組み込み済みのものや
collections
およびitertools
モジュール) が多数含まれています。Python パッケージインデックスにも他の代替となるパッケージがあります。時には、標準ライブラリや第三パーティのモジュールから適切なものを選択することで、自分自身でアクセラレータパッケージを作成する必要に迫られないで済むでしょう。長時間に渡って走り続けるアプリケーションに対しては、標準の CPython ランタイムの代わりに JIT コンパイルされた PyPy インタープリタ を使うことが適切な代替策となるかもしれません。 PyPy を適用しようとする時、典型的には他のバイナリ拡張モジュールへの依存が主たる障害になります - PyPy が CPython の C 言語 API をエミュレートする一方で、その API に依存するモジュールの側が PyPy の JIT に問題を引き起こし、また、エミュレーション層が CPython では現在は許容されているような拡張モジュール側の潜在的な瑕疵 (参照カウントに関係するエラーであることが多い - あるオブジェクトへの参照が2個あるはずのところが1個しかないという状態は何も悪さをしませんが、1個の参照のはずが参照なしとなる場合が主な問題になります) をしばしば暴露してしまうのです。
Cython は、ほとんどの Python コードを C 言語の拡張モジュールに変換できる成熟した静的コンパイラです。当初のコンパイルでは、(CPython のインタプリタ層をバイパスすることによって) いくらかの速度向上が見込めるとともに、 Cython のオプション扱いの静的型付け機能によってさらに速度向上の機会があるかもしれません。Cython の使用には、バイナリ拡張の使用に関連した 欠点 がまだ残っていますが、Python プログラマにとって (C 言語や C++ のような他言語に比べると) 参入障壁が低いという利点があります。
Numba は新しめのツールで、LLVM を活用して Python アプリケーションの一部分をランタイムに選択的にネイティブの機械コードにコンパイルできるようにすることを目指している科学分野の Python コミュニティによって作成されました。この取り組みでは、コードが動作するシステムに LLVM が存在していなければなりませんが、特にベクトル化が容易な操作について顕著な速度増加を提供することができます。
ハードコードされたラッパモジュールに対する代替策¶
C 言語 ABI (アプリケーションバイナリインタフェイス <Application Binary Interface>)は、複数のアプリケーションの間で機能を共有する上での一般的な標準です。CPython の C 言語 API (アプリケーションプログラミングインタフェイス <Application Programming Interface>) を使えば Python のユーザがその機能に入り込むことができます。しかしながら、手動でモジュールごとにラッピングしていくのはうんざりするような作業なので、いくつもの代替アプローチが検討されるべきです。
以下に記述するアプローチは、単に配布物での事例ということは全くなく、むしろラッパモジュールを最新に保つというメンテナンス上の重荷を大いに軽減することができるものです。
アクセラレータモジュールの作成に役に立つことに加えて、 Cython は C 言語や C++ の API で書かれたものをラップするモジュールを作成することにも役立ちます。インターフェイスを手動でラップする作業が必要であり、それはラッパーのソースコードを設計し最適化する上では大きな自由度を与えはしますが、大規模な API を素早くラップするためにはあまり良い選択ではないかもしれません。Cython を用いた自動ラッピングについては サードパーティが提供するツールのリスト を見てください。これらは、 PyPy や Pyston のような CPython に類似した C-API を提供する性能重視の Python 実装をもサポートしています。
pybind11 は純粋な C++11 で書かれたライブラリで、綺麗な C++ インタフェイスを CPython (および PyPy) の C 言語 API に提供します。事前処理 (pre-processing) の段階が不要で、全体がテンプレート化された C++ で書かれています。ヘルパーツールが Setuptools や CMake ビルドに含まれています。 Boost.Python を基礎にしていますが、Boost ライブラリや BJam がなくてはダメということはありません。
cffi は、 Python と C 言語の両方を知っている開発者が C 言語で書いたモジュールを Python アプリケーションから単刀直入に使えるようにと何人かの PyPy 開発者たちが作成しました。たとえ 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
と同じく一貫性を欠いたインタフェースの問題を孕みます。
低レベルのシステムアクセスを行う代替策¶
(理由を問わず) 低レベルのシステムにアクセスする必要のあるアプリケーションにとって、バイナリ拡張モジュールはしばしば 進むべき最良の道 です。これは、 ctypes
や cffi
といったモジュールが適切な C 言語の API インタフェースへのアクセスを取得するために使われている場合であってさえも、 (グローバルインタプリタロックの解除のような) いくつかの操作がインタプリタがコードを実行している時には無効であるということだけを見ても、CPython ランタイム自身の低レベル部分にアクセスする際によく当てはまると言えます。
拡張モジュールが (CPythonのランタイムではなく) 下層のオペレーティングシステムやハードウェアを操作しようとする場合には、単純に通常の C 言語 (または C++ や Rust のような別のシステム言語で C 言語と互換性のある ABI をエクスポートできるもの) でライブラリを書いて、それを上述のようなインポート可能な Python のモジュールとしてインタフェースを構築できるラッピングテクニックを使う方が良いという場合もあるでしょう。
バイナリ拡張を実装する¶
CPython の 拡張と埋め込み のガイドには、 C 言語による独自の拡張モジュール を書くための導入的な説明があります。
FIXME (ここを修正してください)
安定な ABI (3.2+ のこと。 CPython C API 説明文書にリンクすること)
モジュールのライフサイクルに言及してください
shared static state やサブインタプリタの試みについて言及すること
拡張モジュール向けの GIL 実装について言及すること
3.4+ におけるメモリ割り当てについて言及すること
まさにこれが拡張モジュールを支援ツールなしで書きたいと 思わない であろうという理由のひとつであることに言及すること :)
バイナリ拡張をビルドする¶
複数のプラットフォーム向けに拡張モジュールをビルドする¶
あなたが自分の書いた拡張モジュールを配布するつもりがあるなら、あたながサポートしようと思うすべてのプラットフォーム向けに wheels を準備するべきでしょう。ほとんどの拡張モジュールにおいて、Python のバージョン毎にサポートする予定の OS とアーキテクチャの数を掛け算した分に少なくともひとつのパッケージが必要であるということになります。通常はこれらを継続的インテグレーション (CI) システム上でビルドします。cibuildwheel や multibuild のような CI から再配布が非常にやりやすいバイナリをビルドするのを補助するツールの存在が知られています。
Windows 向けのバイナリ拡張¶
バイナリ拡張をびるどできるようになる前に、適切なコンパイラが利用できるようになっていることを保証しなければなりません。Windows 上でCPython インタプリタをビルドするのに Visual C が使われていますが、互換性のあるバイナリ拡張をビルドする時にも同じコンパイラを使うべきです。バイナリ拡張のためのビルド環境を構築するためには、 Visual Studio コミュニティエディション をインストールしてください - 最近のバージョンならどれでも構いません。
注意: Visual Studio 2019 またはこれ以降のバージョンを使う場合には、あなたの拡張モジュールは、2015 までのそれ以前のバージョンで依存していた VCRUNTIME140.dll`
に加えて、"追加的な" ファイルである VCRUNTIME140_1.dll
にも依存するでしょう。この追加のファイルを同梱していないバージョンの CPython 上であなたの拡張モジュールを使う場合には、要求事項が追加されることになります。コンパイル時に引数 /d2FH4
を追加しておくことで、これを避けることができます。最近のバージョンの Python ならこのファイルを同梱しているかもしれません。
Visual Studio の古いバージョンがもはや Microsoft 社から入手できないので、3.5より古いバージョンの Python 向けにビルドすることは推奨されません。どうしても古いバージョン向けにビルドする必要がある場合には、 DISTUTILS_USE_SDK=1
および MSSdk=1
をセットすることで現在も使用可能なバージョンの MSVC が見つかるようにすることと、あなたの拡張モジュールの中でライブラリを跨いでメモリを malloc/free することのないように設計を見直すこと、また、修正されたデータ構造に依存しないようにすることなどが必要です。拡張モジュールを生成するツール群は、通常はこのようなことが起こらないようにしてくれています。
Linux 向けのバイナリ拡張¶
Linux のバイナリは、古い配布物と互換性を保つために十分に古い glibc を使わなければなりません。 manylinux の Docker イメージを使えば、よくあるアーキテクチャのほとんどの現行版 Linux 配布物をサポートするのに十分なほど古い glibc を伴ったビルド環境を準備できるでしょう。
macOS 向けのバイナリ拡張¶
macOS におけるバイナリ互換性は、 macOS のバイナリをビルドする際にしばしば MACOSX_DEPLOYMENT_TARGET
環境変数で指定される例えば 10.9 のようなターゲットの最小デプロイメントシステムで決まります。 setuptools や distutils でビルドする時には、デプロイメントターゲットは --plat-name
フラグを使って、例えば macosx-10.9-x86_64
のように指定されます。 macOS 上の Python 配布物向けのよくあるデプロイメントターゲットについては、 MacPython スピンニングホイール wiki を見てください。
バイナリ拡張を公開する¶
この話題に関する暫定的なガイダンスが この課題 にあります。
FIXME (ここを修正してください)
PyPI やその他のインデックスサーバに wheel ファイルを公開することについて書くこと
Windows 向けや macOS 向けのインストーラの作り方について書くこと
weak linking について書くこと
Linux のディストロ群ではそれぞれのビルドシステムでソースコードからビルドできることを要求するので、バイナリのみのリリースは強い非推薦の状態にあるという事実について書くこと
追加のリソース¶
拡張モジュールのプラットフォームを跨ぐ開発・配布は込み入った話題なので、このガイドでは主として背景にある技術的な課題の取り扱いを自動化するさまざまなツールへのポインタを提供することに重点を置くことにします。代わりに、この節のその他の部分では、そのようなシステムが実行時に依存するような下層のバイナリインタフェースについて開発者が理解を深めるために見ることを意図しています。
sckit-build を用いたクロスプラットフォームな wheel の生成¶
scikit-build は、抽象的なビルド作業を補助し、バイナリ拡張のパッケージを作成する時に必要な追加的な能力を提供します。 Python のバイナリ拡張モジュールに関するさらなる説明文書は、 C 言語におけるランタイム・コンパイラ・ビルドシステム生成器 にあります。
C/C++ による拡張モジュールの紹介¶
Debian システム上で CPython がどのようにして拡張モジュールを使うのかについて、もっと深掘りした説明が次の記事に出ています: