フリースレッド向け Stable ABI (abi3t) への移行

CPython 3.15 から、フリースレッド の Python に対応する Stable ABI がサポートされます。 これはフリースレッドビルド向け Stable ABI、略して abi3t と呼ばれます。 このドキュメントでは、C API を使う拡張をフリースレッドに対応させる方法を説明します。

移行する理由

Stable ABI を使う主な理由は、ライブラリの各バージョンについて、ビルドして配布する成果物の数を減らすことです。

Stable ABI を使わない場合、サポートする CPython の機能リリースごとに別の共有ライブラリと、通常は wheel 配布物をビルドする必要があります。 例えば、次の表の各タグは、それぞれ別のライブラリや wheel を表します。

CPython のバージョン

非フリースレッド

フリースレッド

3.12

cpython-312

---

3.13

cpython-313

cpython-313t

3.14

cpython-314

cpython-314t

3.15

cpython-315

cpython-315t

3.16

cpython-316

cpython-316t

以降のバージョン

cpython-3XX

cpython-3XXt

これではビルドの数が多くなります。 サポートするプラットフォームの数も掛け合わせると、なおさらです。

CPython 3.2 で導入された Stable ABI (abi3) を使うと、プラットフォームごとに 1 つの拡張で、CPython の 非フリースレッド ビルドすべてに対応できます。

CPython のバージョン

非フリースレッド

フリースレッド

3.12

abi3

---

3.13

cpython-313t

3.14

cpython-314t

3.15

cpython-315t

3.16

cpython-316t

以降のバージョン

cpython-3XXt

CPython 3.15 で導入されたフリースレッドビルド向け Stable ABI (abi3t) は、フリースレッドビルドで同じ役割を果たします。 非フリースレッドビルドとも互換性があります。

CPython のバージョン

非フリースレッド

フリースレッド

3.12

abi3 *

---

3.13

cpython-313t

3.14

cpython-314t

3.15

abi3t

3.16

以降のバージョン

* (前述のとおり、abi3 拡張はすべての非フリースレッドビルドと互換性があります。 この表で abi3t の対応範囲として示した 3.15 以降も含まれます。)

移行 しない 理由

Stable ABI には主に 2 つの欠点があります。

第 1 に、Stable ABI は性能より互換性を優先するため、拡張が遅くなる可能性があります。 通常は気付かない程度の違いであり、同じソースから Stable ABI 向けのビルドに加えて、主要な CPython バージョン向けの専用ビルドもいくつか作ることで、影響を軽減できる場合が多くあります。

第 2 に、C API のすべてを利用できるわけではありません。 Stable ABI 向けにビルドするには拡張を移植する必要があり、これが難しい場合や、まれに不可能な場合もあります。

特に abi3t には、CPython 3.15 で追加された API が必要です。 同じソースから古い CPython 向けにも拡張をビルドしたい場合は、主に 2 つの選択肢があります。

  • プリプロセッサの条件分岐を使う。

    このガイドに従って変更する際に、サポートしたい CPython バージョンでビルドできなくなる変更には、#ifdef Py_TARGET_ABI3T ブロックを使ってください。 従来のコードは #else ブロックに残します。

    PEP 697 で導入された機能により、手書きの C 拡張では CPython 3.12 まで対応を維持するのが現実的です。 Cython などのコード生成ツールでは、3.11 以前との互換性を維持する価値もあるでしょう。

  • 古いバージョンのサポートを終了できるまで、abi3t へ移植せず、CPython のバージョンごとに別の拡張をビルドし続ける。

    これも妥当な方法です。 すべての拡張が今すぐ abi3t に移行する必要はありません。

前提条件

このガイドでは、C または C++ で直接書いた拡張を abi3t に移植することを想定しています。

拡張が Cython のようなコード生成ツールや PyO3 のような言語バインディングを使っている場合は、そのツールが abi3t に対応するまで待つのがよいでしょう。 そのようなツールを保守している場合は、このガイドの手順をツールに合わせて応用できるかもしれません。

非フリースレッドの Stable ABI

拡張は非フリースレッドの Stable ABI (abi3) に対応している必要があります。 未対応の場合は先に対応させるか、このガイドに記載されていない問題も解決するつもりで読み進めてください。

フリースレッドへの対応

厳密には必須条件ではありませんが、通常は abi3t への移植前に拡張をフリースレッドに対応させておくとよいでしょう。 手順は C API 拡張のフリースレッド対応 を参照してください。

参考

Porting Extension Modules to Support Free-Threading: 拡張の開発者向けにコミュニティが保守している移植ガイド。

拡張モジュールの分離

モジュールは 複数フェーズ初期化 を使い、分離されているか、プロセスあたりの読み込みを最大 1 回に制限する必要があります。 そうでなければ、先に 拡張モジュールを分離する に従ってください。 (簡単な方法については 分離を避ける節 を参照してください。)

可変長の型を避ける

拡張が Py_tp_itemsize や PyTypeObject.tp_itemsize を使って可変長の型を定義している場合、abi3t 3.15 には移植できません。

ビルドの設定

ビルドツールを使っている場合は、そのドキュメントで「abi3t」を検索し、ABI を選択する手順に従ってください。 参考として、よく使われるビルドツールへのリンクを示します。

ツールが正しいフラグを設定したか確認するには、#include <Python.h> の直後に、一時的に次のコードを追加するとよいでしょう。

#if Py_TARGET_ABI3T+0 <= 0x30f0000
#error "abi3t define is not set!"
#endif

This should result in a different error than "abi3t define is not set".

参考

Building and distributing abi3t extensions: コミュニティが保守している Python Free-Threading Guide にある、ビルド設定と wheel のテストの例。

注釈

ビルドツールがまだ abi3t に対応していない場合は、Python.h をインクルードする前に次のマクロを定義してください。

#define Py_TARGET_ABI3T 0x30f0000

または、例えば次のようにコンパイラのフラグで指定します。

-DPy_TARGET_ABI3T=0x30f0000

この設定で拡張をビルドできれば、CPython 3.15 以降と互換性のある拡張になります。

このマクロを手動で設定した場合は、生成される拡張の名前とタグも後で手動で設定する必要があります。 これについては、後の タグ付けと配布 で説明します。

このガイドでは一連の変更を行います。 それぞれの変更後に、もとの (abi3t ではない) 設定でも拡張をビルドできることを確認してください。 できれば、サポートするすべての Python バージョンでテストを実行してください。 これにより、移植中に既存の動作を壊していないことを確認できます。

モジュールエクスポートフック

この手順をまだ行っていなければ、拡張モジュールには PyInit_<modname> という モジュール初期化関数 が定義されているはずです (modname はモジュール名です)。 これを モジュールエクスポートフック PyModExport_<modname> に移行する必要があります。 この機能は PEP 793 により CPython 3.15 で追加されました。

既存の初期化関数は、次のようになっているはずです (<modname> と <moddef> は自分のコードの名前になります)。

PyMODINIT_FUNC
PyInit_<modname>(void)
{
    return PyModuleDef_Init(&<moddef>);
}

return の前に処理がある場合は、Py_mod_create または Py_mod_exec スロットの関数に移してください。 関連する情報は PyInit のドキュメント を参照してください。

この関数は PyModuleDef オブジェクト (上のコードの <moddef>) を参照しています。 その定義は次のようになっているはずですが、値は異なり、一部のフィールド名が指定されていなかったり、フィールドが省略されていたりするかもしれません。

static PyModuleDef <moddef> = {
    PyModuleDef_HEAD_INIT,
    .m_name = "my_module",
    .m_doc = "my docstring",
    .m_size = sizeof(my_state_struct),
    .m_methods = my_methods,
    .m_slots = my_slots,
    .m_traverse = my_traverse,
    .m_clear = my_clear,
    .m_free = my_free,
};

この定義と PyInit 関数を削除し、次のコードに置き換えてください。 後方互換性を保つ場合は、削除する代わりに #ifndef Py_TARGET_ABI3T ブロックに入れます。

PyABIInfo_VAR(abi_info);

static PySlot my_slot_array[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "my_module"),
    PySlot_STATIC_DATA(Py_mod_doc, "my docstring"),
    PySlot_SIZE(Py_mod_state_size, sizeof(my_state_struct)),
    PySlot_STATIC_DATA(Py_mod_methods, my_methods),
    PySlot_STATIC_DATA(Py_mod_slots, my_slots),
    PySlot_FUNC(Py_mod_state_traverse, my_traverse),
    PySlot_FUNC(Py_mod_state_clear, my_clear),
    PySlot_FUNC(Py_mod_state_free, my_free),
    PySlot_END
};

PyMODEXPORT_FUNC
PyModExport_<modname>(void)
{
    return my_slot_array;
}

もともと存在しなかったフィールドは省き (新設の Py_mod_abi は例外です)、自分のコードの値に置き換えてください。

この API の詳細は PySlot と エクスポートフック のドキュメントを参照してください。

この例のように、PyModExport_ 関数では静的データへのポインタを返す だけ にしてください。 ほかの処理を避けられない場合は、PyModExport のドキュメントの注意事項 を参照してください。

注釈

Setuptools ビルドツールを使って Windows 向けにビルドする場合、PyInit_<modname> 関数を削除すると、リンカエラー LINK : error LNK2001: unresolved external symbol PyInit_<modname> が発生することがあります。 原因は Setuptools が /EXPORT リンカフラグを渡すことです。 このフラグは Python 3.15 から不要になっています (gh-141671 を参照)。 回避策として、ダミーの PyInit_<modname> 関数を追加できます。 Python 3.15 以降では PyModExport_<modname> が存在するとこの関数は呼ばれないため、常に失敗する実装で構いません。

// 次の問題の回避策: https://github.com/pypa/distutils/issues/387
PyMODINIT_FUNC
PyInit_<modname>(void)
{
    PyErr_SetString(PyExc_SystemError,
                    "PyInit_* called for module with PyModExport_*");
    return NULL;
}

(この問題は Setuptools 84.0.0 に存在します。 より新しいバージョンでは修正されている可能性があります。)

既存のスロット

Py_mod_slots スロットがある場合は、それが参照する配列を確認してください。 次のような PyModuleDef_Slot 配列になっているはずです。

static PyObject *create_module(PyObject *spec, PyModuleDef *def) { ... }
static int my_first_module_exec(PyObject *module) { ... }
static int my_second_module_exec(PyObject *module) { ... }

static PyModuleDef_Slot my_slots[] = {
   {Py_mod_gil, Py_MOD_GIL_NOT_USED},
   {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
   {Py_mod_create, my_module_create},
   {Py_mod_exec, my_first_module_exec},
   {Py_mod_exec, my_second_module_exec},
   {0, NULL}
};

py_mod_create

Py_mod_create エントリーがある場合は、その関数の第 2 引数に、削除する PyModuleDef の代わりに NULL を渡してもよいことを確認してください。 多くの場合、この引数は使われていません。 名前を変更して確かめられます。

static PyObject *create_module(PyObject *spec, PyModuleDef *_unused) { ... }

引数が使われている場合は、別の方法でデータを渡してください。 多くの場合、その情報は静的なので直接参照できます。 (複数のモジュールで同じ関数を使い回している場合は、別々の関数を定義することを検討してください。)

Multiple py_mod_exec

Py_mod_exec エントリーが 複数 ある場合は、1 つにまとめてください。 各関数を呼び出す新しい関数を作り、既存のスロットをそれに置き換えます。

static int my_module_exec(PyObject *module) {
   if (my_first_module_exec(module) < 0) return -1;
   if (my_second_module_exec(module) < 0) return -1;
}

static PyModuleDef_Slot my_slots[] = {
   ...
   /* (remove other Py_mod_exec slots) */
   ...
   {Py_mod_exec, my_module_exec},
   {0, NULL}
};

各関数がほかで使われていなければ、代わりにそれぞれの本体を 1 つにまとめることもできます。

スロット配列の統合

Python 3.14 との互換性を維持する必要がなくなったら、任意の整理として、スロットを PySlot 配列に移し、定義を PySlot_DATA と PySlot_FUNC に変えることができます。

static PySlot my_slot_array[] = {
    ...
    PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
    PySlot_DATA(Py_mod_multiple_interpreters,
         Py_MOD_PER_INTERPRETER_GIL_SUPPORTED)
    PySlot_FUNC(Py_mod_create, my_module_create),
    PySlot_FUNC(Py_mod_exec, my_module_exec),
    PySlot_END
};

この変更を行ったら、もとの PyModuleDef_Slot 配列と、それを指す Py_mod_slots エントリーを削除してください。

関連付けられた PyModuleDef

新しい API は PyModuleDef 構造体を使わないため、作成されたモジュールにモジュール定義は関連付けられません。 その結果、次の関数の動作が変わります。

これらの関数を使っているか、コードを確認してください。 使っていなければ、この節は読み飛ばせます。

これらの関数は、通常 2 つの目的で使われます。

  1. モジュールの作成に使われた定義を取得する。 新しい API ではこれはできなくなります。 モジュールは定義への参照を保持しなくなるため、必要なデータを受け渡す別の方法を考える必要があります。

  1. あるモジュールオブジェクトが「自分のもの」かを確認する。 この用途には、モジュールを識別する不透明なポインタである モジュールトークン を使います。 トークンを使うには、一意な静的変数を宣言するか、既存のものを再利用します。 例えば、次のようにします。

    static char my_token;
    

    その変数へのポインタを、モジュールの PySlot 配列の新しいエントリーとして追加します。

    static PySlot my_slot_array[] = {
       ...
       PySlot_STATIC_DATA(Py_mod_token, &my_token),
       PySlot_END
    }
    

    そして、次のような PyModule_GetDef() の呼び出しを、

    PyModuleDef *def = PyModule_GetDef(module);
    

    PyModule_GetToken() に置き換えます。 この関数は出力用引数を使い、例外を送出して失敗することがあります。

    void *token;
    if (PyModule_GetToken(module, &token) < 0) {
        /* エラーを処理する */
    }
    

    また、次のような PyType_GetModuleByDef() の呼び出しを、

    PyObject *module = PyType_GetModuleByDef(type, my_def);
    /* エラーを処理し、module を使う */
    

    強参照を返す PyType_GetModuleByToken() に置き換えます。

    PyObject *module = PyType_GetModuleByToken(type, my_token);
    /* handle error; use module */
    Py_XDECREF(module);
    

PyObject の不透明化

abi3t では PyObject と PyVarObject の構造体は不透明です。

メンバーへのアクセスは禁止されています。 メンバーにアクセスしている場合は、それぞれのドキュメントに記載された取得・設定用の関数に置き換えてください。

また、コンパイラには PyObject 構造体の サイズ が分かりません。 サイズは CPython のビルドごとに変わる可能性があり、実際に変わります。

注釈

サイズは実行時に取得できますが (例えば Python コードで sys.getsizeof(object()) を使えます)、それをもとにポインタのオフセットを計算したくなっても避けてください。 オブジェクトのメモリレイアウトは、将来の abi3t の実装で変わる可能性があります。

独自の型の定義

PyObject が不透明になるため、独自の型を定義する従来の方法は使えなくなります。

typedef struct {
   PyObject_HEAD  // サイズが不明な `PyObject ob_base;` に展開される

   int my_data;
} CustomObject;

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = sizeof(CustomObject),
   ...
};

おそらく、すべてのクラス定義 と、そのクラスのデータにアクセスするすべてのコードを書き直す必要があります。 これが abi3t 対応に必要な最大の変更になるでしょう。

こうした型ごとに、インスタンス全体の struct を定義する代わりに、スーパークラスのフィールドを含めず、そのクラス固有の「追加」フィールドだけを持つ構造体を定義してください。

typedef struct {
   int my_data;
} CustomObjectData;

名前も変更してください。 この構造体を使うコードのほぼすべてに変更が必要です (特に、新しい構造体のポインタと PyObject* は相互にキャストできません)。 名前を変えれば、使用箇所をコンパイルエラーで見つけられます。 ただし、typeof や C++ の auto など、型名を直接書かない方法を使っている箇所では、この方法は使えません。 特に注意し、未定義の動作を検出するツールの利用を検討してください。

次に、クラスの作成時には basicsize を 負の値 にして、インスタンス 全体 のサイズではなく「追加」の格納領域のサイズを示します。

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = -sizeof(CustomObjectData), /* マイナス符号に注意 */
   ...
};

Py_tp_members を使っている場合は、各メンバーに Py_RELATIVE_OFFSET フラグを設定し、新しい構造体を基準とした offset を指定してください。

独自の型のデータへのアクセス

ここからが難しい部分です。 この構造体にアクセスするすべてのコードに PyObject_GetTypeData() の呼び出しを追加し、PyObject * から CustomObjectData * ポインタを取得する必要があります。

PyObject *obj = ...;
CustomObjectData *data = PyObject_GetTypeData(obj, cls);

この呼び出しには、自分のクラスの 型オブジェクト (cls) が必要です。

そのクラスがサブクラス化できない場合 (つまり Py_TPFLAGS_BASETYPE フラグを使わない場合)、cls は Py_TYPE(obj) です。 それ以外の場合、PyObject_GetTypeData() に Py_TYPE を 使わないでください。 無関係なサブクラス用のメモリが返されるかもしれません。 例えば、利用者が次のようなサブクラスを作ったとします。

class Sub(YourCustomClass):
   __slots__ = ('a', 'b')

このとき Py_TYPE(obj) は Sub であり、メモリの配置は次のようになっているかもしれません。

╭─ PyObject *obj
│              ╭─ 取得したいポインタ
│              │                    ╭─ PyObject_GetTypeData(obj, Py_TYPE(obj))
▼              ▼                    ▼
┌──────────┬───┬────────────────┬───┬─────────────┬───┬─────────────┐
│ PyObject │...│ CustomTypeData │...│ PyObject *a │...│ PyObject *b │
└──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘

(省略記号はパディングが入る可能性を示します。 このメモリレイアウトは保証されていません。 将来の Python ではパディングが変わったり、構造体の順序が変わったりする可能性もあります。)

正しいクラスを取得するには、主に 2 つの方法があります。

  • インスタンスメソッドでは、PyCMethod シグネチャと PyMethodDef.ml_flags の METH_METHOD ビットを使い、defining_class 引数でクラスを受け取れます。

  • それ以外の場合は、Py_tp_token スロットを使ってクラスに一意な静的トークンを設定し、次のようにします。

    PyTypeObject cls;
    if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, &cls) < 0) {
        /* handle error */
    }
    CustomObjectData *data = PyObject_GetTypeData(obj, cls);
    

    型のトークンは、このガイドの前半 で説明したモジュールトークンと同じように働きます。

ビルド時の条件分岐を避ける

拡張の ビルド に使った Python のバージョンを特定する API を使っていないか、コードを確認してください。 そのバージョンは拡張を実行する Python のバージョンとは一致しなくなるため、この情報を使うコードには変更が必要なことが多くあります。 確認するマクロは次のとおりです。

  • PY_VERSION_HEX, PY_MAJOR_VERSION, PY_MINOR_VERSION:

    • 実行時のバージョンを取得するには、Py_Version を使います。

    • 利用可能な C API を判断するには、Py_TARGET_ABI3T を使います。 このマクロには、サポートする最も古いバージョンが設定されています。

  • Py_GIL_DISABLED: abi3t では、このマクロは常に定義されています。 フリースレッドの Python で動くコードは、GIL が有効でも動作する はず です (GIL は実行時に有効化できるためです)。 通常は実際に 動作します が、何らかの理由で複数の アタッチされたスレッド状態 を同時に必要とする場合は例外です。

その他のコードの変更

コンパイルエラーや警告がまだ残っている場合は、解決方法を探してください。 残念ながら、このガイドには限界があり、拡張に必要になり得るすべてのコード変更を網羅することはできません。

ほかの拡張の開発者も遭遇しそうな問題が見つかった場合は、このガイドについて issue を報告する か、プルリクエストを送ることを検討してください。

現在の abi3t では解決できない問題もあり得ます。 その場合でも、報告することで CPython の次のバージョンで優先的に対応されるかもしれません。

タグ付けと配布

abi3t に対応したビルドツールを使っていれば、拡張の準備はできています。 ただし、正しくビルドされたか確認するとよいでしょう。

abi3t 向けにビルドした拡張の拡張子は、次のようになります。

  • Windows: ほかの拡張と同じく .pyd。

  • Linux、macOS、その他 .so サフィックスを使うシステム: .abi3t.so (.cpython-315t.so や .abi3.so ではありません)。 フリースレッドビルドでも非フリースレッドビルドでも、.abi3t.so 拡張を読み込みます。

  • その他のシステム: 配布元に確認し、必要に応じてこのガイドを更新してください。

拡張を wheel として配布する場合は、次のタグを使います。

  • Python タグ: cp3XX。 XX は、拡張が対応するようにビルドされた最も古い Python バージョンです。 例えば、Py_TARGET_ABI3T を 0x30f0000 に設定した場合は cp315 です。 ほかの値は Stable ABI 向けのコンパイル を参照してください。

  • ABI タグ: abi3.abi3t。 これは非フリースレッドビルドとフリースレッドビルドの両方に対応することを示す 圧縮タグ集合 です。

例えば、wheel のファイル名は次のようになります。

myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl

参考

PyPA のパッケージ配布メタデータにある Platform Compatibility Tags。

ファイル名やタグが正しくなければ修正してください。

テスト

複数の CPython バージョンと互換性のある拡張をビルドしたら、サポートする各バージョン (例えば 3.15、3.16 など) で必ず テスト してください。 Stable ABI が保証するのは ABI の互換性だけです。 動作には、PEP 387 の対象となる意図的な変更も、バグによる変更もあり得ます。

CPython のフリースレッドビルドと非フリースレッドビルドの両方で、必ずテストを実行してください。

テストが通れば、おめでとうございます! abi3t 拡張の完成です。