🐍

Pythonのコンパイラを作りたい #8 - オブジェクトモデルの構築

2025/03/10に公開

こんにちは。前回(#7.5 - プロジェクト名の変更とレイアウト再構成(番外編))までで「字句解析 -> AST 変換 -> LLVM IR 化」や、プリミティブ型のボクシング/アンボクシングによる高速化などを一通り整備してきました。
今回は、ついに 本格的な「オブジェクト指向モデル」の導入 についてお話しします。具体的には、classobject.c / instanceobject.c / methodobject.c / functionobject.c といったファイルを新しく作成し、CPython に近い仕組みでクラス・インスタンス・メソッドを再現し始めました。
この 「オブジェクトモデル構築」 が何を狙っているのか、そして全体的にどんな構造を取っているのかを、なるべく分かりやすくまとめます。

1. どんな「オブジェクトモデル」を作りたいのか

1-1. CPython のオブジェクトシステムを手本に

本プロジェクトは「Python コードを LLVM IR へ変換して実行ファイル化する」というテーマを掲げていますが、実のところ、最終的に Python の主要ライブラリや C 拡張との親和性を保ちたいという狙いがあります。
そこでベースとして「CPython 流のオブジェクトシステム」をかなり忠実に模倣し始めました。具体的には:

  • PyObjectPyVarObject を中心とする型構造
    • すべてのオブジェクトが ob_refcnt, ob_type を共有する
    • リストや辞書などは PyVarObject を使って要素数 ob_size を持つ
  • tp_* (デストラクタや演算子テーブル) が詰まった PyTypeObject
    • tp_call, tp_repr, tp_as_number などを構造体メンバとして持ち、「この型はどう振る舞うか」を定義
  • Py_INCREF, Py_DECREF による参照カウント管理 + Boehm GC
    • CPython では参照カウント + GC (循環検知) ですが、本プロジェクトでは Boehm GC を併用し、循環参照の解決を任せる形
    • それでも互換性を意識して、Py_INCREF, Py_DECREF などは欠かさず置いています。

こうした設計にすることで、将来的に C レベルで「CPython と似た API」を部分的に実装したり、既存の Python 拡張モジュールを流用できる可能性が残るのです。

1-2. 従来との最大の違い

以前は「int は LLVM の i32」「str (Python 文字列) は PyUnicodeObject* のように最小限の型だけ C で定義し、あとはロジックをシンプルに...」というスタンスでした。
そこから今回のアップデートで大きく変わった点は、「class」や「def」まで含めて、Python 的なオブジェクトレイアウトとディスパッチ機構を再現し始めたということです。

そのため runtime/builtin/objects/ に多くのファイルが増え、PyClassObject, PyInstanceObject, PyMethodObject, PyFunctionObject といった概念が登場します。これらを組み合わせることで、

  • クラス本体 (class MyClass:) を「PyClassObject + cl_dict (属性辞書)」で表現
  • インスタンスを「PyInstanceObject + in_dict」として生成し、myinstance.method() の呼び出しを PyMethodObject 経由で行う
  • 関数オブジェクト (def func()) も PyFunctionObject という形で持つ

...といった、CPython にかなり近いアーキテクチャを目指しています。

2. 新たに追加された主な C ファイル・構造体

2-1. classobject.c/h - クラス定義

  • PyClassObject: クラスを表すための構造体
    • cl_name: クラス名 ("MyClass" など)
    • cl_dict: クラス辞書 (__dict__ 相当) - メソッドやクラス変数が入る
    • cl_bases: 基底クラスのリスト (継承をサポートするため)
  • PyClass_Type: PyTypeObject を継承した「クラスの型オブジェクト」
    • tp_callclass_call を割り当て、「クラス呼び出し (= インスタンス化)」を実装
  • PyClass_New(name, bases, dict): 新しいクラスを生成する関数

2-2. functionobject.c/h - 関数定義

  • PyFunctionObject: 関数を保持するための構造体
    • func_code: 実際の関数コード (ネイティブ関数なら関数ポインタ、将来的には LLVM IR or バイトコード)
    • func_name: 関数名
    • func_defaults: デフォルト引数 (未実装部分もある)
  • PyFunction_New(...): 新しい関数オブジェクトを作成
  • PyFunction_FromNative(...): ネイティブC関数を Python 関数オブジェクトとして登録する場合に使う

2-3. methodobject.c/h - メソッドバインディング

  • PyMethodObject: 「インスタンス + 関数」をひとまとめにしたバインドメソッドの構造体
    • im_func: 関数 (PyFunctionObject*)
    • im_self: インスタンス (PyInstanceObject*)
    • tp_call で呼び出し時に最初の引数に im_self を挿入する
  • PyMethod_New(func, self): 関数とインスタンスをバインドして新しいメソッドを作る

2-4. classobject.c/h - インスタンス

  • PyInstanceObject:
    • in_class: どのクラス (PyClassObject*) から生成されたか
    • in_dict: このインスタンス固有の属性辞書
  • PyInstance_NewRaw / PyInstance_New: 実際にインスタンスを作る関数
    • PyInstance_NewRaw__init__ を呼び出さない
    • PyInstance_New__init__ メソッドを検索して呼び出す

大まかにこのような構造になっており、クラス -> インスタンス -> メソッド呼び出しの流れを C レベルで完結できるようになっています。

3. どうやってクラス定義やインスタンス生成が行われるのか

3-1. クラス定義 (class MyClass:) の裏側

  1. class_dict = PyDict_New()
  • まず空の辞書を作成し、クラスのメソッドや変数を登録するために使う
  1. メソッド定義 (FunctionDef) を読み取り、PyFunctionObject (またはネイティブ関数) を作成
  • PyDict_SetItem(class_dict, method_name, function_object) で登録
  1. クラス名 ("MyClass") を Python 文字列 (PyUnicodeObject) に変換
  2. PyClass_New(class_name, bases, class_dict) を呼び出して PyClassObject を得る
  • cl_name = "MyClass"
  • cl_dict = class_dict
  • cl_bases = ... (現状は空リスト)
  1. 生成されたクラスオブジェクトを %MyClass のようなシンボルとして保存し、シンボルテーブルに登録

こうして、実行時に「MyClass は Python のクラスオブジェクト (PyClassObject)」として動作できるようになります。

3-2. インスタンス生成 (x = MyClass())

  1. LLVM IR レベルで %class_obj = load ptr, ptr %MyClass(クラスオブジェクトをロード)
  2. call ptr @PyObject_Call(ptr %class_obj, <args>, null) もしくは class_call に直接飛ぶ
  3. class_call の中で PyInstance_New が呼ばれ、PyInstanceObject を作る
  4. __init__ メソッドがあれば(cl_dict["__init__"])呼び出し

この時点で PyInstanceObject が返り、それを %x として保持します。
ユーザープログラムから見ると「x = MyClass()」でインスタンスができているように見えますが、実際にはクラスの tp_call 実装が動いてインスタンス生成 -> __init__呼び出しという流れになっているわけです。

3-3. メソッド呼び出し (x.method(...))

  • instance_getattromethod を取り出すときに、PyMethodObject を作成する
    • (im_func=cl_dict["method"], im_self=x)
  • 呼び出し時 (method_call) に self を先頭に差し込んで実行
  • 本質的には PyFunctionObject の中にあるネイティブ関数 or LLVM IR 関数が呼ばれる

これが 「バインドメソッド (bound method)」 の仕組みです。CPython と同じ流れなので、Python の「メソッドはクラス辞書に格納され、インスタンスアクセス時に自動バインド」という挙動が再現されます。

4. コンパイラ (LLVM IR 生成) の変更ポイント

4-1. StmtVisitor.visit_ClassDef()

  • ClassDef ノードが来たら、クラス辞書を PyDict_New() で作成し、メソッドを登録 (PyDict_SetItem)
  • クラス名を PyUnicode_FromString で用意
  • 最後に PyClass_New を呼んでクラスオブジェクトを得る
  • %ClassName = alloca ptr; store ptr <class_obj>, ptr %ClassName でクラスをローカルシンボルにする

このフローが終わると、ユーザーが定義したクラスはプログラム内で「ポインタ」として扱われます。
class_dict__init__ などは C レベルで管理されており、動的にも拡張しやすい状態です。

4-2. ExprVisitor.visit_Call() -> 「クラス呼び出し」の特別対応

  • もし呼び出そうとしている対象がクラスオブジェクト (PyClassObject) なら、tp_callclass_call を指している -> PyInstance_New(...)
  • 実際にはまだそこまで厳密には作り込まれていませんが、最終的には isinstance(呼び出し対象, PyClass_Type) かどうかチェックし、クラスならインスタンス生成、それ以外なら関数呼び出し... という流れを想定しています。

これは 「ユーザー関数呼び出し」と「クラス呼び出し (インスタンス化)」 の分岐をコンパイラがどう判断するか、という点に関わります。現状は「単に %funcName が登録されているシンボルかどうか」を見ていますが、今後は「シンボルがクラスか関数かをより明確に区別する仕組み」が必要になるでしょう。

5. メリットとこれからの拡張

5-1. メリット

  1. 本格的な Python のクラス・メソッドが書けるようになる
  • class Foo: def method(self): ... の形を IR 化し、実際に動作できる下地が整う
  1. 既存の Python オブジェクト指向構文を転用しやすい
  • CPython 互換 API に近いので、tp_call, tp_getattro, tp_setattro などを活用できる
  1. 将来的な C 拡張モジュールとの連携
  • PyTypeObject という形を使っていれば、C 拡張ライブラリも、そう大きく書き換えずに使える可能性がある

5-2. これからの拡張ポイント

  1. 実際のバイトコード / LLVM IR 関数呼び出し
  • PyFunctionObject の中にある func_codefunc_globals をどう運用するか
  • 現在は最小限のネイティブ関数ポインタに対応しているだけで、Python の複雑なバイトコード実行は未実装
  1. __init____new__ の完全対応
  • PyInstance_NewPyClass_Type.tp_call__init__ を呼ぶ仕組みはあるが、引数の受け取り方や継承関係のメソッド探索は十分ではない
  1. 例外や継承チェーン
  • cl_bases を使った class Derived(Base) の対応、PyErr_* まわりの実装強化など
  • 例外は exceptions.c / excepthandler.c を介して try: ... except: ... をどう IR 化するかも課題

まとめ & 今後の話

  • CPython ライクなオブジェクト指向モデルを一気に導入し、PyClassObject, PyInstanceObject, PyMethodObject, PyFunctionObject などを中心にクラス・インスタンス・メソッドを表せるようにした
  • classobject.c / methodobject.c / functionobject.c / instanceobject.c といった形で、ほぼ CPython の C API を想起させるレイアウトや関数名を採用している
  • コンパイラ (LLVM IR 生成) 側では、ClassDef / FunctionDef / Call に対し「クラス辞書の用意」「PyClass_New によるクラス生成」「インスタンス化 ( PyInstance_New )」「メソッドバインディング」などを出力する処理を追加中
  • 今後は __init____new__, 例外システム、バイトコード風の関数コードなど、より高度な Python の挙動に対応していく予定

まとめると、今回のアップデートで Python のオブジェクト志向機能の「骨格」が通り始めました。あとは各種メソッド・例外・継承などの実装を段階的に拡張していけば、かなり本格的に「CPython 互換」に近い世界が見えてきそうです。
次回はクラス辞書 (cl_dict) の扱いをもう少し深堀りしながら、メソッド登録や継承チェーンの雛形をどう組んでいるかを見ていく予定です。引き続きお楽しみに!

Discussion