バイナリ拡張をパッケージングする#

ページステイタス:

未完了

最終査読日:

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 で書かれていて、言語のレベルには提供されていないようなインタープリタの内部にアクセスできるようになっています。 : syssysmodule.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 と同じく一貫性を欠いたインタフェースの問題を孕みます。

低レベルのシステムアクセスを行う代替策#

(理由を問わず) 低レベルのシステムにアクセスする必要のあるアプリケーションにとって、バイナリ拡張モジュールはしばしば 進むべき最良の道 です。これは、 ctypescffi といったモジュールが適切な C 言語の API インタフェースへのアクセスを取得するために使われている場合であってさえも、 (グローバルインタプリタロックの解除のような) いくつかの操作がインタプリタがコードを実行している時には無効であるということだけを見ても、CPython ランタイム自身の低レベル部分にアクセスする際によく当てはまると言えます。

拡張モジュールが (CPythonのランタイムではなく) 下層のオペレーティングシステムやハードウェアを操作しようとする場合には、単純に通常の C 言語 (または C++ や Rust のような別のシステム言語で C 言語と互換性のある ABI をエクスポートできるもの) でライブラリを書いて、それを上述のようなインポート可能な Python のモジュールとしてインタフェースを構築できるラッピングテクニックを使う方が良いという場合もあるでしょう。

バイナリ拡張を実装する#

CPython の 拡張と埋め込み のガイドには、 C 言語による独自の拡張モジュール を書くための導入的な説明があります。

FIXME: これが拡張モジュールを支援ツールなしで書きたいと 思わない であろうという理由のひとつであることを詳しく説明すること :)

拡張モジュールのライフサイクル#

FIXME: この節には肉付けが必要です。

shared static state やサブインタプリタの影響#

FIXME: この節には肉付けが必要です。

GIL の影響#

FIXME: この節には肉付けが必要です。

メモリを割り当てる API 群#

FIXME: この節には肉付けが必要です。

ABI の互換性#

CPython の C 言語 API は、マイナーリリース (3.2, 3.3, 3.4 等) 間の ABI の互換性を保証しません。これが意味するところは、典型的には、Python のあるバージョン向けに拡張モジュールをビルドした時にマイナーバージョンまで同じ Python での動作が保証されるだけであって、他のマイナーバージョンについては保証されないということです。

Python 3.2 では、Python の C 言語 API のよく定義されたサブセットとして Limited API を導入しました。Limited API が必要とするシンボル群は、Python 3.x のすべてのバージョンを通じて互換性を保つことが保証された "Stable ABI" を形成しています。Stable ABI を使ってビルドされた拡張部分を含む Wheel は、Python 3.x のすべてのバージョンで互換性を保っていることを反映するために abi3 という ABI タグを使用します。

CPython の C 言語 API の安定性 のページには、 API/ABI の安定性保証、つまり、 Limited API をどのように使うのかや "Limited API" の正確な内容について詳しい情報があります。

バイナリ拡張をビルドする#

FIXME: Cover the build-backends available for building extensions.

複数のプラットフォーム向けに拡張モジュールをビルドする#

あなたが自分の書いた拡張モジュールを配布するつもりがあるなら、あたながサポートしようと思うすべてのプラットフォーム向けに wheels を準備するべきです。通常はこれらを継続的インテグレーション (CI) システム上でビルドします。cibuildwheelmultibuild のような CI から再配布が非常にやりやすいバイナリをビルドするのを補助するツールの存在が知られています。

ほとんどの拡張部分向けに、すべてのサポートするつもりのあるプラットフォーム用の wheel をビルドする必要があるでしょう。これが意味するところは、ビルドする必要のある wheel 群の数が次のような掛け算になるだろうということです:

count(Python minor versions) * count(OS) * count(architectures)

CPython の Stable ABI を使うことで、準備する必要のある wheel の数を大いに減らすことに役立つでしょう、というのは、あるプラットフォーム上の単一の wheel が Python のすべてのマイナーバージョンで使える; つまり、マトリクスの次元をひとつ削除することになるからです。さらに、新しいマイナーバージョンの Python が出現するたびに新たに wheel を生成する必要もなくなります。

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 を見てください。

バイナリ拡張を公開する#

PyPI を通じてバイナリ拡張を公開する時にも、純 Python のパッケージを公開するときと同じアップロードのメカニズムを使います。その拡張の wheel をビルドバックエンドを使ってビルドし、 twine を使って PyPI にアップロードするということです。

バイナリだけのリリースを避ける#

バイナリ拡張を公開する時にはそれをビルドする際に用いたソースコードも公開することが強く推奨されています。こうすることで、必要であればユーザがその拡張をソースコードからビルドすることができます。特筆すべきことに、 Linux ディストリビューションの中には、そのディストロ向けのパッケージリポジトリの独自のビルドシステム内でソースコードからビルドすることを要求するものがあるのです。

弱いリンキング <Weak linking>#

FIXME: この節には肉付けが必要です。

追加のリソース#

拡張モジュールのプラットフォームを跨ぐ開発・配布は込み入った話題なので、このガイドでは主として背景にある技術的な課題の取り扱いを自動化するさまざまなツールへのポインタを提供することに重点を置くことにします。代わりに、この節のその他の部分では、そのようなシステムが実行時に依存するような下層のバイナリインタフェースについて開発者が理解を深めるために見ることを意図しています。

sckit-build を用いたクロスプラットフォームな wheel の生成#

scikit-build は、抽象的なビルド作業を補助し、バイナリ拡張のパッケージを作成する時に必要な追加的な能力を提供します。 Python のバイナリ拡張モジュールに関するさらなる説明文書は、 C 言語におけるランタイム・コンパイラ・ビルドシステム生成器 にあります。

C/C++ による拡張モジュールの紹介#

Debian システム上で CPython がどのようにして拡張モジュールを使うのかについて、もっと深掘りした説明が次の記事に出ています: