Pythonのコンパイラを作りたい #8 - オブジェクトモデルの構築
こんにちは。前回(#7.5 - プロジェクト名の変更とレイアウト再構成(番外編))までで「字句解析 -> AST 変換 -> LLVM IR 化」や、プリミティブ型のボクシング/アンボクシングによる高速化などを一通り整備してきました。
今回は、ついに 本格的な「オブジェクト指向モデル」の導入 についてお話しします。具体的には、classobject.c
/ instanceobject.c
/ methodobject.c
/ functionobject.c
といったファイルを新しく作成し、CPython に近い仕組みでクラス・インスタンス・メソッドを再現し始めました。
この 「オブジェクトモデル構築」 が何を狙っているのか、そして全体的にどんな構造を取っているのかを、なるべく分かりやすくまとめます。
1. どんな「オブジェクトモデル」を作りたいのか
1-1. CPython のオブジェクトシステムを手本に
本プロジェクトは「Python コードを LLVM IR へ変換して実行ファイル化する」というテーマを掲げていますが、実のところ、最終的に Python の主要ライブラリや C 拡張との親和性を保ちたいという狙いがあります。
そこでベースとして「CPython 流のオブジェクトシステム」をかなり忠実に模倣し始めました。具体的には:
-
PyObject
やPyVarObject
を中心とする型構造- すべてのオブジェクトが
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 ファイル・構造体
classobject.c/h
- クラス定義
2-1. -
PyClassObject
: クラスを表すための構造体-
cl_name
: クラス名 ("MyClass"
など) -
cl_dict
: クラス辞書 (__dict__
相当) - メソッドやクラス変数が入る -
cl_bases
: 基底クラスのリスト (継承をサポートするため)
-
-
PyClass_Type
:PyTypeObject
を継承した「クラスの型オブジェクト」-
tp_call
にclass_call
を割り当て、「クラス呼び出し (= インスタンス化)」を実装
-
-
PyClass_New(name, bases, dict)
: 新しいクラスを生成する関数
functionobject.c/h
- 関数定義
2-2. -
PyFunctionObject
: 関数を保持するための構造体-
func_code
: 実際の関数コード (ネイティブ関数なら関数ポインタ、将来的には LLVM IR or バイトコード) -
func_name
: 関数名 -
func_defaults
: デフォルト引数 (未実装部分もある)
-
-
PyFunction_New(...)
: 新しい関数オブジェクトを作成 -
PyFunction_FromNative(...)
: ネイティブC関数を Python 関数オブジェクトとして登録する場合に使う
methodobject.c/h
- メソッドバインディング
2-3. -
PyMethodObject
: 「インスタンス + 関数」をひとまとめにしたバインドメソッドの構造体-
im_func
: 関数 (PyFunctionObject*
) -
im_self
: インスタンス (PyInstanceObject*
) -
tp_call
で呼び出し時に最初の引数にim_self
を挿入する
-
-
PyMethod_New(func, self)
: 関数とインスタンスをバインドして新しいメソッドを作る
classobject.c/h
- インスタンス
2-4. -
PyInstanceObject
:-
in_class
: どのクラス (PyClassObject*
) から生成されたか -
in_dict
: このインスタンス固有の属性辞書
-
-
PyInstance_NewRaw
/PyInstance_New
: 実際にインスタンスを作る関数-
PyInstance_NewRaw
は__init__
を呼び出さない -
PyInstance_New
は__init__
メソッドを検索して呼び出す
-
大まかにこのような構造になっており、クラス -> インスタンス -> メソッド呼び出しの流れを C レベルで完結できるようになっています。
3. どうやってクラス定義やインスタンス生成が行われるのか
class MyClass:
) の裏側
3-1. クラス定義 (class_dict = PyDict_New()
- まず空の辞書を作成し、クラスのメソッドや変数を登録するために使う
- メソッド定義 (FunctionDef) を読み取り、
PyFunctionObject
(またはネイティブ関数) を作成
-
PyDict_SetItem(class_dict, method_name, function_object)
で登録
- クラス名 (
"MyClass"
) を Python 文字列 (PyUnicodeObject
) に変換 -
PyClass_New(class_name, bases, class_dict)
を呼び出してPyClassObject
を得る
cl_name = "MyClass"
cl_dict = class_dict
-
cl_bases = ...
(現状は空リスト)
- 生成されたクラスオブジェクトを
%MyClass
のようなシンボルとして保存し、シンボルテーブルに登録
こうして、実行時に「MyClass は Python のクラスオブジェクト (PyClassObject)」として動作できるようになります。
x = MyClass()
)
3-2. インスタンス生成 (- LLVM IR レベルで
%class_obj = load ptr, ptr %MyClass
(クラスオブジェクトをロード) -
call ptr @PyObject_Call(ptr %class_obj, <args>, null)
もしくはclass_call
に直接飛ぶ -
class_call
の中でPyInstance_New
が呼ばれ、PyInstanceObject
を作る -
__init__
メソッドがあれば(cl_dict["__init__"]
)呼び出し
この時点で PyInstanceObject
が返り、それを %x
として保持します。
ユーザープログラムから見ると「x = MyClass()
」でインスタンスができているように見えますが、実際にはクラスの tp_call
実装が動いてインスタンス生成 -> __init__
呼び出しという流れになっているわけです。
x.method(...)
)
3-3. メソッド呼び出し (-
instance_getattro
でmethod
を取り出すときに、PyMethodObject
を作成する(im_func=cl_dict["method"], im_self=x)
- 呼び出し時 (
method_call
) にself
を先頭に差し込んで実行 - 本質的には
PyFunctionObject
の中にあるネイティブ関数 or LLVM IR 関数が呼ばれる
これが 「バインドメソッド (bound method)」 の仕組みです。CPython と同じ流れなので、Python の「メソッドはクラス辞書に格納され、インスタンスアクセス時に自動バインド」という挙動が再現されます。
4. コンパイラ (LLVM IR 生成) の変更ポイント
StmtVisitor.visit_ClassDef()
4-1. -
ClassDef
ノードが来たら、クラス辞書をPyDict_New()
で作成し、メソッドを登録 (PyDict_SetItem
) - クラス名を
PyUnicode_FromString
で用意 - 最後に
PyClass_New
を呼んでクラスオブジェクトを得る -
%ClassName = alloca ptr; store ptr <class_obj>, ptr %ClassName
でクラスをローカルシンボルにする
このフローが終わると、ユーザーが定義したクラスはプログラム内で「ポインタ」として扱われます。
class_dict
や __init__
などは C レベルで管理されており、動的にも拡張しやすい状態です。
ExprVisitor.visit_Call()
-> 「クラス呼び出し」の特別対応
4-2. - もし呼び出そうとしている対象がクラスオブジェクト (
PyClassObject
) なら、tp_call
がclass_call
を指している ->PyInstance_New(...)
- 実際にはまだそこまで厳密には作り込まれていませんが、最終的には
isinstance(呼び出し対象, PyClass_Type)
かどうかチェックし、クラスならインスタンス生成、それ以外なら関数呼び出し... という流れを想定しています。
これは 「ユーザー関数呼び出し」と「クラス呼び出し (インスタンス化)」 の分岐をコンパイラがどう判断するか、という点に関わります。現状は「単に %funcName
が登録されているシンボルかどうか」を見ていますが、今後は「シンボルがクラスか関数かをより明確に区別する仕組み」が必要になるでしょう。
5. メリットとこれからの拡張
5-1. メリット
- 本格的な Python のクラス・メソッドが書けるようになる
-
class Foo: def method(self): ...
の形を IR 化し、実際に動作できる下地が整う
- 既存の Python オブジェクト指向構文を転用しやすい
- CPython 互換 API に近いので、
tp_call
,tp_getattro
,tp_setattro
などを活用できる
- 将来的な C 拡張モジュールとの連携
-
PyTypeObject
という形を使っていれば、C 拡張ライブラリも、そう大きく書き換えずに使える可能性がある
5-2. これからの拡張ポイント
- 実際のバイトコード / LLVM IR 関数呼び出し
-
PyFunctionObject
の中にあるfunc_code
やfunc_globals
をどう運用するか - 現在は最小限のネイティブ関数ポインタに対応しているだけで、Python の複雑なバイトコード実行は未実装
__init__
や__new__
の完全対応
-
PyInstance_New
やPyClass_Type.tp_call
で__init__
を呼ぶ仕組みはあるが、引数の受け取り方や継承関係のメソッド探索は十分ではない
- 例外や継承チェーン
-
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