[インデックス 1641] ファイルの概要
このコミットは、Go言語の unsafe パッケージに Sizeof および Offsetof 関数を追加するものです。これらの関数は、Goプログラムが型のサイズや構造体フィールドのオフセットをコンパイル時に取得できるようにするためのもので、低レベルのメモリ操作やC言語との相互運用性において非常に重要です。
変更されたファイルは以下の通りです。
src/cmd/gc/dcl.c: Goコンパイラの宣言処理に関連するC言語のソースファイル。unsafe.Sizeofとunsafe.Offsetofのコンパイル時評価ロジックが追加されました。src/cmd/gc/go.h: Goコンパイラのヘッダーファイル。unsafenmagic関数のプロトタイプが追加されました。src/cmd/gc/go.y: Go言語の文法定義ファイル(Yacc/Bison形式)。unsafe.Sizeofとunsafe.Offsetofの呼び出しを特別に処理するためのパーサーの変更が含まれています。src/cmd/gc/unsafe.go:unsafeパッケージのGo言語のソースファイル。SizeofとOffsetofの関数宣言が追加されました。
コミット
commit 8a70545b57c05a0b72b83b7d7fd7d6b77bfbf9d3
Author: Ken Thompson <ken@golang.org>
Date: Sat Feb 7 12:34:45 2009 -0800
unsafe.Sizeof and unsafe.Offsetof
R=r
OCL=24639
CL=24639
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8a70545b57c05a0b72b83b7d7fd7d6b77bfbf9d3
元コミット内容
diff --git a/src/cmd/gc/dcl.c b/src/cmd/gc/dcl.c
index 9f7244fc7c..d5d3a9bf4d 100644
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -1483,3 +1483,54 @@ loop:
c = listnext(&citer);
goto loop;
}
+
+/*
+ * look for
+ * unsafe.Sizeof
+ * unsafe.Offsetof
+ * rewrite with a constant
+ */
+Node*
+unsafenmagic(Node *l, Node *r)
+{
+ Node *n;
+ Sym *s;
+ long v;
+ Val val;
+
+ if(l == N || r == N)
+ goto no;
+ if(l->op != ONAME)
+ goto no;
+ s = l->sym;
+ if(s == S)
+ goto no;
+ if(strcmp(s->opackage, "unsafe") != 0)
+ goto no;
+
+ if(strcmp(s->name, "Sizeof") == 0) {
+ walktype(r, Erv);
+ if(r->type == T)
+ goto no;
+ v = r->type->width;
+ goto yes;
+ }
+ if(strcmp(s->name, "Offsetof") == 0) {
+ if(r->op != ODOT && r->op != ODOTPTR)
+ goto no;
+ walktype(r, Erv);
+ v = n->xoffset;
+ goto yes;
+ }
+
+no:
+ return N;
+
+yes:
+ val.ctype = CTINT;
+ val.u.xval = mal(sizeof(*n->val.u.xval));
+ mpmovecfix(val.u.xval, v);
+ n = nod(OLITERAL, N, N);
+ n->val = val;
+ return n;
+}
diff --git a/src/cmd/gc/go.h b/src/cmd/gc/go.h
index 7b861d38c2..436ddd9a9b 100644
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -763,6 +763,7 @@ void constiter(Node*, Type*, Node*);
void funclit0(Type*);
Node* funclit1(Type*, Node*);
+Node* unsafenmagic(Node*, Node*);
/*
* export.c
diff --git a/src/cmd/gc/go.y b/src/cmd/gc/go.y
index 5ed7ed09c9..29a08912c4 100644
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -804,7 +804,9 @@ pexpr:
}
| pexpr '(' oexpr_list ')'
{
- $$ = nod(OCALL, $1, $3);
+ $$ = unsafenmagic($1, $3);
+ if($$ == N)
+ $$ = nod(OCALL, $1, $3);
}
| LLEN '(' expr ')'
{
diff --git a/src/cmd/gc/unsafe.go b/src/cmd/gc/unsafe.go
index 2b2187b3e3..47703f6e0f 100644
--- a/src/cmd/gc/unsafe.go
+++ b/src/cmd/gc/unsafe.go
@@ -6,3 +6,5 @@
package PACKAGE
type Pointer *any;
+func Offsetof(any) int;
+func Sizeof(any) int;
変更の背景
Go言語は、安全性と生産性を重視した言語ですが、システムプログラミングやハードウェアとの直接的なインタラクションが必要な場面では、低レベルのメモリ操作が不可欠になります。C言語で書かれたライブラリとの連携(Functin Foreign Interface, FFI)や、特定のデータ構造を効率的に扱うためには、Goの型システムが提供する抽象化を一時的に迂回し、メモリ上のバイト列としてデータを扱う必要が生じます。
unsafe.Sizeof と unsafe.Offsetof は、このような低レベル操作を可能にするために導入されました。これらは、Goのコンパイラによって特別に扱われる組み込み関数であり、実行時ではなくコンパイル時に評価され、対応する定数(バイト数)に置き換えられます。これにより、Goプログラムは、特定の型のメモリサイズや、構造体内のフィールドがメモリ上でどこに配置されているかを知ることができます。
具体的な背景としては、以下のようなニーズが挙げられます。
- C言語との相互運用性 (FFI): C言語の構造体とGoの構造体をマッピングする際に、両者のメモリレイアウトが一致していることを確認したり、Cの関数に渡すポインタのオフセットを計算したりするために必要です。
- メモリ効率の最適化: 非常に大きなデータ構造や、メモリ使用量が厳しく制限される環境において、パディングによる無駄をなくすために構造体のフィールド順序を最適化する際に、各フィールドのオフセットを確認するために使用されます。
- 低レベルのデータ操作: バイト列としてデータを読み書きする際に、特定の型のサイズや、構造体内の特定の部分へのオフセットを知ることで、正確なメモリ操作が可能になります。例えば、ネットワークプロトコルのパケット解析や、ファイルフォーマットの読み書きなどです。
- アライメントの考慮: CPUのアーキテクチャによっては、データが特定のアドレス境界に配置されている必要がある場合があります。
SizeofやOffsetofは、このようなアライメント要件を考慮したメモリレイアウトを理解するのに役立ちます。
これらの関数は unsafe パッケージに属しているため、使用には注意が必要です。unsafe パッケージの利用は、Goの型安全性を損なう可能性があり、プラットフォーム依存のコードや、将来のGoのバージョンで動作しなくなる可能性のあるコードにつながるため、本当に必要な場合にのみ使用すべきとされています。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について基本的な知識が必要です。
1. Go言語の型システムとメモリレイアウト
Go言語は静的型付け言語であり、すべての変数には型があります。型は、その変数がメモリ上でどれくらいの領域を占めるか(サイズ)や、どのようにデータが表現されるかを定義します。
- 型のサイズ (Sizeof): 各データ型(
int,float64,string, 構造体など)は、メモリ上で特定のバイト数を占めます。例えば、intは通常32ビットまたは64ビット(4バイトまたは8バイト)です。構造体の場合、そのフィールドの型のサイズと、アライメントのためのパディングによって全体のサイズが決まります。 - 構造体フィールドのオフセット (Offsetof): 構造体は、複数のフィールドをまとめた複合型です。各フィールドは、構造体の先頭から特定のバイト数だけ離れた位置(オフセット)に配置されます。コンパイラは、メモリ効率やCPUのアクセス効率を考慮して、フィールドの順序を調整したり、パディングを挿入したりすることがあります。
2. unsafe パッケージ
Go言語の unsafe パッケージは、Goの型安全性を意図的にバイパスするための機能を提供します。このパッケージは、Goの標準ライブラリの一部ですが、その名前が示す通り、使用には細心の注意が必要です。
unsafe パッケージが提供する主な機能は以下の通りです。
unsafe.Pointer: 任意の型のポインタを保持できる特殊なポインタ型です。*T型(任意の型Tへのポインタ)とuintptr型(ポインタの値を整数として扱う型)の間で相互変換が可能です。これにより、型システムが通常許可しないポインタ演算や型変換が可能になります。unsafe.Sizeof(x): 式xが占めるメモリのバイト数を返します。unsafe.Offsetof(x.f): 構造体xのフィールドfが、構造体の先頭からどれだけ離れているか(バイト単位のオフセット)を返します。unsafe.Alignof(x): 式xのアライメント要件(バイト単位)を返します。
unsafe パッケージの関数は、通常の関数呼び出しとは異なり、コンパイル時に評価され、その結果が定数としてコードに埋め込まれます。これにより、実行時のオーバーヘッドなしにメモリレイアウト情報を取得できます。
3. Goコンパイラ (gc) の内部構造
Goコンパイラ (gc) は、Goのソースコードを機械語に変換するツールです。このコミットで変更されているファイルは、gc の内部処理に関連しています。
src/cmd/gc/dcl.c: 宣言 (declaration) 処理を担当する部分です。変数の宣言、型の定義、関数の定義など、プログラムの構造を解析し、内部的な表現(AST: 抽象構文木)を構築します。このファイルには、コンパイル時の最適化や特殊な組み込み関数の処理ロジックが含まれることがあります。src/cmd/gc/go.y: Go言語の文法を定義するYacc/Bisonの入力ファイルです。パーサー(構文解析器)は、この定義に基づいてソースコードを解析し、ASTを生成します。関数呼び出しのような構文要素がどのように認識され、処理されるかが定義されています。Node: コンパイラ内部でASTのノードを表すデータ構造です。プログラムの各要素(変数、式、関数呼び出しなど)はNodeとして表現されます。Sym: シンボル(識別子)を表すデータ構造です。変数名、関数名、パッケージ名などがシンボルとして管理されます。Val: 定数値を表すデータ構造です。unsafe.Sizeofやunsafe.Offsetofの結果は、コンパイル時に定数として扱われるため、このVal構造体に格納されます。walktype(r, Erv): コンパイラの内部関数で、式の型情報を解決し、評価する役割を持ちます。
これらの知識を前提として、コミットの変更内容を詳細に見ていきます。
技術的詳細
このコミットの核心は、unsafe.Sizeof と unsafe.Offsetof の呼び出しをコンパイル時に定数に置き換えるためのコンパイラ(gc)の変更です。これらの関数は、通常の関数呼び出しとして扱われるのではなく、コンパイラの特殊なパスで処理されます。
1. unsafe.go での関数宣言
まず、src/cmd/gc/unsafe.go に Offsetof と Sizeof の関数宣言が追加されています。
func Offsetof(any) int;
func Sizeof(any) int;
これらの宣言は、Goのソースコードからこれらの関数を呼び出せるようにするためのものです。しかし、これらの関数には実際のGoのコードによる実装はありません。その代わりに、コンパイラがこれらの関数呼び出しを特別に認識し、処理します。引数に any が指定されているのは、Go 1.18より前のバージョンでは interface{} が使われており、任意の型を受け入れることを示しています。
2. go.y でのパーサーの変更
src/cmd/gc/go.y はGo言語の文法定義ファイルです。ここで、関数呼び出しを解析する pexpr ルールが変更されています。
- $$ = nod(OCALL, $1, $3);
+ $$ = unsafenmagic($1, $3);
+ if($$ == N)
+ $$ = nod(OCALL, $1, $3);
この変更により、パーサーは関数呼び出し pexpr '(' oexpr_list ')' を見つけると、まず unsafenmagic 関数を呼び出すようになりました。
$1は関数名(例:unsafe.Sizeof)に対応するASTノードです。$3は引数リスト(例:xやx.f)に対応するASTノードです。
unsafenmagic 関数が N(nilノード)を返した場合、それは unsafe.Sizeof や unsafe.Offsetof の呼び出しではなかったことを意味するため、通常の関数呼び出し nod(OCALL, $1, $3) として処理が続行されます。
3. dcl.c での unsafenmagic 関数の実装
src/cmd/gc/dcl.c に追加された unsafenmagic 関数が、このコミットの主要なロジックを担っています。この関数は、渡されたASTノードが unsafe.Sizeof または unsafe.Offsetof の呼び出しであるかどうかを識別し、そうであればコンパイル時にその値を計算して定数ノードに置き換えます。
unsafenmagic の処理フローは以下の通りです。
-
引数の検証:
l(関数名ノード) またはr(引数ノード) がN(nil) でないことを確認します。lがONAME(名前ノード) であることを確認します。lのシンボルsがS(nilシンボル) でないことを確認します。- シンボル
sのパッケージ名が"unsafe"であることを確認します。これにより、unsafeパッケージの関数呼び出しのみが対象となります。
-
Sizeofの処理:- 関数名が
"Sizeof"である場合、引数rの型情報を解決するためにwalktype(r, Erv)を呼び出します。Ervは "expression r-value" を意味し、式の値を評価するコンテキストを示します。 r->typeがT(nil型) でないことを確認します。- 引数の型
r->typeのwidth(サイズ) をvに格納します。
- 関数名が
-
Offsetofの処理:- 関数名が
"Offsetof"である場合、引数rがODOT(構造体フィールドアクセス、例:x.f) またはODOTPTR(ポインタ経由の構造体フィールドアクセス、例:p.f) であることを確認します。 walktype(r, Erv)を呼び出して、引数の型情報を解決します。- フィールドのオフセット
n->xoffsetをvに格納します。ここでnはwalktypeの結果として得られるノードで、フィールドのオフセット情報を含んでいます。
- 関数名が
-
定数ノードへの変換:
SizeofまたはOffsetofの条件に合致し、値vが計算された場合 (goto yes)、その値vを持つOLITERAL(リテラル定数) ノードを新しく作成します。val.ctypeをCTINT(整数定数) に設定します。val.u.xvalにvの値を多倍長整数として格納します(mpmovecfixは多倍長整数を扱うための関数です)。- 新しく作成された
OLITERALノードを返します。
-
非
unsafe関数の処理:- 上記のどの条件にも合致しない場合 (
goto no)、N(nilノード) を返します。これは、この関数呼び出しがunsafe.Sizeofやunsafe.Offsetofではないことを示し、パーサーは通常の関数呼び出しとして処理を続行します。
- 上記のどの条件にも合致しない場合 (
このメカニズムにより、unsafe.Sizeof(T) や unsafe.Offsetof(s.f) のようなコードは、コンパイル時にそれぞれ T のサイズや s.f のオフセットを表す整数定数に置き換えられます。これにより、実行時の関数呼び出しのオーバーヘッドが完全に排除され、非常に効率的な低レベル操作が可能になります。
4. go.h でのプロトタイプ宣言
src/cmd/gc/go.h には、unsafenmagic 関数のプロトタイプ宣言が追加されています。
Node* unsafenmagic(Node*, Node*);
これは、dcl.c で定義された unsafenmagic 関数が他のコンパイラソースファイルから呼び出せるようにするための標準的なC言語の慣習です。
コアとなるコードの変更箇所
src/cmd/gc/dcl.c
unsafenmagic 関数が追加されました。この関数は、unsafe.Sizeof と unsafe.Offsetof の呼び出しを検出し、コンパイル時にその値を計算して定数に置き換えるロジックを含んでいます。
Node*
unsafenmagic(Node *l, Node *r)
{
Node *n;
Sym *s;
long v;
Val val;
if(l == N || r == N)
goto no;
if(l->op != ONAME)
goto no;
s = l->sym;
if(s == S)
goto no;
if(strcmp(s->opackage, "unsafe") != 0)
goto no;
if(strcmp(s->name, "Sizeof") == 0) {
walktype(r, Erv);
if(r->type == T)
goto no;
v = r->type->width;
goto yes;
}
if(strcmp(s->name, "Offsetof") == 0) {
if(r->op != ODOT && r->op != ODOTPTR)
goto no;
walktype(r, Erv);
v = n->xoffset; // nはwalktypeの結果として得られるノード
goto yes;
}
no:
return N;
yes:
val.ctype = CTINT;
val.u.xval = mal(sizeof(*n->val.u.xval));
mpmovecfix(val.u.xval, v);
n = nod(OLITERAL, N, N);
n->val = val;
return n;
}
src/cmd/gc/go.h
unsafenmagic 関数のプロトタイプ宣言が追加されました。
Node* unsafenmagic(Node*, Node*);
src/cmd/gc/go.y
関数呼び出しを処理する pexpr ルールが変更され、unsafenmagic を最初に試行するように修正されました。
}
| pexpr '(' oexpr_list ')'
{
- $$ = nod(OCALL, $1, $3);
+ $$ = unsafenmagic($1, $3);
+ if($$ == N)
+ $$ = nod(OCALL, $1, $3);
}
| LLEN '(' expr ')'
{
src/cmd/gc/unsafe.go
unsafe パッケージに Offsetof と Sizeof の関数宣言が追加されました。
func Offsetof(any) int;
func Sizeof(any) int;
コアとなるコードの解説
このコミットのコアとなる変更は、Goコンパイラが unsafe.Sizeof と unsafe.Offsetof の呼び出しを特別に処理し、コンパイル時にその結果を定数に置き換えるメカニズムを導入した点です。
-
unsafe.goでの宣言:unsafe.goには、これらの関数のシグネチャ(引数と戻り値の型)のみが定義されています。実際のGoコードによる実装は存在しません。これは、これらの関数がGoのランタイムによって実行される通常の関数ではなく、コンパイラによって特別に扱われる「組み込み関数」であることを示しています。 -
go.yでのパーサーのフック:go.yの変更は、Goのソースコードがコンパイラによって解析される初期段階で、これらのunsafe関数の呼び出しを「捕捉」するためのものです。通常の関数呼び出し (nod(OCALL, $1, $3)) を生成する前に、unsafenmagic($1, $3)が呼び出されます。これにより、コンパイラはunsafeパッケージの特定の関数呼び出しを他の関数呼び出しとは異なる方法で処理する機会を得ます。 -
dcl.cのunsafenmagic: この関数は、unsafe.Sizeofまたはunsafe.Offsetofの呼び出しであるかどうかを識別する「魔法の」部分です。- 関数名が
unsafeパッケージに属しているか、そして関数名が"Sizeof"または"Offsetof"であるかを文字列比較で確認します。 Sizeofの場合、引数(型または変数)のメモリ上のサイズをr->type->widthから取得します。widthはコンパイラが型情報を解決する際に計算する、その型のバイトサイズです。Offsetofの場合、引数が構造体のフィールドアクセス(ODOTまたはODOTPTR)であることを確認し、そのフィールドのオフセットをn->xoffsetから取得します。xoffsetは、コンパイラが構造体のメモリレイアウトを決定する際に計算する、フィールドの先頭からのバイトオフセットです。- これらの値が取得できた場合、
unsafenmagicはその値を表すOLITERAL(定数リテラル) ノードを生成して返します。これにより、元のunsafe.Sizeof(...)やunsafe.Offsetof(...)の呼び出しは、コンパイル時に対応する整数定数に置き換えられます。 - もし呼び出しが
unsafe.Sizeofやunsafe.Offsetofでなかった場合、unsafenmagicはN(nil) を返し、パーサーは通常の関数呼び出しとして処理を続行します。
- 関数名が
この一連の処理により、Goのプログラムは、実行時のオーバーヘッドなしに、コンパイル時にメモリレイアウトに関する情報を取得できるようになります。これは、Goが低レベルのシステムプログラミングやC言語との連携を可能にする上で不可欠な機能です。
関連リンク
- Go言語
unsafeパッケージの公式ドキュメント: https://pkg.go.dev/unsafe - Go言語のメモリレイアウトに関するブログ記事 (例: "Go Data Structures" by Dave Cheney): https://dave.cheney.net/2014/03/20/go-data-structures (これは一般的な情報源であり、特定のコミットに関連するものではありませんが、背景知識として有用です。)
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gcディレクトリ) - Go言語の公式ドキュメント
- Go言語のコンパイラに関する一般的な知識
- Yacc/Bison の文法定義に関する知識
- C言語のポインタとメモリ管理に関する知識
- Go言語の
unsafeパッケージに関する一般的な解説記事