初めての C API 拡張モジュール
*****************************

このチュートリアルでは、C または C++ で簡単な Python 拡張モジュールを
作成する手順を説明します。

低レベルの Python C API を直接使います。 拡張モジュールをもっと簡単に
作成する方法は、推奨するサードパーティのツール を参照してください。

このチュートリアルは Python の基礎知識を前提とします。 C で関数を書き
始める前に、Python コードで関数を定義できるようになっている必要があり
ます。 Python 自体の入門には Python チュートリアル を参照してください
。

基本的な C ライブラリを書ける方なら、このチュートリアルに取り組めるは
ずです。 "static" 関数やリンケージ宣言など、C の初心者にはなじみのない
概念にも触れますが、これらを理解していなくても進められます。

Python の C API がどのようなものか「感覚」をつかむことに重点を置きます
。 エラー処理や参照カウントといった重要な概念は、後の章で説明します。

Unix 系のシステム (macOS や Linux を含む) または Windows を使うことを
想定しています。 それ以外のシステムでは、システムコマンド名など一部の
細かい点を調整する必要があるかもしれません。

適切な C コンパイラと Python の開発用ヘッダがインストールされている必
要があります。 Linux では、ヘッダは "python3-dev" や "python3-devel"
といったパッケージに含まれていることがよくあります。

Python パッケージをインストールできる必要があります。 このチュートリア
ルでは pip ("pip install") を使いますが、uv ("uv pip install") など、
"pyproject.toml" ベースのプロジェクトをビルドしてインストールできるツ
ールで代用できます。 できれば 仮想環境 を有効にしてください。

注釈:

  このチュートリアルでは、CPython 3.15 で追加された API を使います。
  以前の CPython と互換性のある拡張を作成する場合は、このドキュメント
  の以前のバージョンを参照してください。このチュートリアルでは、C11 と
  C++20 で追加された C の構文を使います。 以前の規格に対応する必要があ
  る場合は、Python 3.14 以前のドキュメントのチュートリアルを参照してく
  ださい。


作成するもの
============

"spam" [1] という名前の拡張モジュールを作成しましょう。 このモジュール
には、C 標準ライブラリの "system()" 関数を Python から呼び出すためのイ
ンターフェースを用意します。 この関数は "stdlib.h" に定義されています
。 C 文字列を引数として受け取り、システムコマンドとして実行して、結果
を整数で返します。 "system()" のマニュアルページには、次のような概要が
載っているでしょう。

   #include <stdlib.h>
   int system(const char *command);

C 標準ライブラリの多くの関数と同様に、この関数はすでに Python から利用
できます。 実用のコードでは、ここで作成するモジュールではなく
"os.system()" または "subprocess.run()" を使ってください。

この関数を Python から次のように呼び出せるようにします。

   >>> import spam
   >>> status = spam.system("whoami")
   User Name
   >>> status
   0

注釈:

  システムコマンド "whoami" はユーザー名を表示します。 Unix と Windows
  で同じ名前なので、このようなチュートリアルで使うのに便利です。


ヘッダから始める
================

まず、このチュートリアル用のディレクトリを作成し、コマンドラインでその
ディレクトリに移動してください。 次に、その中に "spammodule.c" という
ファイルを作成します。 [2]

このファイルでは、二つのヘッダをインクルードします。 "Python.h" から
Python C API のすべての宣言を取り込み、"stdlib.h" から "system()" 関数
の宣言を取り込みます。 [3]

"spammodule.c" に以下の行を追加してください。

   #include <Python.h>
   #include <stdlib.h>     // system() 用

"stdlib.h" と、それ以外の標準ライブラリのヘッダは、必ず "Python.h" の
*後に* インクルードしてください。 システムによっては、Python が標準ヘ
ッダに影響するプリプロセッサ定義を行うことがあります。


ビルドツールを実行する
======================

インクルードを書いただけでは、この拡張は何もしません。 それでも、ここ
でコンパイルしてインポートを試すとよいでしょう。 ビルドツールが動くこ
とを確認しておけば、以降の説明に沿って少しずつ変更し、その都度テストで
きます。

CPython 自体には拡張モジュールのビルドツールは付属しておらず、サードパ
ーティのプロジェクトを使うことを推奨します。 このチュートリアルでは
meson-python を使います。 (別のツールを使いたい場合は 付録: ほかのビル
ドツール を参照してください。)

"meson-python" では、追加の二つのファイルを使って「プロジェクト」を定
義する必要があります。

まず、次の内容の "pyproject.toml" を追加してください。

   [build-system]
   build-backend = 'mesonpy'
   requires = ['meson-python']

   [project]
   # 仮のプロジェクト情報
   # (モジュールを配布する前に変更してください)
   name = 'sampleproject'
   version = '0'

次に、以下の内容の "meson.build" を作成してください。

   project('sampleproject', 'c')

   py = import('python').find_installation(pure: false)

   py.extension_module(
      'spam',          # インポート可能な Python モジュールの名前
      'spammodule.c',  # C ソースファイル
      install: true,
   )

注釈:

  設定の詳細は meson-python のドキュメント を参照してください。

これで、"pip" を使って *現在のディレクトリのプロジェクト* (".") をビル
ドし、インストールできます。

   python -m pip -v install .

"-v" ("--verbose") オプションを付けると、"pip" がコンパイラの出力を表
示します。 開発中には便利なことが多いでしょう。

Tip:

  "pip" がインストールされていなければ、できれば 仮想環境 で "python
  -m ensurepip" を実行してください。 ("pyproject.toml" ベースのプロジ
  ェクトをビルドしてインストールできる別のツールを使いたい場合は、それ
  を使ってください。)

拡張を変更するたびに、このコマンドを再実行する必要があります。 Python
と異なり、C では明示的なコンパイルの手順が必要です。

拡張をコンパイルしてインストールしたら、Python を起動してインポートを
試してください。 次の例外が発生して失敗するはずです。

   >>> import spam
   Traceback (most recent call last):
      ...
   ImportError: dynamic module does not define module export function (PyModExport_spam or PyInit_spam)


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

モジュールのインポート時に発生した例外は、Python が「モジュールエクス
ポート関数」を探していることを示しています。 この関数は モジュールエク
スポートフック とも呼びます。 定義してみましょう。

まず、"#include" 行の下にプロトタイプを追加してください。


   PyMODEXPORT_FUNC PyModExport_spam(void);


Tip:

  プロトタイプは厳密には必須ではありませんが、最近のコンパイラの中には
  、これがないと警告を出すものがあります。 一般には、警告を無効にする
  よりプロトタイプを追加する方がよいでしょう。

"PyMODEXPORT_FUNC" マクロは関数の戻り値の型を宣言し、CPython が関数を
読み込むときにそれを見つけて使えるようにするため、必要な特別なリンケー
ジ宣言を追加します。

プロトタイプの後に関数本体を追加してください。 今は "NULL" を返すよう
にします。

   PyMODEXPORT_FUNC
   PyModExport_spam(void)
   {
      return NULL;
   }

モジュールをもう一度コンパイルして読み込んでください。 今度は別のエラ
ーになるはずです。

   >>> import spam
   Traceback (most recent call last):
      ...
   SystemError: module export hook for module 'spam' failed without setting an exception

単に "NULL" を返すのはエクスポートフックとして *正しい動作ではない* た
め、CPython がエラーを報告します。 これはよい兆候です。 CPython が関数
を見つけたことを意味するからです。 次は、有用な処理を行うようにしまし
ょう。


スロットテーブル
================

エクスポートフックは "NULL" ではなく、モジュールの作成に必要な情報を返
す必要があります。 まず、基本となる名前とドキュメント文字列から始めま
しょう。

この情報は、実質的にはキーと値のペアである "PySlot" エントリーの配列と
して定義します。 エクスポートフックの直前にこの配列を定義してください
。

   PyABIInfo_VAR(abi_info);

   static PySlot spam_slots[] = {
      PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
      PySlot_STATIC_DATA(Py_mod_name, "spam"),
      PySlot_STATIC_DATA(Py_mod_doc, "サンプル関数を持つ素晴らしいモジュール"),
      PySlot_END
   };

"PySlot_STATIC_DATA" マクロは、スロットの値 (ここでは "&abi_info"、
""spam""、ドキュメント文字列) が静的に割り当てられた定数データへのポイ
ンタである場合に使います。

"PyABIInfo_VAR(abi_info);" マクロと "Py_mod_abi" スロットは、別のバー
ジョンの Python 向けにコンパイルされた拡張がインタープリタをクラッシュ
させるのを防ぐための定型的な記述です。

"Py_mod_name" と "Py_mod_doc" の値はいずれも C 文字列、つまり NUL 終端
された UTF-8 エンコードのバイト配列です。

末尾にある番兵エントリー "PySlot_END" に注目してください。 これは配列
の終わりを示します。 忘れると未定義の動作を引き起こします。

配列は "static"、つまりこの ".c" ファイルの外からは見えないものとして
定義しています。 以降もこのような定義をよく使います。 CPython がアクセ
スする必要があるのはエクスポートフックだけです。 ほかの拡張との衝突を
避けるため、一般にすべてのグローバル変数とそれ以外の関数は "static" に
するべきです。

エクスポートフックから "NULL" の代わりにこの配列を返してください。

   PyMODEXPORT_FUNC
   PyModExport_spam(void)
   {
      return spam_slots;
   }

もう一度コンパイルして試しましょう。

   >>> import spam
   >>> print(spam)
   <module 'spam' from '/home/encukou/dev/cpython/spam.so'>

これで拡張モジュールができました。 "help(spam)" でドキュメント文字列を
確認してみてください。

次は関数を追加します。


関数を公開する
==============

C の "system()" 関数を Python から直接使えるようにするには、Python オ
ブジェクトの引数を C の値に変換し、C の戻り値を Python に戻す橋渡しの
コードを書く必要があります。

橋渡しのコードを書く最も簡単な方法の一つは、二つの Python オブジェクト
を受け取って一つを返す「"METH_O"」関数です。 Python オブジェクトは、
Python での型に関係なく、C ではすべて "PyObject" 構造体へのポインタで
表します。

スロット配列の上に、このような関数を追加してください。

   static PyObject *
   spam_system(PyObject *self, PyObject *arg)
   {
      Py_RETURN_NONE;
   }

今は引数を無視し、Python の "None" オブジェクトを正しく返す "return"
文に展開される "Py_RETURN_NONE" マクロを使います。

拡張を再コンパイルして、構文エラーがないことを確認してください。 まだ
"spam_system" をモジュールに追加していないため、使われていないという警
告が出るかもしれません。


メソッドの定義
--------------

C 関数を Python に公開するには、"PyMethodDef" [4] という構造体にいくつ
かの情報を設定する必要があります。

* "ml_name": Python 関数の名前。

* "ml_doc": ドキュメント文字列。

* "ml_meth": 呼び出す C 関数。

* "ml_flags": Python の引数を C 関数に渡す方法など、詳しい仕様を記述す
  るフラグの組み合わせ。 ここでは "spam_system" 関数のシグネチャに合う
  フラグ "METH_O" を使います。

モジュールは通常いくつもの関数を作成するため、これらの定義は配列にまと
め、末尾にゼロで埋めた番兵を置く必要があります。 "spam_system" 関数の
すぐ下に、この配列を追加してください。

   static PyMethodDef spam_methods[] = {
       {
           .ml_name="system",
           .ml_meth=spam_system,
           .ml_flags=METH_O,
           .ml_doc="シェルコマンドを実行します。",
       },
       {NULL, NULL, 0, NULL}        /* 番兵 */
   };

モジュールスロットと同じく、ゼロで埋めた番兵が配列の終わりを示します。

Next, we'll add the method to the module. Add a "Py_mod_methods" slot
to your "PyMethodDef" array:

   PyABIInfo_VAR(abi_info);

   static PySlot spam_slots[] = {
       PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
       PySlot_STATIC_DATA(Py_mod_name, "spam"),
       PySlot_STATIC_DATA(Py_mod_doc, "サンプル関数を持つ素晴らしいモジュール"),
       PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
       PySlot_END
   };

拡張をもう一度コンパイルしてテストしてください。 "import spam" が新し
いバージョンのモジュールを読み込むよう、Python インタープリタを必ず再
起動してください。

これで関数を呼び出せるはずです。

   >>> import spam
   >>> print(spam.system)
   <built-in function system>
   >>> print(spam.system('whoami'))
   None

まだ "spam.system" は "whoami" コマンドを実行せず、"None" を返すだけで
す。

"METH_O" フラグの指定どおり、関数が引数をちょうど一つ受け取ることを確
認してください。

   >>> print(spam.system('too', 'many', 'arguments'))
   Traceback (most recent call last):
      ...
   TypeError: spam.system() takes exactly one argument (3 given)


整数を返す
==========

次は戻り値を見てみましょう。 "spam.system" が "None" ではなく数値、つ
まり Python の "int" オブジェクトを返すようにします。 最終的にはシステ
ムコマンドの終了コードになりますが、まずは "3" などの固定値から始めま
しょう。

Python C API には、C の "int" 値から Python の "int" オブジェクトを作
成する "PyLong_FromLong()" 関数があります。 [5]

この関数を呼び出すために、"Py_RETURN_NONE" を次の 3 行に置き換えてくだ
さい。

   static PyObject *
   spam_system(PyObject *self, PyObject *arg)
   {
      int status = 3;
      PyObject *result = PyLong_FromLong(status);
      return result;
   }

再コンパイルして Python インタープリタを再起動し、関数が 3 を返すよう
になったことを確認してください。

   >>> import spam
   >>> spam.system('whoami')
   3


文字列を受け取る
================

最後に、関数の引数を処理しましょう。

C 関数 "spam_system()" は二つの引数を受け取ります。 最初の引数
"PyObject *self" には、"spam" モジュールオブジェクトが設定されます。
今回の用途では使わないので無視します。

もう一つの引数 "PyObject *arg" には、ユーザーが Python から渡したオブ
ジェクトが設定されます。 ここでは Python の文字列を想定しています。 そ
の内容を使うために C の値、ここでは C 文字列 ("const char *") に変換す
る必要があります。

ここには少し型の違いがあります。 Python の "str" オブジェクトは
Unicode テキストを格納しますが、C 文字列はバイトの配列です。 そのため
データの *エンコード* が必要で、ここでは UTF-8 を使います。 (UTF-8 は
システムコマンドに常に適しているとは限りませんが、"str.encode()" のデ
フォルトであり、C API には専用のサポートがあります。)

Python 文字列を UTF-8 のバッファにエンコードする関数は
"PyUnicode_AsUTF8AndSize()" [6] です。 次のように呼び出します。

   static PyObject *
   spam_system(PyObject *self, PyObject *arg)
   {
      const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
      int status = 3;
      PyObject *result = PyLong_FromLong(status);
      return result;
   }

"PyUnicode_AsUTF8AndSize()" が成功すると、"command" は結果の C 文字列
、つまりゼロ終端のバイト配列 [7] を指します。 このバッファは "arg" オ
ブジェクトが管理するため、解放する必要はありませんが、いくつかの規則を
守る必要があります。

* バッファは "spam_system" 関数内でだけ使ってください。 "spam_system"
  から戻った後は、"arg" とそれが管理するバッファがガベージコレクション
  によって回収される可能性があります。

* バッファを変更してはいけません。 これが "const" を使う理由です。

"PyUnicode_AsUTF8AndSize()" が *成功しなかった* 場合は "NULL" ポインタ
を返します。 Python C API を呼び出すときは *どの API でも*、このような
エラーを必ず処理する必要があります。 一般的な処理方法はこのドキュメン
トの後の章で説明します。 今の段階では、"PyLong_FromLong()" のエラーは
すでに正しく処理できているので安心してください。

"PyUnicode_AsUTF8AndSize()" の呼び出しに失敗した場合の正しい処理は、
"spam_system" から "NULL" を返すことです。 そのための "if" ブロックを
追加してください。

   static PyObject *
   spam_system(PyObject *self, PyObject *arg)
   {
      const char *command = PyUnicode_AsUTF8AndSize(arg);
      if (command == NULL) {
         return NULL;
      }
      int status = 3;
      PyObject *result = PyLong_FromLong(status);
      return result;
   }

エラー処理が動くことをテストするため、再コンパイルし、"import spam" が
新しいバージョンのモジュールを読み込むよう Python を再起動して、文字列
以外の値を関数に渡してみてください。

   >>> import spam
   >>> spam.system(3)
   Traceback (most recent call last):
      ...
   TypeError: bad argument type for built-in operation

残るのは、"char *" バッファを渡して C ライブラリの "system()" 関数を呼
び出し、"3" の代わりにその結果を使うことだけです。

   static PyObject *
   spam_system(PyObject *self, PyObject *arg)
   {
      const char *command = PyUnicode_AsUTF8AndSize(arg);
      if (command == NULL) {
         return NULL;
      }
      int status = system(command);
      PyObject *result = PyLong_FromLong(status);
      return result;
   }

モジュールをコンパイルし、Python を再起動してテストしてください。 今度
は "whoami" システムコマンドの出力であるユーザー名が表示されるはずです
。

   >>> import spam
   >>> result = spam.system('whoami')
   User Name
   >>> result
   0

"ls" や "dir"、あるいは存在しないコマンドなど、ほかのコマンドでもテス
トできます。

   >>> import spam
   >>> result = spam.system('nonexistent-command')
   sh: line 1: nonexistent-command: command not found
   >>> result
   32512


完成
====

これで完成です。 Python C API の拡張モジュールが一つできあがり、このチ
ュートリアルも終わりです。

参照しやすいよう、ソースファイル全体を載せておきます。

   /// インクルード

   #include <Python.h>
   #include <stdlib.h>     // system() 用

   /// spam.system の実装

   static PyObject *
   spam_system(PyObject *self, PyObject *arg)
   {
      const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
      if (command == NULL) {
         return NULL;
      }
      int status = system(command);
      PyObject *result = PyLong_FromLong(status);
      return result;
   }

   /// モジュールのメソッドテーブル

   static PyMethodDef spam_methods[] = {
       {
           .ml_name="system",
           .ml_meth=spam_system,
           .ml_flags=METH_O,
           .ml_doc="シェルコマンドを実行します。",
       },
       {NULL, NULL, 0, NULL}        /* 番兵 */
   };

   /// モジュールのスロットテーブル

   PyABIInfo_VAR(abi_info);

   static PySlot spam_slots[] = {
       PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
       PySlot_STATIC_DATA(Py_mod_name, "spam"),
       PySlot_STATIC_DATA(Py_mod_doc, "サンプル関数を持つ素晴らしいモジュール"),
       PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
       PySlot_END
   };

   /// エクスポートフックのプロトタイプ

   PyMODEXPORT_FUNC PyModExport_spam(void);

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

   PyMODEXPORT_FUNC
   PyModExport_spam(void)
   {
      return spam_slots;
   }


付録: ほかのビルドツール
========================

*ビルドツールを実行する* 節を除けば、"meson-python" 以外のビルドツール
でもこのチュートリアルを進められるはずです。

Python Packaging User Guide に 推奨ツールの一覧 があります。 C 言語に
対応したものを選んでください。


PyInit 関数がない場合の回避策
-----------------------------

ビルドツールが "PyInit_spam" がないというエラーを出す場合は、ひとまず
次の関数をモジュールに追加してください。

   // 回避策
   void *PyInit_spam(void) { return NULL; }

これは、CPython 3.14 以前の拡張モジュールで必要だった、旧式の 初期化関
数 の代わりになるものです。 現在の CPython はこの関数を必要としません
が、ビルドツールによっては、すべての拡張モジュールで定義する必要がある
と想定しているものがあります。

この回避策を使った場合は、"ImportError: dynamic module does not define
module export function" ではなく、"SystemError: initialization of spam
failed without raising an exception" という例外が発生します。


直接コンパイルする
------------------

サードパーティのビルドツールの使用を強く推奨します。 プラットフォーム
やインストールされた Python の細かな違い、生成する拡張の命名、さらに将
来の配布にも対応してくれるためです。

*特定の* システム向け、あるいは自分専用の拡張をビルドするなら、コンパ
イラを直接実行したい場合もあるでしょう。 その方法はシステムによって異
なるため、問題が起きた場合は自分で解決する必要があります。


Linux
~~~~~

Linux では、Python の開発用パッケージに、必要なコンパイラフラグを表示
する "python3-config" コマンドが含まれている場合があります。 使う場合
は、モジュールの読み込みに使う CPython インタープリタに対応しているこ
とを確認してください。 そのうえで、まず次のコマンドを試してください。

   gcc --shared $(python3-config --cflags --ldflags) spammodule.c -o spam.so

"spam.so" ファイルが生成されるはずです。 このファイルを "sys.path" に
含まれるディレクトリに置く必要があります。

-[ 脚注 ]-

[1] "spam" はモンティ・パイソンのファンの大好物です…

[2] ソースファイル名は自由に決められますが、ツールによっては拡張子が
    ".c" であることを求める場合があります。 このチュートリアルでは、伝
    統的な "*module.c" という末尾の形式を使います。 "spam" というモジ
    ュールを実装するのに単に "spam.c" を使う人もいれば、Python が主要
    言語ではないプロジェクトでは "py_spam.c" を使うこともあります。

[3] 技術的には "stdlib.h" のインクルードは不要です。 "Python.h" が、自
    身で使うため、あるいは後方互換性のために、このヘッダと ほかの標準
    ヘッダ をいくつかインクルードするからです。 ただし、必要なヘッダは
    明示的にインクルードするのがよい習慣です。

[4] "PyMethodDef" 構造体はクラスのメソッドを作成するためにも使うので、
    別途「"PyFunctionDef"」があるわけではありません。

[5] "PyLong_FromLong()" という名前は、すぐには意味が分からないかもしれ
    ません。 "PyLong" は、もともと "long" と呼ばれていた Python の
    "int" を指し、"FromLong" は C の "long" (または "long int") 型を指
    します。

[6] ここで "PyUnicode" は、Python の "str" クラスのもともとの名前
    "unicode" を指します。

    名前の "AndSize" は、出力引数を使ってバッファのサイズも取得できる
    ことを意味します。 ここでは不要なので、第 2 引数を NULL に設定しま
    す。

[7] ここでは、C 文字列を終端する NUL バイトを Python の文字列が含めら
    れることは考慮していません。 つまり、この関数は
    "spam.system("foo\0bar")" を "spam.system("foo")" として扱います。
    これはセキュリティ上の問題につながる可能性があるため、本物の
    "os.system" 関数はこの場合をチェックしてエラーを送出します。
