原文はこちら。
The original article was written by Štěpán Šindelář (Technical lead of the R (“FastR”) runtime of GraalVM developed by Oracle Labs).
https://medium.com/graalvm/hpy-better-python-c-api-in-practice-79328246e2f8
HPyは、GraalPythonとPyPyの開発者が共同で開発したPython拡張用の標準CPython C APIに代わるAPIです。HPyの設計目標は、よりシンプルで将来性のあるAPIを提供することにあり、これは標準のC APIとは異なり、Pythonの実装の詳細を抽象化して隠すことで、CPythonの改良を妨げず、参照カウントではなく移動ガベージコレクタなど、異なる実装戦略を使う可能性のある他の代替Pythonともうまく機能するようにします。
HPy – A better C API for Python
https://hpyproject.org
Pythonのユーザにとっては、HPyはCPythonの安定版APIのように、異なるCPythonのバージョン間だけでなく、異なるPythonの実装間でもPython拡張のバイナリ互換性をもたらします。HPyベースの拡張機能の1つのバイナリリリースは、CPython、GraalPython、PyPyのいずれでも動作します。拡張機能をソースから再コンパイルする必要はありませんし、数多くのバイナリリリースを配布する必要もありません。
oracle/graalpython: A Python 3 implementation built on GraalVM
https://github.com/oracle/graalpython
PyPy
https://pypy.org
Introduction
HPyの”H”は”handle”を意味しています。PyObject*
のような内部データ構造へのポインタの代わりに不透明なハンドルを使用することが、HPyの核となる概念です。
しかし、HPyが標準C APIの基本的なハイレベルのkコンセプトを完全にひっくり返してしまうとは思わないでください。HPyでも、拡張メソッド、型、スロットを定義し、例えばCのlongをPythonのintオブジェクトに変換するAPI関数を呼び出すことは可能です。次のコード・スニペットは、同じ機能の2つの実装を示したものです。一方はC APIを使用し、もう一方はHPy APIを使用していますが、見た目は非常によく似ています。
#include "Python.h" | |
static PyObject *myabs(PyObject *self, PyObject *arg) { | |
return Py_Absolute(arg); | |
} | |
static PyMethodDef methods[] = { | |
{"myabs", myabs, METH_O, ""}, | |
{NULL, NULL, 0, NULL} | |
}; | |
static struct PyModuleDef module = { | |
.m_methods = methods | |
// ... | |
}; | |
// ------------------------------------------- | |
// The same with HPy: | |
#include "hpy.h" | |
HPyDef_METH(myabs, "myabs", myabs_impl, HPyFunc_O) | |
static HPy myabs_impl(HPyContext *ctx, HPy self, HPy arg) { | |
return HPy_Absolute(ctx, arg); | |
} | |
static HPyDef *methods[] = { &myabs, NULL }; | |
static HPyModuleDef module = { | |
.defines = methods, | |
// ... | |
}; |
では、なぜ既存のPython拡張をHPyに移植したり、新しいPython拡張にCPython C APIではなくHPyを選択する必要があるのでしょうか?HPyにはいくつかの特徴的な機能と目標があり、それは実装の詳細を抽象化して隠す、よりシンプルなAPIを提供するという主な設計目標から自然に派生しています。
バイナリ互換性 | HPy をベースにした拡張機能のバイナリディストリビューションは、将来のバージョンも含めて、どのCPythonバージョンでも動作するようにします。HPyがCPythonの安定版ABIと異なるのは、同じバイナリが、HPyをネイティブにサポートする代替Python実装でも動作することです。現時点では、GraalPythonとPyPyがこれにあたります。 |
代替のPython実装で優れたパフォーマンスを発揮 | CPythonのC APIを使用する場合、GraalPythonとPyPyはCPythonの実装の詳細をエミュレートしなければならず、かなりのコストがかかります。HPyの拡張機能は、代替のPython上で著しく優れたパフォーマンスを発揮します。 |
素のCPython APIのパフォーマンス | HPy の普遍性は、CPython のパフォーマンスのオーバーヘッドをほんの少し増加させるかもしれません。このオーバーヘッドを取り除くために、HPy拡張機能を “CPython ABI “モードでコンパイルできます。この場合、ビルド時にHPyのAPIコールはすべてCPythonのAPIコールにワイアリングし直されます。その結果、CPython APIの欠点をすべて備えた、HPy に依存しない標準的な CPython 拡張ができあがります。 |
デバッグコンテキスト | 拡張モジュールのロード時に、HPy 拡張モジュールをいわゆるデバッグコンテキストでのロードを選択できます。この場合、拡張機能の再コンパイルは必要ないことに注意してください。デバッグコンテキストはすべての API 呼び出しを傍受し、一般的なエラー、特にハンドルリーク (CPython の参照カウントエラーに似ています) を探します。デバッグコンテキストでテストを実行すると、テストカバレッジが良ければ、HPy 仕様を正しく実装している Python で拡張機能がうまく動作し、拡張機能が意図せずに未指定の動作に依存していないことを確認できるはずです。 |
近い将来、ほとんどすべてのPython拡張がHPyをベースにしているという明るい未来が来ることを期待していますが、残念ながら現状はそうではありません。したがって、HPyのもう一つの重要な設計目標は、C APIからHPyへのスムーズな移行経路を確保することです。この点で、Python拡張機能の開発者に役立つのは何でしょうか?
可能な限りC APIと類似している点 | 前述のように、HPy APIの基本概念は既存のC APIと類似しており、拡張関数や型などを定義できます。 |
C APIとHPyの組み合わせが可能な点 | C APIとHPy APIは、1つのPython拡張機能の中で組み合わせることができます。HPyでは、HPy とPyObject* の変換を可能にする関数を提供しており、HPyのコードとC APIコードを混在させることが可能です。HPy モジュールは、”レガシー” C API ベースのビルトインと HPy ビルトインの両方を公開でき、1関数ごとに反復的に拡張機能を移行できます。これは、公開されている型や型スロットについても同様です。 |
HPy APIはC APIと何が違うのでしょうか?Python拡張機能の開発者視点から見た基本的な違いをいくつか挙げます。
- HPyでは、すべてのAPI関数の第一引数に
HPyContext*
を渡す必要があります。 - HPy は内部構造を一切公開しません。例えば、
HPy
のCという型は単なる不透明な構造体であり、実際の内容が何であるかはPythonの実装に任されています。ユーザーコードはそれについていかなる仮定も立てることはできません。 - HPyでは、APIで参照カウントを公開しません。その代わり、
HPy
ハンドルは1つ1つ明示的に閉じなければなりません。C APIから知られている参照の「借用」「盗用」はありません。
その他の違いやHPy APIに関する詳細は、HPyのドキュメントに記載されています。
HPy | Read the Docs
https://readthedocs.org/projects/hpy/
Kiwisolver and Matplotlib Case Studies
HPyは、既存のPython拡張機能を新しいAPIに移植するのに必要な労力のバランスをとりながら、十分に抽象的で将来性のあるPython拡張機能を書くためのAPIとして機能すると考えています。HPyは、基礎となるPythonの実装の詳細を効果的に隠すので、Pythonネイティブ拡張を実行しながら、代替Pythonがその能力を最大限に発揮できます。さらに、HPyは、CPython自身が実験し、標準のCPython APIでは不可能な最適化を取り入れることができるかもしれません。すでに検討した例としては、unboxedな整数 (つまりPrimitive型) をハンドルで渡すことで、CPython上での数学演算を高速化することができます。
具体的には、この後のブログで、Kiwisolver[1]とMatplotlibのPythonパッケージのHPyへ実験的に移植した例で、HPyの機能のいくつかを紹介する予定です。
hpyproject/kiwi-hpy: Efficient C++ implementation of the Cassowary constraint solving algorithm
https://github.com/hpyproject/kiwi-hpy/tree/HPy-1.3.2
hpyproject/matplotlib-hpy at HPy-V3.5.x
https://github.com/hpyproject/matplotlib-hpy/tree/HPy-V3.5.x
インクリメンタルな移植プロセスとHPyのパフォーマンス | Porting Matplotlib from C API to HPy https://medium.com/graalvm/porting-matplotlib-from-c-api-to-hpy-aa32faa1f0b5 https://logico-jp.io/2022/10/02/porting-matplotlib-from-c-api-to-hpy/ |
KiwisolverとのHPyのバイナリ互換性 | HPy: binary compatibility and API evolution with Kiwisolver https://medium.com/graalvm/hpy-binary-compatibility-and-api-evolution-with-kiwisolver-7f7a811ef7f9 https://logico-jp.io/2023/03/11/hpy-binary-compatibility-and-api-evolution-with-kiwisolver/ |
Kiwisolverを使ってデバッグ機能を試す | HPy: Debugging Features with Kiwisolver https://medium.com/graalvm/hpy-debugging-features-with-kiwisolver-1467d691663d https://logico-jp.io/2023/03/14/hpy-debugging-features-with-kiwisolver/ |
GraalPythonとHPyに関する他のブログ記事もお楽しみに。
[1] Kiwisolverは、Cassowary制約解消アルゴリズムの効率的なC++実装のPythonバインディングを提供します。KiwisolverはMatplotlibパッケージの依存関係にあたります。