[インデックス 10876] ファイルの概要
このコミットは、Goコンパイラ(gc
)の内部定義において、runtime
パッケージの定義からunsafe
パッケージへの依存を取り除くことを目的としています。具体的には、メモリ比較を行うmemequal
系の組み込み関数の引数型をunsafe.Pointer
から*any
(当時のinterface{}
)に変更し、それに伴うコンパイラ内部の処理を修正しています。
変更されたファイルは以下の通りです。
src/cmd/gc/builtin.c.boot
: コンパイラが起動時に読み込む組み込み関数の定義ファイル。src/cmd/gc/runtime.go
:runtime
パッケージのGo言語側の定義ファイル。src/cmd/gc/subr.c
: コンパイラのサブルーチン(補助関数)が定義されているC言語ファイル。主に型チェックやコード生成に関連する部分。src/cmd/gc/walk.c
: コンパイラのAST(抽象構文木)ウォーク処理が定義されているC言語ファイル。主に比較演算子の処理に関連する部分。
コミット
- コミットハッシュ:
1d0f93b4be68263ec7e07255e8fe20e1168c9bba
- 作者: Russ Cox (
rsc@golang.org
) - 日付: Mon Dec 19 15:52:15 2011 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d0f93b4be68263ec7e07255e8fe20e1168c9bba
元コミット内容
gc: avoid unsafe in defn of package runtime
Keeps -u tracking simple.
R=ken2
CC=golang-dev
https://golang.org/cl/5495094
変更の背景
この変更の主な背景は、Goコンパイラがunsafe
パッケージの使用状況を追跡するメカニズム(コミットメッセージにある「-u
tracking」)を簡素化することにあります。
Go言語のunsafe
パッケージは、Goの型システムを迂回してメモリを直接操作する機能を提供します。これは非常に強力ですが、同時に型安全性を損ない、予期せぬバグやセキュリティ脆弱性を引き起こす可能性があります。そのため、Goツールチェインはunsafe
パッケージの使用を厳密に監視し、特定の条件下で警告を発したり、ビルドを失敗させたりすることがあります。
runtime
パッケージはGoプログラムの実行を支える基盤であり、ガベージコレクション、スケジューリング、プリミティブな型操作など、低レベルな処理を多数含んでいます。歴史的に、runtime
パッケージの内部実装ではunsafe
パッケージが多用されてきました。しかし、runtime
パッケージの定義自体がunsafe
パッケージに依存していると、コンパイラがunsafe
の使用状況を追跡する際に複雑さが増します。
このコミット以前は、runtime
パッケージの組み込み関数(特にmemequal
系)の定義にunsafe.Pointer
が直接使用されていました。これにより、コンパイラはruntime
パッケージの定義を処理する際にunsafe
パッケージの存在を考慮する必要があり、-u
フラグ(またはそれに類するunsafe
使用状況の追跡機能)の実装が複雑になっていました。
この変更は、runtime
パッケージの定義からunsafe
パッケージへの直接的な依存を取り除くことで、コンパイラがunsafe
の使用状況をよりシンプルに追跡できるようにすることを目的としています。これにより、コンパイラの内部構造が整理され、将来的なメンテナンスや機能追加が容易になります。
前提知識の解説
Go言語のunsafe
パッケージとunsafe.Pointer
unsafe
パッケージは、Go言語の型安全性を意図的にバイパスするための機能を提供します。その中でもunsafe.Pointer
は、任意の型のポインタと相互変換可能な特殊なポインタ型です。これにより、プログラマは型システムが通常許可しないメモリ操作(例: 異なる型のポインタ間の変換、ポインタ演算)を行うことができます。
unsafe.Pointer
の主な用途:- C言語とのFFI (Foreign Function Interface)
- 低レベルなシステムプログラミング(例: OSカーネルとのインタラクション)
- パフォーマンスがクリティカルなコードでのメモリレイアウトの最適化
- 危険性:
unsafe.Pointer
を使用すると、Goのメモリモデルやガベージコレクタの保証が破られる可能性があり、クラッシュ、データ破損、セキュリティ脆弱性につながることがあります。そのため、unsafe
パッケージの使用は極力避け、必要な場合にのみ慎重に行うべきとされています。
Go言語のruntime
パッケージ
runtime
パッケージは、Goプログラムの実行環境を構成する中核的なライブラリです。Go言語で書かれたプログラムは、このruntime
パッケージが提供する機能の上に成り立っています。主な機能には以下のようなものがあります。
- ガベージコレクション (GC): 不要になったメモリを自動的に解放する。
- ゴルーチンとスケジューラ: 軽量な並行処理単位であるゴルーチンを管理し、CPUコアに効率的に割り当てる。
- メモリ管理: ヒープメモリの割り当てと解放。
- システムコール: OSとのインタラクション。
- プリミティブな型操作: スライス、マップ、チャネルなどの組み込み型の低レベルな操作。
- パニックとリカバリ: エラーハンドリングメカニズム。
runtime
パッケージの多くの部分はGo言語で書かれていますが、パフォーマンスやOSとの連携のためにC言語やアセンブリ言語で書かれた部分も含まれています。
Goコンパイラ (gc
) の役割
gc
は、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。コンパイラは、ソースコードの解析、抽象構文木(AST)の構築、型チェック、最適化、コード生成など、複数のフェーズを経て実行可能ファイルを生成します。
このコミットで変更されているファイル(builtin.c.boot
, runtime.go
, subr.c
, walk.c
)は、いずれもgc
コンパイラの内部実装の一部です。
builtin.c.boot
: コンパイラがGoプログラムをコンパイルする際に、runtime
パッケージの特定の関数や型を「組み込み」として認識させるための定義が含まれています。これは、runtime
パッケージがGo言語の他の部分とは異なる特別な扱いを受けるためです。subr.c
やwalk.c
: コンパイラのバックエンドに近い部分で、ASTの走査やコード生成の際に、特定の操作(例: 構造体の比較、メモリのコピー)をどのように実装するかを決定します。
any
型(当時のinterface{}
型)の役割
Go 1.18より前のバージョンでは、any
型はinterface{}
として知られていました。これは、任意の型の値を保持できる空のインターフェース型です。interface{}
型の変数は、内部的に値の型情報と値そのもの(または値へのポインタ)を保持する構造体として表現されます。
このコミットでは、unsafe.Pointer
の代わりに*any
(つまり*interface{}
)を使用することで、型安全なポインタとして扱いつつ、任意の型のデータへのポインタを抽象的に表現しようとしています。これにより、コンパイラはunsafe
パッケージの特別な処理を必要とせず、通常の型システムの一部としてこれらのポインタを扱うことができます。
-u
フラグまたはunsafe
使用状況の追跡
Goコンパイラには、unsafe
パッケージの使用状況を追跡し、特定のルールに違反した場合に警告やエラーを出すための内部メカニズムが存在します。これは、Goの型安全性を維持し、unsafe
パッケージの誤用を防ぐための重要な機能です。
コミットメッセージの「Keeps -u
tracking simple」は、このunsafe
使用状況の追跡ロジックを簡素化することを指しています。runtime
パッケージの定義自体がunsafe.Pointer
に依存していると、コンパイラはruntime
の特殊性を考慮しながらunsafe
の使用を追跡する必要があり、そのロジックが複雑になります。unsafe.Pointer
を*any
に置き換えることで、runtime
の定義が通常のGoの型システムに沿った形になり、追跡ロジックがより直接的になります。
技術的詳細
このコミットの技術的な核心は、Goコンパイラがruntime
パッケージの組み込み関数、特にmemequal
(メモリ領域の比較)系の関数をどのように扱うかを変更することにあります。
以前は、これらの関数は以下のようなシグネチャを持っていました(簡略化)。
func memequal(eq *bool, size uintptr, x, y unsafe.Pointer)
ここで、x
とy
は比較対象のメモリ領域へのポインタであり、unsafe.Pointer
型でした。これは、Goの型システムでは異なる型のメモリ領域を直接比較することができないため、unsafe
パッケージの力を借りて型チェックを回避していました。
このコミットでは、これらの関数のシグネチャを以下のように変更します。
func memequal(eq *bool, size uintptr, x, y *any)
unsafe.Pointer
が*any
(*interface{}
)に置き換えられています。この変更は、コンパイラ内部で以下のような影響をもたらします。
- 型安全性の向上(コンパイラ内部の視点):
unsafe.Pointer
はコンパイラにとって「何でもあり」の型であり、特別な処理が必要でした。*any
はGoの通常の型システムの一部であるため、コンパイラはこれらのポインタをより標準的な方法で型チェックし、処理することができます。これにより、コンパイラの内部ロジックが簡素化されます。 unsafe
パッケージ依存の排除:runtime
パッケージの定義からunsafe
パッケージへの直接的なimport
が不要になります。これは、runtime
パッケージがGoの基盤であるため、その定義が可能な限りクリーンで、特殊な依存関係を持たないことが望ましいという設計思想に基づいています。-u
トラッキングの簡素化: コンパイラがunsafe
の使用を追跡する際、runtime
パッケージの組み込み関数がunsafe.Pointer
を使用しているという特殊なケースを考慮する必要がなくなります。これにより、unsafe
使用状況の追跡ロジックがよりシンプルになり、バグの発生リスクが低減します。- コンパイラ内部のAST変換と型チェックの調整:
src/cmd/gc/subr.c
とsrc/cmd/gc/walk.c
の変更は、このシグネチャ変更に対応するためのものです。eqmemfunc
関数は、memequal
系の関数を呼び出す際に、引数の型をunsafe.Pointer
から*any
に適切に変換するように変更されます。eqmem
関数やwalkcompare
関数では、メモリ比較を行う際に、引数として渡されるポインタがunsafe.Pointer
にキャストされるのではなく、直接*any
として扱われるようになります。これには、typecheck
の呼び出しや、引数をリストに追加する際の処理の変更が含まれます。
この変更は、Go言語の設計哲学である「シンプルさ」と「型安全性」をコンパイラの内部構造にも適用しようとする試みの一環と言えます。unsafe
パッケージは必要悪として存在しますが、その使用は最小限に抑え、特にGoのコア部分であるruntime
パッケージの定義からは可能な限り排除するという方向性を示しています。
コアとなるコードの変更箇所
src/cmd/gc/builtin.c.boot
--- a/src/cmd/gc/builtin.c.boot
+++ b/src/cmd/gc/builtin.c.boot
@@ -1,7 +1,6 @@
char *runtimeimport =
"package runtime\\n"\
"import runtime \\\"runtime\\\"\\n"\
-" import unsafe \\\"unsafe\\\"\\n"\
"func @\\\"\\\".new(@\\\"\\\".typ *byte) *any\\n"\
"func @\\\"\\\".panicindex()\\n"\
"func @\\\"\\\".panicslice()\\n"\
@@ -91,12 +90,12 @@ char *runtimeimport =
"func @\\\"\\\".sliceslice(@\\\"\\\".old []any, @\\\"\\\".lb uint64, @\\\"\\\".hb uint64, @\\\"\\\".width uint64) []any\\n"\
"func @\\\"\\\".slicearray(@\\\"\\\".old *any, @\\\"\\\".nel uint64, @\\\"\\\".lb uint64, @\\\"\\\".hb uint64, @\\\"\\\".width uint64) []any\\n"\
"func @\\\"\\\".closure()\\n"\
-" func @\\\"\\\".memequal(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-" func @\\\"\\\".memequal8(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-" func @\\\"\\\".memequal16(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-" func @\\\"\\\".memequal32(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-" func @\\\"\\\".memequal64(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-" func @\\\"\\\".memequal128(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
+" func @\\\"\\\".memequal(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+" func @\\\"\\\".memequal8(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+" func @\\\"\\\".memequal16(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+" func @\\\"\\\".memequal32(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+" func @\\\"\\\".memequal64(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+" func @\\\"\\\".memequal128(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
"func @\\\"\\\".int64div(? int64, ? int64) int64\\n"\
"func @\\\"\\\".uint64div(? uint64, ? uint64) uint64\\n"\
"func @\\\"\\\".int64mod(? int64, ? int64) int64\\n"\
runtimeimport
文字列からimport unsafe \"unsafe\"\\n
の行が削除され、memequal
系の関数の引数型が@\"unsafe\".Pointer
から*any
に変更されています。これは、コンパイラがruntime
パッケージの組み込み関数を認識する際の定義を変更するものです。
src/cmd/gc/runtime.go
--- a/src/cmd/gc/runtime.go
+++ b/src/cmd/gc/runtime.go
@@ -8,8 +8,6 @@
package PACKAGE
-import "unsafe"
-
// emitted by compiler, not referred to by go programs
func new(typ *byte) *any
@@ -123,12 +121,12 @@ func slicearray(old *any, nel uint64, lb uint64, hb uint64, width uint64) (ary [\
func closure() // has args, but compiler fills in
-func memequal(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal8(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal16(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal32(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal64(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal128(eq *bool, size uintptr, x, y unsafe.Pointer)
+func memequal(eq *bool, size uintptr, x, y *any)
+func memequal8(eq *bool, size uintptr, x, y *any)
+func memequal16(eq *bool, size uintptr, x, y *any)
+func memequal32(eq *bool, size uintptr, x, y *any)
+func memequal64(eq *bool, size uintptr, x, y *any)
+func memequal128(eq *bool, size uintptr, x, y *any)
// only used on 32-bit
func int64div(int64, int64) int64
import "unsafe"
の行が削除され、memequal
系の関数のシグネチャがunsafe.Pointer
から*any
に変更されています。これは、runtime
パッケージのGo言語側の定義からunsafe
への直接的な依存を取り除くものです。
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2636,20 +2636,27 @@ eqfield(Node *p, Node *q, Node *field, Node *eq)\n }\n \n static Node*\n-eqmemfunc(vlong size)\n+eqmemfunc(vlong size, Type *type)\n {\n char buf[30];\n+\tNode *fn;\n \n switch(size) {\n+\tdefault:\n+\t\tfn = syslook(\"memequal\", 1);\n+\t\tbreak;\n case 1:\n case 2:\n case 4:\n case 8:\n case 16:\n snprint(buf, sizeof buf, \"memequal%d\", (int)size*8);\n-\t\treturn syslook(buf, 0);\n+\t\tfn = syslook(buf, 1);\n+\t\tbreak;\n }\n-\treturn syslook(\"memequal\", 0);\n+\targtype(fn, type);\n+\targtype(fn, type);\n+\treturn fn;\n }\n \n // Return node for\n@@ -2663,12 +2670,14 @@ eqmem(Node *p, Node *q, Node *field, vlong size, Node *eq)\n nx->etype = 1; // does not escape\n ny = nod(OADDR, nod(OXDOT, q, field), N);\n ny->etype = 1; // does not escape\n+\ttypecheck(&nx, Erv);\n+\ttypecheck(&ny, Erv);\
\n-\tcall = nod(OCALL, eqmemfunc(size), N);\n+\tcall = nod(OCALL, eqmemfunc(size, nx->type->type), N);\
\tcall->list = list(call->list, eq);\n \tcall->list = list(call->list, nodintconst(size));\n-\tcall->list = list(call->list, conv(nx, types[TUNSAFEPTR]));\n-\tcall->list = list(call->list, conv(ny, types[TUNSAFEPTR]));\n+\tcall->list = list(call->list, nx);\n+\tcall->list = list(call->list, ny);\
\n \tnif = nod(OIF, N, N);\n \tnif->ninit = list(nif->ninit, call);\
eqmemfunc
関数のシグネチャにType *type
が追加され、memequal
系の関数ノードを取得する際に、その型情報も考慮されるようになりました。また、eqmem
関数内でmemequal
を呼び出す際、nx
とny
(比較対象のノード)がtypes[TUNSAFEPTR]
(unsafe.Pointer
型)に変換されるのではなく、直接引数として渡されるようになりました。これに伴い、typecheck
が追加されています。
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2408,8 +2408,12 @@ eqfor(Type *t)\n \tif(a != AMEM && a != -1)\n \t\tfatal(\"eqfor %T\", t);\n \n-\tif(a == AMEM)\n-\t\treturn syslook(\"memequal\", 0);\n+\tif(a == AMEM) {\n+\t\tn = syslook(\"memequal\", 1);\n+\t\targtype(n, t);\n+\t\targtype(n, t);\n+\t\treturn n;\n+\t}\n \n \tsym = typesymprefix(\".eq\", t);\n \tn = newname(sym);\n@@ -2417,8 +2421,8 @@ eqfor(Type *t)\n \tntype = nod(OTFUNC, N, N);\n \tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(ptrto(types[TBOOL]))));\n \tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(types[TUINTPTR])));\n-\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(types[TUNSAFEPTR])));\n-\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(types[TUNSAFEPTR])));\n+\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(ptrto(t))));\n+\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(ptrto(t))));\n \ttypecheck(&ntype, Etype);\n \tn->type = ntype->type;\n \treturn n;\n@@ -2536,8 +2540,8 @@ walkcompare(Node **np, NodeList **init)\n \ta->etype = 1; // does not escape\n \tcall->list = list(call->list, a);\n \tcall->list = list(call->list, nodintconst(t->width));\n-\tcall->list = list(call->list, conv(l, types[TUNSAFEPTR]));\n-\tcall->list = list(call->list, conv(r, types[TUNSAFEPTR]));\n+\tcall->list = list(call->list, l);\n+\tcall->list = list(call->list, r);\
\ttypecheck(&call, Etop);\n \twalkstmt(&call);\n \t*init = list(*init, call);\
eqfor
関数内でmemequal
関数ノードを取得する際に、引数型を*any
として設定するようになりました。また、walkcompare
関数内で比較を行う際に、l
とr
(比較対象の左辺と右辺)がtypes[TUNSAFEPTR]
に変換されることなく、直接引数として渡されるようになりました。
コアとなるコードの解説
このコミットのコアとなる変更は、Goコンパイラがmemequal
系の組み込み関数を扱う方法を根本的に変更した点にあります。
-
unsafe
パッケージのインポート削除とシグネチャ変更:src/cmd/gc/builtin.c.boot
とsrc/cmd/gc/runtime.go
における変更は、runtime
パッケージの定義からunsafe
パッケージへの直接的な依存を排除します。これにより、runtime
パッケージがGoの型システム内でより「通常の」パッケージとして扱われるようになります。memequal
系の関数の引数型をunsafe.Pointer
から*any
に変更することで、これらの関数がGoの型システムに則った形で定義されるようになります。*any
は任意の型へのポインタを抽象的に表現できるため、unsafe.Pointer
が提供していた「任意のメモリ領域を指す」という機能性を、型安全な範囲で代替しています。
-
コンパイラ内部の型処理の調整:
src/cmd/gc/subr.c
とsrc/cmd/gc/walk.c
における変更は、このシグネチャ変更に対応するためのコンパイラ内部の調整です。- 以前は、
memequal
を呼び出す際に、比較対象のポインタを明示的にunsafe.Pointer
型に変換(キャスト)していました。これは、unsafe.Pointer
が特殊な型であるため、コンパイラがその変換を認識し、適切なコードを生成する必要があったためです。 - 変更後は、比較対象のポインタ(
nx
,ny
,l
,r
)は*any
型として直接memequal
に渡されます。これにより、コンパイラは通常のポインタ型とインターフェース型の変換ルールに従って処理を行うことができます。 - 特に
subr.c
のeqmemfunc
とeqmem
、walk.c
のeqfor
とwalkcompare
では、conv(..., types[TUNSAFEPTR])
のようなunsafe.Pointer
への明示的な変換が削除され、代わりにtypecheck
が追加されています。これは、コンパイラが*any
型への変換を通常の型チェックプロセスの一部として処理できるようになったことを意味します。
この一連の変更により、Goコンパイラはruntime
パッケージの定義を処理する際にunsafe
パッケージの特殊なセマンティクスを考慮する必要がなくなり、コンパイラの内部ロジックが簡素化され、-u
トラッキング(unsafe
使用状況の追跡)の実装がより直接的かつ容易になりました。これは、Go言語の進化において、コンパイラとランタイムの間のインターフェースをよりクリーンで型安全なものにするための重要なステップと言えます。
関連リンク
- Go CL 5495094: https://golang.org/cl/5495094
参考にした情報源リンク
- Go言語の
unsafe
パッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe - Go言語の
runtime
パッケージに関する公式ドキュメント: https://pkg.go.dev/runtime - Go言語の
interface{}
(any
)に関する公式ドキュメント: https://pkg.go.dev/builtin#any - Goコンパイラの内部構造に関する一般的な情報(Goのバージョンによって異なる可能性があるため、当時の情報源を探すのが理想的ですが、一般的な概念は共通しています)