初めての 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 に含まれるディレクトリに置く必要があります。
脚注