[インデックス 1433] ファイルの概要
このコミットは、Goコンパイラの内部、特にsrc/cmd/6g/obj.cファイルにおける型シグネチャの生成ロジックに関する修正です。6gは当時のGoコンパイラ(gcツールチェーン)の64ビットアーキテクチャ向けバックエンドを指します。このファイルは、コンパイルされたGoプログラムのオブジェクトファイル(.oファイル)を生成する際に、型情報やメソッドシグネチャを適切に出力する役割を担っていました。
コミット
commit 1b1f1b53ea8dfabf84db11c871fc6070a851532a
Author: Russ Cox <rsc@golang.org>
Date: Wed Jan 7 13:29:03 2009 -0800
correct signature generation decision
for non-pointer types with methods.
R=r
DELTA=37 (13 added, 14 deleted, 10 changed)
OCL=22217
CL=22219
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1b1f1b53ea8dfabf84db11c871fc6070a851532a
元コミット内容
このコミットは、Goコンパイラ(6g)において、メソッドを持つ非ポインタ型に対するシグネチャ生成の判断を修正することを目的としています。具体的には、dumpsigt関数とdumpsignatures関数の間で型情報の受け渡しと処理ロジックが変更され、ポインタ型ではないがメソッドを持つ型(例えば、struct型そのものにメソッドが定義されている場合)のシグネチャが正しく生成されるように調整されました。
変更の背景
Go言語では、メソッドは値型(T)とポインタ型(*T)の両方に定義できます。ポインタ型*Tのレシーバを持つメソッドは*Tにのみ関連付けられますが、値型Tのレシーバを持つメソッドはTと*Tの両方に関連付けられます(*TからTへの自動的な間接参照が行われるため)。
このコミットが行われた2009年当時、Go言語はまだ開発の初期段階にあり、コンパイラの型システムやメソッド解決のロジックは進化の途上にありました。以前の実装では、dumpsigt関数がポインタ型を受け取った場合に、その基底型(ポインタが指す型)に自動的に展開してメソッドを処理するロジックを持っていました。しかし、このアプローチでは、ポインタではないがメソッドを持つ型(例: type MyStruct struct { ... } に func (m MyStruct) MyMethod() { ... } が定義されている場合)のシグネチャ生成において、誤った判断やスキップが発生する可能性がありました。
特に、外部ファイルで定義された型(!t->local)で、かつ非ポインタ型でありながらメソッドを持つ場合、そのシグネチャがオブジェクトファイルに適切に埋め込まれない、あるいは不必要にスキップされるという問題があったと考えられます。オブジェクトファイルに型やメソッドのシグネチャを正確に埋め込むことは、リンカが正しく動作し、リフレクションなどのランタイム機能が型情報を利用するために不可欠です。
このコミットは、このようなシグネチャ生成の不整合を解消し、Goの型システムにおけるメソッドの振る舞いをコンパイラがより正確に反映できるようにするために導入されました。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラ(当時のgcツールチェーン)の内部構造とGo言語の基本的な概念に関する知識が必要です。
-
Goコンパイラ(
gc)の構造:src/cmd/6g/obj.c: これはGoコンパイラのバックエンドの一部であり、64ビットアーキテクチャ(amd64)向けのオブジェクトファイル生成を担当するC言語のソースファイルです。コンパイラが生成した中間表現から、最終的な機械語コードやメタデータを含むオブジェクトファイルを生成する過程で、型情報やメソッドシグネチャの出力も行われます。- 型システム: Goコンパイラは、プログラム内のすべての型を内部的なデータ構造(
Type構造体など)で表現します。これには、基本型、構造体、インターフェース、ポインタなどが含まれます。 - シンボルテーブル: コンパイラは、変数、関数、型などの名前(シンボル)とその定義を管理するシンボルテーブルを持っています。
Sym構造体はシンボルを表します。 - メソッドセット: Goでは、各型は関連付けられたメソッドのセット(メソッドセット)を持ちます。メソッドはレシーバの型によって値レシーバ(
T)とポインタレシーバ(*T)に区別されます。
-
Type構造体と関連フィールド:Type *t: コンパイラ内部で型を表すポインタ。t->etype: 型の基本的な種類(例:TINT、TSTRUCT、TPTR、TINTERなど)。isptr配列はこのetypeがポインタ型であるかどうかを判定するために使われます。t->type: ポインタ型の場合、そのポインタが指す基底型へのポインタ。t->sym: その型に関連付けられたシンボル(名前)。t->method: その型に定義されているメソッドのリスト(通常は連結リスト)。t->local: その型が現在のコンパイルユニット内でローカルに定義されているかどうかを示すフラグ。
-
Sym構造体:Sym *s: シンボルを表すポインタ。
-
expandmeth関数:- この関数は、与えられた型のメソッドセットを展開する役割を担います。特にポインタ型の場合、その基底型に定義されたメソッドもポインタ型から呼び出せるため、それらのメソッドをポインタ型のメソッドセットに含める処理を行います。
-
シグネチャ生成:
- コンパイラがオブジェクトファイルを生成する際、型やメソッドに関するメタデータ(シグネチャ)を埋め込みます。これにより、リンカが異なるコンパイルユニット間で型を解決したり、ランタイムがリフレクションを通じて型情報を取得したりすることが可能になります。
技術的詳細
このコミットの技術的な核心は、dumpsigt関数とdumpsignatures関数の間の責任分担の変更と、型シグネチャ生成の条件判断の厳密化にあります。
dumpsigt関数の変更
- シグネチャの変更:
- 変更前:
void dumpsigt(Type *t0, Sym *s) - 変更後:
void dumpsigt(Type *t0, Type *t, Sym *s) - 新しい
Type *t引数が追加されました。これは、t0がポインタ型である場合に、そのポインタが指す基底型(メソッドが実際に定義されている型)を明示的に渡すためのものです。
- 変更前:
- 内部ロジックの削除:
- 変更前は
dumpsigt関数内で、t0がポインタ型であればt = t0->type; expandmeth(t->sym, t);というロジックで基底型に展開し、メソッドセットを拡張していました。 - このコミットでは、このロジックが
dumpsigtから削除されました。これにより、dumpsigtは常に、メソッドセットを生成すべき適切な型(ポインタの基底型、または非ポインタ型そのもの)をt引数として受け取ることを期待するようになりました。この変更は、関数の単一責任原則に沿ったリファクタリングとも言えます。
- 変更前は
dumpsignatures関数の変更
dumpsignatures関数は、コンパイル対象のすべての型を走査し、必要に応じてそのシグネチャをオブジェクトファイルにダンプする役割を担っています。この関数における変更が、シグネチャ生成の判断ロジックの核心です。
- ローカル変数
t0の導入:Type *t, *t0;が追加され、元の型を保持するためのt0が導入されました。
- インターフェース型の処理の明確化:
- インターフェース型(
et == TINTER)の場合の処理が独立したブロックに切り出されました。 if(t->sym && !t->local) continue;という条件で、シンボルを持ち、かつローカルではないインターフェース型はスキップされます。これは、外部で定義されたインターフェースのシグネチャを重複して生成しないための最適化と考えられます。- その後、
dumpsigi(t, s);が呼び出され、インターフェースのシグネチャがダンプされます。
- インターフェース型(
- ポインタ型と非ポインタ型のメソッド処理の統一:
- 変更前は、ポインタ型と非ポインタ型でシグネチャ生成の呼び出しが分かれていました。
- 変更後:
// if there's a pointer, methods are on base. t0 = t; // 元の型をt0に保存 if(isptr[et] && t->type->sym != S) { // ポインタ型の場合 t = t->type; // 基底型に展開 expandmeth(t->sym, t); // 基底型のメソッドセットを展開 } if(t->method && t->sym && !t->local) // メソッドを持ち、シンボルがあり、ローカルではない型の場合 continue; // シグネチャ生成をスキップ dumpsigt(t0, t, s); // dumpsigtを呼び出し、元の型(t0)とメソッドの基底型(t)を渡す - この新しいロジックでは、まず
t0に元の型を保存します。 - 次に、現在の型
tがポインタ型である場合、その基底型にtを更新し、expandmethを呼び出してメソッドセットを拡張します。これにより、tは常にメソッドが実際に定義されている型(ポインタの基底型、または非ポインタ型そのもの)を指すようになります。 - その後の
if(t->method && t->sym && !t->local) continue;という条件は、メソッドを持ち、シンボルがあり、かつローカルではない型(つまり、外部で定義された型で、すでにシグネチャが生成されている可能性のある型)のシグネチャ生成をスキップするためのものです。この条件は、特にメソッドを持つ非ポインタ型が以前は誤ってスキップされていた可能性を修正するものです。 - 最後に、
dumpsigt(t0, t, s);が呼び出されます。ここで、t0は元の型、tはメソッドセットの展開が行われた後の型(ポインタの基底型または非ポインタ型そのもの)として渡されます。これにより、dumpsigtは常に正しいコンテキストでシグネチャを生成できるようになります。
この変更により、コンパイラは、ポインタ型を介してアクセスされるメソッドと、非ポインタ型に直接定義されたメソッドの両方について、正確なシグネチャ情報をオブジェクトファイルに埋め込むことができるようになりました。
コアとなるコードの変更箇所
--- a/src/cmd/6g/obj.c
+++ b/src/cmd/6g/obj.c
@@ -612,9 +612,9 @@ out:
}
void
-dumpsigt(Type *t0, Sym *s)
+dumpsigt(Type *t0, Type *t, Sym *s)
{
- Type *f, *t;
+ Type *f;
Sym *s1;
int o;
Sig *a, *b;
@@ -623,12 +623,6 @@ dumpsigt(Type *t0, Sym *s)
at.sym = s;
- t = t0;
- if(isptr[t->etype] && t->type->sym != S) {
- t = t->type;
- expandmeth(t->sym, t);
- }
-
a = nil;
o = 0;
for(f=t->method; f!=T; f=f->down) {
@@ -815,7 +809,7 @@ dumpsignatures(void)
{
int et;
Dcl *d, *x;
- Type *t;
+ Type *t, *t0;
Sym *s, *s1;
Prog *p;
@@ -893,22 +887,27 @@ dumpsignatures(void)
// don't emit non-trivial signatures for types defined outside this file.
// non-trivial signatures might also drag in generated trampolines,
// and ar can't handle duplicates of the trampolines.
- s1 = S;
- if(isptr[et] && t->type != T) {
- s1 = t->type->sym;
- if(s1 && !t->type->local)
- continue;
- }
- else if(et == TINTER) {
- s1 = t->sym;
- if(s1 && !t->local)
- continue;
- }
-
- if(et == TINTER)
- dumpsigi(t, s);
- else
- dumpsigt(t, s);
+ // only pay attention to types with symbols, because
+ // the ... structs and maybe other internal structs
+ // don't get marked as local.
+
+ // interface is easy
+ if(et == TINTER) {
+ if(t->sym && !t->local)
+ continue;
+ dumpsigi(t, s);
+ continue;
+ }
+
+ // if there's a pointer, methods are on base.
+ t0 = t;
+ if(isptr[et] && t->type->sym != S) {
+ t = t->type;
+ expandmeth(t->sym, t);
+ }
+ if(t->method && t->sym && !t->local)
+ continue;
+ dumpsigt(t0, t, s);
}
if(stringo > 0) {
コアとなるコードの解説
dumpsigt関数の変更点
dumpsigt(Type *t0, Type *t, Sym *s):- この関数は、特定の型
t(メソッドが実際に定義されている基底型)のメソッドシグネチャをダンプする役割を担います。t0は元の型(ポインタ型の場合、そのポインタ型自体)を保持します。 - 変更前は、
dumpsigt内でt0がポインタ型であれば、その基底型にtを更新し、expandmethを呼び出してメソッドセットを展開していました。 - 変更後は、このロジックが削除され、
dumpsigtは呼び出し元(dumpsignatures)から既に適切なt(メソッドセットが展開された基底型)が渡されることを前提とするようになりました。これにより、dumpsigtの責務が「与えられた型のメソッドシグネチャをダンプする」ことに限定され、コードの分離と理解が容易になりました。
- この関数は、特定の型
dumpsignatures関数の変更点
Type *t, *t0;:t0という新しいローカル変数が導入されました。これは、ループ内で処理している現在の型を一時的に保持するために使用されます。
- インターフェース型の処理:
if(et == TINTER)ブロックが明確に分離されました。if(t->sym && !t->local) continue;という条件は、シンボルを持ち、かつローカルではないインターフェース型(つまり、他のコンパイルユニットで定義されたインターフェース)のシグネチャ生成をスキップします。これは、重複するシグネチャの生成を避けるための最適化です。dumpsigi(t, s);はインターフェース型のシグネチャをダンプする関数です。
- ポインタ型と非ポインタ型のメソッド処理の統合と修正:
t0 = t;で、現在の型tをt0に保存します。これは、後でdumpsigtを呼び出す際に元の型情報が必要になるためです。if(isptr[et] && t->type->sym != S): もし現在の型tがポインタ型であり、かつその基底型にシンボルがある場合、以下の処理を行います。t = t->type;:tをポインタの基底型に更新します。これにより、tはメソッドが実際に定義されている型を指すようになります。expandmeth(t->sym, t);: 基底型のメソッドセットを展開します。これにより、ポインタ型を介してアクセス可能なメソッドも考慮されます。
if(t->method && t->sym && !t->local) continue;:- この条件が最も重要な変更点の一つです。これは、メソッドを持ち (
t->method)、シンボルがあり (t->sym)、かつローカルではない (!t->local) 型のシグネチャ生成をスキップします。 - 以前のロジックでは、非ポインタ型でメソッドを持つ場合でも、この条件が適切に評価されず、シグネチャが生成されない、または誤ってスキップされる可能性がありました。この修正により、外部で定義された型であっても、メソッドを持つ非ポインタ型が正しく処理されるようになりました。
- この条件が最も重要な変更点の一つです。これは、メソッドを持ち (
dumpsigt(t0, t, s);:- 最終的に
dumpsigtが呼び出されます。ここで、t0(元の型)と、メソッドセットが展開された後のt(基底型)の両方が渡されます。これにより、dumpsigtは元の型と、メソッドが関連付けられている実際の型の両方のコンテキストでシグネチャを生成できます。
- 最終的に
この一連の変更により、Goコンパイラは、ポインタ型と非ポインタ型の両方について、メソッドシグネチャをより正確かつ効率的にオブジェクトファイルに埋め込むことができるようになりました。特に、外部で定義された型や、ポインタではないがメソッドを持つ型に対するシグネチャ生成の信頼性が向上しました。
関連リンク
- Go言語の型システム: https://go.dev/tour/methods/1 (Go Tourのメソッドに関するセクション)
- Goコンパイラのソースコード: https://github.com/golang/go
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード(特に
src/cmd/6g/ディレクトリ) - Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(当時のコンパイラの設計思想を理解するため)
- Go言語のメソッドセットに関する公式ブログ記事や解説記事