Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14585] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)におけるシンボルエクスポートの挙動を修正し、不要なプライベートシンボルがエクスポートされるのを防ぐものです。特に、Go言語の組み込み(builtin)シンボルがユーザーコードによって再宣言された場合に、その再宣言されたシンボルが正しく扱われ、エクスポートデータが破損しないようにするための変更が含まれています。これにより、コンパイラの効率性が向上し、エクスポートされるメタデータの正確性が保証されます。

コミット

commit 561edbd63ceb0b1c0177051a0c151c68278219d6
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sat Dec 8 14:43:00 2012 +0100

    cmd/gc: do not export useless private symbols.
    
    Fixes #4252.
    
    R=rsc, golang-dev, mirtchovski, daniel.morsing, dave, lvd
    CC=golang-dev
    https://golang.org/cl/6856126

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/561edbd63ceb0b1c0177051a0c151c68278219d6

元コミット内容

    cmd/gc: do not export useless private symbols.
    
    Fixes #4252.

このコミットの目的は、Goコンパイラ(cmd/gc)が「役に立たないプライベートシンボル」をエクスポートしないようにすることです。これは、Go言語のIssue #4252を修正するものです。

変更の背景

Go言語では、パッケージ間でシンボル(変数、関数、型など)を共有するためにエクスポートメカニズムを使用します。エクスポートされるシンボルは、その名前が大文字で始まるものに限られます。しかし、コンパイラが内部的に処理するシンボルの中には、外部に公開する必要のないプライベートなものや、Goの組み込み関数や型(len, true, false, nilなど)がユーザーコードによって再宣言された場合に、その再宣言されたシンボルがエクスポートデータに誤って含まれてしまう問題がありました。

Issue #4252は、まさにこの問題、特に組み込みシンボルがユーザーコードで再宣言された際に、コンパイラがそれらを適切に処理せず、エクスポートデータが破損したり、予期せぬ挙動を引き起こしたりする可能性を指摘していました。例えば、あるパッケージがtrueという名前の変数を定義した場合、それがGoの組み込みのtrueと混同され、エクスポートデータに不正確な情報が含まれることが考えられます。

このコミットは、このような不要な、あるいは誤ったシンボルのエクスポートを防ぎ、コンパイラが生成するエクスポートデータ(Goのパッケージバイナリ形式である.aファイルなどに含まれる)の正確性と効率性を向上させることを目的としています。これにより、コンパイルされたパッケージのサイズが削減され、リンケージ時の問題が回避されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。

  • Go言語のパッケージとエクスポート: Go言語では、コードはパッケージに分割されます。パッケージ内のシンボルは、その名前が大文字で始まる場合にのみ、他のパッケージから参照可能(エクスポートされる)になります。小文字で始まるシンボルはパッケージプライベートです。
  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラです。ソースコードを解析し、中間表現を生成し、最終的に実行可能なバイナリやパッケージアーカイブ(.aファイル)を生成します。
  • シンボルテーブル: コンパイラがプログラム内のすべての識別子(変数名、関数名、型名など)とその属性(型、スコープ、メモリ位置など)を管理するために使用するデータ構造です。
  • エクスポートデータ: コンパイラがパッケージをコンパイルする際に生成するメタデータの一部で、そのパッケージが外部に公開するシンボルに関する情報が含まれます。他のパッケージがそのパッケージをインポートする際に、このエクスポートデータを読み込んで、利用可能なシンボルを認識します。
  • 組み込み(Builtin)シンボル: Go言語に最初から定義されている関数、型、定数などです(例: len, cap, make, new, true, false, nil, int, stringなど)。これらは特別な扱いを受け、通常はどのパッケージからも明示的にインポートすることなく利用できます。
  • シンボルの再宣言: Go言語では、組み込みシンボルと同じ名前の変数や関数をユーザーが定義することが可能です。この場合、そのスコープ内ではユーザー定義のシンボルが優先されます。コンパイラは、このような再宣言を正しく処理し、組み込みシンボルとユーザー定義シンボルを区別する必要があります。
  • Sym構造体: Goコンパイラの内部でシンボルを表すデータ構造です。これにはシンボルの名前、パッケージ、フラグ(エクスポートされているか、パッケージプライベートかなど)が含まれます。
  • Node構造体: Goコンパイラの内部で抽象構文木(AST)のノードを表すデータ構造です。シンボルへの参照を含むことがあります。

技術的詳細

このコミットの主要な変更点は、src/cmd/gc/export.csrc/cmd/gc/lex.cの2つのファイルに集中しています。

src/cmd/gc/export.cの変更

このファイルは、Goコンパイラがシンボルをエクスポートするロジックを扱います。

  1. packagesym関数の削除: 以前は、エクスポートされないパッケージローカルなシンボルをマークするためのpackagesym関数が存在しました。このコミットでは、この関数が削除され、autoexport関数内の関連する呼び出しも削除されています。これは、シンボルがエクスポートされるかどうかを判断するロジックがより洗練されたため、明示的にパッケージローカルとしてマークする必要がなくなったことを示唆しています。
  2. exportedsym関数の導入: 新たにexportedsym(Sym *sym)関数が導入されました。この関数は、与えられたシンボルが現在のパッケージをインポートする他のファイルから可視である(エクスポートされる)かどうかを判断します。
    • 組み込みパッケージ(builtinpkg)のシンボルは常にエクスポートされると見なされます。
    • 現在のローカルパッケージ(localpkg)に属し、かつその名前がエクスポート可能(大文字で始まる)である場合にエクスポートされると判断します。
    • この関数は、シンボルが実際にエクスポートされるべきかどうかを判断する中心的なロジックを提供します。
  3. reexportdep関数の修正: reexportdep関数は、エクスポートされるシンボルが依存する他のシンボル(例えば、エクスポートされる型のフィールドの型など)もエクスポートリストに追加する役割を担っています。この関数内の複数の箇所で、以前はn->sym->pkg != localpkg && n->sym->pkg != builtinpkgのような条件で外部パッケージのシンボルをチェックしていた部分が、新しく導入された!exportedsym(n->sym)に置き換えられました。
    • これは、シンボルが外部パッケージのものであるかどうかだけでなく、「そのシンボルがエクスポートされるべきではないプライベートシンボルである」というより一般的な条件で依存関係をチェックするように変更されたことを意味します。
    • 特に、ユーザーが組み込みシンボルを再宣言した場合、その再宣言されたシンボルはlocalpkgに属しますが、exportedsymはそれがエクスポートされるべきではないと判断するため、エクスポートリストに誤って追加されることを防ぎます。

src/cmd/gc/lex.cの変更

このファイルは、Goコンパイラの字句解析(lexical analysis)とシンボル初期化の段階を扱います。

  1. 組み込みシンボルのorigpkg設定: lexfini関数内で、Goの組み込みシンボル(true, false, nil, byte, error, rune, iotaなど)が初期化される際に、それらのSym構造体のorigpkgフィールドがbuiltinpkgに設定されるようになりました。
    • origpkgは、シンボルが元々どのパッケージで定義されたかを示すフィールドです。
    • この変更により、ユーザーがこれらの組み込みシンボルと同じ名前のシンボルを再宣言した場合でも、コンパイラは元の組み込みシンボルがbuiltinpkgに属することを正確に認識できるようになります。
    • これにより、exportedsym関数が組み込みシンボルとユーザー定義の同名シンボルを区別し、エクスポートの判断を正しく行えるようになります。

テストファイルの追加

test/fixedbugs/issue4252.dir/ディレクトリに、a.gomain.goという2つの新しいテストファイルが追加されました。

  • a.goは、true, false, nil, append, error, int, lenといったGoの組み込みシンボルをパッケージa内で再宣言しています。そして、これらの再宣言されたシンボルが正しく動作することを確認するテストロジックを含んでいます。
  • main.goは、パッケージaをインポートし、a.InlinedFakeTrue(), a.InlinedFakeFalse(), a.InlinedFakeNil()といった関数を呼び出しています。これらの関数は、パッケージa内で再宣言された組み込みシンボルを使用しており、main.goはそれらが組み込みシンボルではなく、パッケージa内で定義されたシンボルとして扱われていることを検証します。
  • test/fixedbugs/issue4252.goは、これらのテストを実行するためのラッパーファイルです。

これらのテストは、このコミットが解決しようとしている問題(組み込みシンボルの再宣言とエクスポートデータの破損)が実際に修正されたことを検証するために非常に重要です。

コアとなるコードの変更箇所

src/cmd/gc/export.c

--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -22,22 +22,8 @@ exportsym(Node *n)
 	}
 	n->sym->flags |= SymExport;
 
-\texportlist = list(exportlist, n);\n-}\n-\n-// Mark n's symbol as package-local\n-static void\n-packagesym(Node *n)\n-{\n-\tif(n == N || n->sym == S)\n-\t\treturn;\n-\tif(n->sym->flags & (SymExport|SymPackage)) {\n-\t\tif(n->sym->flags & SymExport)\n-\t\t\tyyerror("export/package mismatch: %S", n->sym);\n-\t\treturn;\n-\t}\n-\tn->sym->flags |= SymPackage;\n-\n+\tif(debug['E'])\n+\t\tprint("export symbol %S\\n", n->sym);\n \texportlist = list(exportlist, n);\n }\n \n@@ -58,6 +44,18 @@ initname(char *s)\n 	return strcmp(s, "init") == 0;\n }\n \n+// exportedsym returns whether a symbol will be visible\n+// to files that import our package.\n+static int\n+exportedsym(Sym *sym)\n+{\n+\t// Builtins are visible everywhere.\n+\tif(sym->pkg == builtinpkg || sym->origpkg == builtinpkg)\n+\t\treturn 1;\n+\n+\treturn sym->pkg == localpkg && exportname(sym->name);\n+}\n+\n void\n autoexport(Node *n, int ctxt)\n {\n@@ -69,8 +67,6 @@ autoexport(Node *n, int ctxt)\n 		return;\n 	if(exportname(n->sym->name) || initname(n->sym->name))\n 		exportsym(n);\n-\telse\n-\t\tpackagesym(n);\n }\n \n static void
@@ -104,17 +100,17 @@ reexportdep(Node *n)\n 	if(!n)\n 		return;\n \n-//	print("reexportdep %+hN\\n", n);\n+\t//print("reexportdep %+hN\\n", n);\n 	switch(n->op) {\n 	case ONAME:\n 		switch(n->class&~PHEAP) {\n 		case PFUNC:\n 			// methods will be printed along with their type\n-\t\t\tif(!n->type || n->type->thistuple > 0)\n+\t\t\tif(n->left && n->left->op == OTYPE)\n 				break;\n 			// fallthrough\n 		case PEXTERN:\n-\t\t\tif (n->sym && n->sym->pkg != localpkg && n->sym->pkg != builtinpkg)\n+\t\t\tif(n->sym && !exportedsym(n->sym))\n 				exportlist = list(exportlist, n);\n 		}\n 		break;\n@@ -125,7 +121,7 @@ reexportdep(Node *n)\n 		if(t != types[t->etype] && t != idealbool && t != idealstring) {\n 			if(isptr[t->etype])\n 				t = t->type;\n-\t\t\tif (t && t->sym && t->sym->def && t->sym->pkg != localpkg  && t->sym->pkg != builtinpkg) {\n+\t\t\tif(t && t->sym && t->sym->def && !exportedsym(t->sym)) {\n 				exportlist = list(exportlist, t->sym->def);\n 			}\n 		}\n@@ -136,15 +132,19 @@ reexportdep(Node *n)\n 		if(t != types[n->type->etype] && t != idealbool && t != idealstring) {\n 			if(isptr[t->etype])\n 				t = t->type;\n-\t\t\tif (t && t->sym && t->sym->def && t->sym->pkg != localpkg  && t->sym->pkg != builtinpkg) {\n-//\t\t\t\tprint("reexport literal type %+hN\\n", t->sym);\n+\t\t\tif(t && t->sym && t->sym->def && !exportedsym(t->sym)) {\n+\t\t\t\tif(debug['E'])\n+\t\t\t\t\tprint("reexport literal type %S\\n", t->sym);\n 				exportlist = list(exportlist, t->sym->def);\n 			}\n 		}\n 		// fallthrough\n 	case OTYPE:\n-\t\tif (n->sym && n->sym->pkg != localpkg && n->sym->pkg != builtinpkg)\n+\t\tif(n->sym && !exportedsym(n->sym)) {\n+\t\t\tif(debug['E'])\n+\t\t\t\tprint("reexport literal/type %S\\n", n->sym);\n 				exportlist = list(exportlist, n);\n+\t\t}\n 		break;\n 
 	// for operations that need a type when rendered, put the type on the export list.
@@ -158,8 +158,9 @@ reexportdep(Node *n)\n 		t = n->type;\n 		if(!t->sym && t->type)\n 			t = t->type;\n-\t\tif (t && t->sym && t->sym->def && t->sym->pkg != localpkg  && t->sym->pkg != builtinpkg) {\n-//\t\t\tprint("reexport convnop %+hN\\n", t->sym->def);\n+\t\tif(t && t->sym && t->sym->def && !exportedsym(t->sym)) {\n+\t\t\tif(debug['E'])\n+\t\t\t\tprint("reexport type for convnop %S\\n", t->sym);\n 				exportlist = list(exportlist, t->sym->def);\n 		}\n 		break;

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -2016,8 +2016,10 @@ lexfini(void)\n 		s->lexical = lex;\n 
 		etype = syms[i].etype;\n-\t\tif(etype != Txxx && (etype != TANY || debug['A']) && s->def == N)\n+\t\tif(etype != Txxx && (etype != TANY || debug['A']) && s->def == N) {\n \t\t\ts->def = typenod(types[etype]);\n+\t\t\ts->origpkg = builtinpkg;\n+\t\t}\n 
 		etype = syms[i].op;\n-\t\tif(etype != OXXX && s->def == N) {\n+\t\tif(etype != OXXX && s->def == N) {\n \t\t\ts->def = nod(OXXX, N, N);\n \t\t\ts->def->sym = s;\n \t\t\ts->def->etype = etype;\n+\t\t\ts->origpkg = builtinpkg;\n \t\t}\n \t}\n \n+\t// backend-specific builtin types (e.g. int).\n \tfor(i=0; typedefs[i].name; i++) {\n \t\ts = lookup(typedefs[i].name);\n-\t\tif(s->def == N)\n+\t\tif(s->def == N) {\n \t\t\ts->def = typenod(types[typedefs[i].etype]);\n+\t\t\ts->origpkg = builtinpkg;\n+\t\t}\n \t}\n \n 	// there's only so much table-driven we can handle.\n 	// these are special cases.\n \ts = lookup("byte");\n-\tif(s->def == N)\n+\tif(s->def == N) {\n \t\ts->def = typenod(bytetype);\n-\t\n+\t\ts->origpkg = builtinpkg;\n+\t}\n+\n \ts = lookup("error");\n-\tif(s->def == N)\n+\tif(s->def == N) {\n \t\ts->def = typenod(errortype);\n+\t\ts->origpkg = builtinpkg;\n+\t}\n \n \ts = lookup("rune");\n-\tif(s->def == N)\n+\tif(s->def == N) {\n \t\ts->def = typenod(runetype);\n+\t\ts->origpkg = builtinpkg;\n+\t}\n \n \ts = lookup("nil");\n \tif(s->def == N) {\n \t\tv.ctype = CTNIL;\n \t\ts->def = nodlit(v);\n \t\ts->def->sym = s;\n+\t\ts->origpkg = builtinpkg;\n \t}\n-\t\n+\n \ts = lookup("iota");\n \tif(s->def == N) {\n \t\ts->def = nod(OIOTA, N, N);\n \t\ts->def->sym = s;\n+\t\ts->origpkg = builtinpkg;\n \t}\n \n \ts = lookup("true");\n \tif(s->def == N) {\n \t\ts->def = nodbool(1);\n \t\ts->def->sym = s;\n+\t\ts->origpkg = builtinpkg;\n \t}\n \n \ts = lookup("false");\n \tif(s->def == N) {\n \t\ts->def = nodbool(0);\n \t\ts->def->sym = s;\n+\t\ts->origpkg = builtinpkg;\n \t}\n-\t\n+\n \tnodfp = nod(ONAME, N, N);\n \tnodfp->type = types[TINT32];\n \tnodfp->xoffset = 0;

コアとなるコードの解説

src/cmd/gc/export.c

  • packagesymの削除: 以前は、パッケージ内部のシンボルを明示的に「パッケージローカル」としてマークするpackagesym関数がありました。しかし、新しいexportedsym関数が導入されたことで、シンボルがエクスポートされるべきかどうかをより包括的に判断できるようになり、この明示的なマーキングが不要になりました。これにより、コードの複雑さが軽減されています。
  • exportedsymの導入: この関数は、シンボルが外部に公開されるべきかどうかを判断する新しい中心的なロジックです。
    • sym->pkg == builtinpkg || sym->origpkg == builtinpkg: この条件は非常に重要です。builtinpkgはGoの組み込みシンボルが属する仮想的なパッケージです。sym->pkgbuiltinpkgであるか、またはsym->origpkg(シンボルが元々定義されたパッケージ)がbuiltinpkgである場合、そのシンボルは常にエクスポートされると見なされます。これは、組み込みシンボルがどのパッケージからでも利用可能であるというGoの言語仕様を反映しています。
    • sym->pkg == localpkg && exportname(sym->name): この条件は、シンボルが現在のローカルパッケージに属し、かつその名前がGoのエクスポート規則(大文字で始まる)に従っている場合にエクスポートされることを意味します。
    • この新しい関数により、reexportdepなどの他の関数は、シンボルがエクスポートされるべきかどうかを判断する際に、より正確で一貫したロジックを使用できるようになりました。特に、ユーザーが組み込みシンボルを再宣言した場合、その再宣言されたシンボルはlocalpkgに属し、かつ名前が小文字で始まるため(例: var true = ...)、exportname(sym->name)falseを返し、結果としてexportedsymfalseを返します。これにより、再宣言されたプライベートな組み込みシンボルが誤ってエクスポートデータに含まれることを防ぎます。
  • reexportdepの修正: reexportdep関数は、エクスポートされるシンボルが依存する他のシンボルもエクスポートリストに追加する役割を担っています。この関数内の複数の箇所で、以前のn->sym->pkg != localpkg && n->sym->pkg != builtinpkgという条件が!exportedsym(n->sym)に置き換えられました。これは、依存するシンボルが「外部パッケージのシンボルである」という単純な条件から、「エクスポートされるべきではないプライベートシンボルである」というより正確な条件に変わったことを意味します。これにより、エクスポートデータに不要な依存関係が含まれることを防ぎます。

src/cmd/gc/lex.c

  • 組み込みシンボルのorigpkg設定: lexfini関数は、コンパイラの初期化段階で組み込みシンボルをセットアップします。このコミットでは、true, false, nil, byte, error, rune, iotaなどの組み込みシンボルが初期化される際に、そのSym構造体のorigpkgフィールドがbuiltinpkgに明示的に設定されるようになりました。
    • この変更は、export.cexportedsym関数と連携して機能します。exportedsymsym->origpkg == builtinpkgをチェックすることで、シンボルが組み込みのものであるかどうかを正確に判断できます。
    • これにより、ユーザーがtruefalseといった組み込みシンボルと同じ名前のローカル変数や定数を定義した場合でも、コンパイラはそれらを組み込みシンボルとは異なるものとして扱い、エクスポートデータに誤って含めることを防ぐことができます。これは、Go言語が組み込みシンボルの再宣言を許可しているため、コンパイラがこのセマンティクスを正しく処理するために不可欠な変更です。

これらの変更により、Goコンパイラはエクスポートされるシンボルをより厳密に管理し、不要なプライベートシンボルや、ユーザーによって再宣言された組み込みシンボルがエクスポートデータに誤って含まれることを防ぎます。これにより、コンパイルされたパッケージの正確性、効率性、および互換性が向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(パッケージ、エクスポート、組み込み関数などに関するセクション)
  • Goコンパイラのソースコード(src/cmd/gcディレクトリ内のファイル)
  • Go言語のIssueトラッカー(特にIssue #4252の議論)