[インデックス 14282] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるバグ修正です。具体的には、インライン化された関数内で宣言されたローカル変数が、エクスポートされていない型に依存している場合に、その型情報がエクスポートデータから欠落してしまう問題を解決します。これにより、gcflags -lll
オプションを付けてビルドする際に発生していたコンパイルエラーが修正されます。
コミット
commit 8d95245d0dadc1d44ac3567c210d2187e9a4aeea
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Nov 1 19:06:52 2012 +0100
cmd/gc: fix incomplete export data when inlining with local variables.
When local declarations needed unexported types, these could
be missing in the export data.
Fixes build with -gcflags -lll, except for exp/gotype.
R=golang-dev, rsc, lvd
CC=golang-dev
https://golang.org/cl/6813067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8d95245d0dadc1d44ac3567c210d2187e9a4aeea
元コミット内容
cmd/gc
: ローカル変数を持つインライン化時に不完全なエクスポートデータを修正。
ローカル宣言がエクスポートされていない型を必要とする場合、これらの型がエクスポートデータから欠落する可能性がありました。
exp/gotype
を除く gcflags -lll
を使用したビルドを修正します。
変更の背景
Goコンパイラ(gc
)は、プログラムのコンパイル時に、他のパッケージから参照される可能性のある型や関数などの情報を「エクスポートデータ」として生成します。このエクスポートデータは、コンパイル済みのパッケージが他のパッケージから利用される際に、型チェックやリンケージのために必要となります。
このコミットが修正する問題は、特定の条件下で発生しました。Goコンパイラには、パフォーマンス最適化のために「インライン化(inlining)」という機能があります。これは、小さな関数呼び出しを、その関数の本体で直接置き換えることで、関数呼び出しのオーバーヘッドを削減する技術です。
問題は、インライン化される関数の内部で宣言されたローカル変数が、その変数が属するパッケージ外からは直接参照できない「エクスポートされていない型(unexported type)」に依存している場合に発生しました。Goの言語仕様では、識別子が大文字で始まるものがエクスポートされ、小文字で始まるものがエクスポートされません。
通常、エクスポートされていない型はパッケージ内部でのみ使用されるため、エクスポートデータに含める必要はありません。しかし、インライン化によって関数のコードが呼び出し元に展開される際、コンパイラがローカル変数の型情報を適切に追跡し、必要に応じてエクスポートデータに含める処理が不完全でした。結果として、エクスポートデータが不完全になり、特にデバッグ情報や詳細な型情報が必要となる gcflags -lll
のようなコンパイルオプションを使用した場合に、ビルドエラーが発生していました。
このバグは、Goのコンパイラが内部的に型情報を管理し、パッケージ間の依存関係を解決するメカニズムの不備に起因していました。
前提知識の解説
Goコンパイラ (cmd/gc
)
Go言語の公式コンパイラは gc
と呼ばれ、Goツールチェーンの一部です。gc
は、Goのソースコードを機械語に変換するだけでなく、型チェック、最適化、そしてパッケージ間の依存関係の解決など、多くの役割を担っています。
エクスポートデータ (Export Data)
Goのパッケージシステムでは、あるパッケージが別のパッケージの型、関数、変数などを利用できるようにするために、そのパッケージが「エクスポート」する情報を定義します。このエクスポートされる情報は、コンパイル時にバイナリファイル(.a
ファイルなど)に埋め込まれる「エクスポートデータ」として保存されます。他のパッケージがそのパッケージをインポートする際、コンパイラはこのエクスポートデータを読み込み、型チェックやシンボルの解決を行います。エクスポートデータには、エクスポートされた型、関数シグネチャ、定数などの情報が含まれます。
インライン化 (Inlining)
インライン化はコンパイラの最適化手法の一つです。関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減するために、呼び出される関数の本体を呼び出し箇所に直接展開します。これにより、実行時のパフォーマンスが向上しますが、コンパイルされたコードのサイズが増加する可能性があります。Goコンパイラは、特定の条件(関数が小さい、ループを含まないなど)を満たす関数を自動的にインライン化します。
ローカル変数 (Local Variables)
関数内で宣言され、その関数のスコープ内でのみ有効な変数のことです。通常、ローカル変数は関数が終了すると破棄されます。
エクスポートされていない型 (Unexported Types)
Go言語では、識別子(変数名、関数名、型名など)の最初の文字が大文字で始まる場合、その識別子はパッケージ外に「エクスポート」され、他のパッケージから参照可能になります。一方、小文字で始まる識別子は「エクスポートされない(unexported)」と見なされ、その識別子が宣言されたパッケージ内でのみ参照可能です。このメカニズムは、Goのモジュール性とカプセル化を保証するために重要です。
gcflags -lll
gcflags
はGoコンパイラに渡すフラグを指定するためのオプションです。-lll
は、コンパイラが生成するデバッグ情報や、インライン化に関する詳細なログ出力を増やすためのフラグの一つです。このフラグを有効にすると、通常は隠蔽されているコンパイラの内部的な挙動がより詳細に可視化されるため、今回のようなエクスポートデータの不備が顕在化しやすくなります。
技術的詳細
Goコンパイラは、ソースコードを抽象構文木(AST)に変換し、その後、中間表現(IR)を経て最終的な機械語を生成します。この過程で、型情報やシンボル情報が管理されます。
問題の核心は、export.c
ファイル内の reexportdep
関数にありました。この関数は、エクスポートデータに含めるべき依存関係を再帰的に探索し、exportlist
に追加する役割を担っています。
従来の reexportdep
関数は、ODCL
(Declaration、宣言)ノードを処理する際に、ローカル変数の型がエクスポートデータに含めるべきかどうかを適切に判断できていませんでした。特に、インライン化された関数内で宣言されたローカル変数が、その変数が属するパッケージ内でのみ有効な(エクスポートされていない)型を参照している場合、コンパイラはその型がエクスポートデータに必要であると認識せず、結果としてエクスポートデータから欠落させていました。
この欠落は、通常は問題になりませんが、gcflags -lll
のような詳細な型情報やデバッグ情報を要求するビルドモードでは、コンパイラが不完全なエクスポートデータに基づいて処理を進めようとし、エラーを引き起こしました。
修正は、reexportdep
関数に ODCL
ケースを追加し、ローカル変数の型がエクスポートされていない型であるにもかかわらず、それが他のパッケージ(localpkg
や builtinpkg
ではない)に属するシンボルを持つ場合に、その型定義を exportlist
に追加するようにしました。これにより、インライン化されたコードが参照するエクスポートされていない型であっても、必要に応じてエクスポートデータに適切に含められるようになります。
テストケース bug467
は、この問題を具体的に再現しています。
p1.go
:SockaddrUnix
というエクスポートされていない型を定義します。p2.go
:p1
をインポートし、SockUnix
関数内で*p1.SockaddrUnix
型のローカル変数を使用します。SockUnix
関数はインライン化の対象となりうる小さな関数です。p3.go
:p2
をインポートし、p2.SockUnix()
を呼び出します。
この構成により、p3
をコンパイルする際に p2
のエクスポートデータが必要となり、その中で p1.SockaddrUnix
の型情報が欠落しているとエラーが発生する、というシナリオが再現されます。
コアとなるコードの変更箇所
変更は src/cmd/gc/export.c
ファイルの reexportdep
関数内に追加された case ODCL:
ブロックです。
--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -119,6 +119,17 @@ reexportdep(Node *n)
}
break;
+ case ODCL:
+ // Local variables in the bodies need their type.
+ t = n->left->type;
+ if(t != types[t->etype] && t != idealbool && t != idealstring) {
+ if(isptr[t->etype])
+ t = t->type;
+ if (t && t->sym && t->sym->def && t->sym->pkg != localpkg && t->sym->pkg != builtinpkg) {
+ exportlist = list(exportlist, t->sym->def);
+ }
+ }
+ break;
case OLITERAL:
t = n->type;
コアとなるコードの解説
追加された ODCL
(Declaration) ケースは、ローカル変数の宣言ノードを処理します。
t = n->left->type;
: 宣言ノードn
の左の子(通常は宣言される変数)の型を取得し、t
に代入します。if(t != types[t->etype] && t != idealbool && t != idealstring) { ... }
:- これは、型
t
が基本的な組み込み型(int
,bool
,string
など)ではないことを確認するための条件です。types[t->etype]
は、t
の基本型に対応する組み込み型を指します。idealbool
やidealstring
は、型推論前のリテラルの型を表します。つまり、この条件は、カスタム型やポインタ型など、より複雑な型を対象とすることを示しています。
- これは、型
if(isptr[t->etype]) t = t->type;
:- もし型
t
がポインタ型である場合(isptr[t->etype]
が真)、そのポインタが指す基底の型(t->type
)にt
を更新します。これは、ポインタ型そのものではなく、ポインタが指す実体の型がエクスポートデータに必要となるためです。
- もし型
if (t && t->sym && t->sym->def && t->sym->pkg != localpkg && t->sym->pkg != builtinpkg) { ... }
:- この条件が最も重要です。
t
: 型が有効であること。t->sym
: 型にシンボルが関連付けられていること。t->sym->def
: シンボルに定義があること。t->sym->pkg != localpkg
: 型が現在のパッケージ(localpkg
)で定義されたものではないこと。つまり、他のパッケージからインポートされた型であること。t->sym->pkg != builtinpkg
: 型が組み込みパッケージ(builtinpkg
)のものではないこと。
- これらの条件がすべて真である場合、つまり、ローカル変数の型が他のパッケージからインポートされたカスタム型であり、かつその型がエクスポートデータに含めるべき定義を持っている場合に、以下の処理を実行します。
- この条件が最も重要です。
exportlist = list(exportlist, t->sym->def);
:t->sym->def
(型の定義シンボル)をexportlist
に追加します。exportlist
は、最終的にエクスポートデータに書き込まれるべきシンボルのリストです。
この変更により、インライン化された関数内のローカル変数が、たとえエクスポートされていない型であっても、それが他のパッケージからインポートされたものであるならば、その型情報が適切にエクスポートデータに含まれるようになります。これにより、コンパイラが完全な型情報に基づいて処理を進めることができ、gcflags -lll
を使用したビルドエラーが解消されます。
関連リンク
- Go Code Review: https://golang.org/cl/6813067
参考にした情報源リンク
- Go言語の公式ドキュメント (Go言語の仕様、パッケージ、コンパイラに関する一般的な情報)
- Goコンパイラのソースコード (特に
src/cmd/gc
ディレクトリ内のファイル) - Go言語のインライン化に関するブログ記事や議論 (例: GoのIssueトラッカーやメーリングリスト)
- Go言語の型システムとエクスポートルールに関する解説記事
- Goのコンパイラフラグ
gcflags
に関するドキュメントや情報