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

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

このコミットは、Goコンパイラ(cmd/gc)におけるunsafe.Offsetof関数の計算ロジックの修正に関するものです。具体的には、埋め込みフィールド(embedded field)のオフセット計算が誤っていた問題(Issue #4909)を解決します。

コミット

commit 2d3216f4a860c74eb1973ad08c782cf30363b88b
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Apr 5 21:24:07 2013 +0200

    cmd/gc: fix Offsetof computation.
    
    The offset of an embedded field s.X must be relative to s
    and not to the implicit s.Field of which X is a direct field.
    Moreover, no indirections may happen on the path.
    
    Fixes #4909.
    
    R=nigeltao, ality, daniel.morsing, iant, gri, r
    CC=golang-dev
    https://golang.org/cl/8287043

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

https://github.com/golang/go/commit/2d3216f4a860c74eb1973ad08c782cf30363b88b

元コミット内容

cmd/gc: fix Offsetof computation.

埋め込みフィールド s.X のオフセットは、s からの相対的なものでなければならず、X が直接のフィールドである暗黙的な s.Field からの相対的なものであってはならない。さらに、パス上では間接参照が発生してはならない。

Issue #4909 を修正。

変更の背景

Go言語には、構造体内に他の構造体をフィールド名なしで埋め込む「埋め込みフィールド(embedded fields)」という機能があります。これにより、埋め込まれた構造体のフィールドやメソッドが、外側の構造体のフィールドやメソッドであるかのように直接アクセスできるようになります。これは、Goにおける「継承」のような振る舞いを実現する強力なメカニズムです。

unsafeパッケージのOffsetof関数は、構造体のフィールドが構造体の先頭からどれだけ離れているか(バイト単位のオフセット)を計算するために使用されます。このオフセットは、低レベルのメモリ操作や、C言語との相互運用などで重要になります。

このコミットが行われる前、Goコンパイラ(cmd/gc)のunsafe.Offsetofの実装にはバグがありました。特に、埋め込みフィールドを介してアクセスされるフィールドのオフセットを計算する際に、誤った基準点から計算してしまう問題が存在しました。具体的には、s.Xのようなアクセスにおいて、Xsに埋め込まれた構造体Fieldのフィールドである場合、Offsetof(s.X)sの先頭からのオフセットを返す必要がありますが、実際にはs.Fieldの先頭からのオフセットを計算してしまうことがありました。

また、unsafe.Offsetofは、ポインタの間接参照を伴うパス(例: unsafe.Offsetof(p.X)pがポインタの場合)に対してはエラーを報告すべきですが、これも適切に処理されていませんでした。Issue #4909は、これらのunsafe.Offsetofの誤った挙動を報告したものであり、このコミットはその問題を修正するために導入されました。

前提知識の解説

  • Go言語の構造体と埋め込みフィールド: Goの構造体は、異なる型のフィールドをまとめるためのユーザー定義型です。埋め込みフィールドは、構造体内にフィールド名なしで別の構造体型を宣言することで実現されます。これにより、埋め込まれた型のフィールドやメソッドが、外側の構造体の「昇格された(promoted)」フィールドやメソッドとして直接アクセス可能になります。例えば、type Outer struct { Inner; int Z }という構造体で、InnerX intというフィールドを持つ場合、Outerのインスタンスoからo.XとしてInnerのフィールドXにアクセスできます。

  • unsafeパッケージ: Goのunsafeパッケージは、Goの型システムやメモリ安全性の保証をバイパスする低レベルな操作を可能にするためのパッケージです。これには、ポインタとuintptr間の変換、任意の型のサイズ(Sizeof)、アライメント(Alignof)、そして構造体フィールドのオフセット(Offsetof)を取得する関数が含まれます。unsafeパッケージの使用は、非常に注意深く行われるべきであり、通常は標準ライブラリや特定の高性能なコードでのみ使用されます。

  • unsafe.Offsetof関数: unsafe.Offsetof(expr)は、構造体のインスタンスexpr内のフィールドのオフセットをバイト単位で返します。例えば、type S struct { A int32; B int64 }という構造体がある場合、unsafe.Offsetof(s.B)は、sの先頭からフィールドBまでのバイト数を返します。この関数はコンパイル時に評価される定数式でなければなりません。

  • Goコンパイラ(cmd/gc: cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。unsafeパッケージの関数は、コンパイル時に特別な処理が行われる組み込み関数(intrinsic functions)として扱われます。

  • AST (Abstract Syntax Tree): コンパイラは、ソースコードを解析して抽象構文木(AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。コンパイラの各フェーズ(字句解析、構文解析、型チェック、コード生成など)でASTが操作されます。cmd/gcunsafe.cファイルは、このASTを操作してunsafe関数のセマンティクスを処理する部分に関連しています。

  • ODOTODOTPTRノード: GoコンパイラのASTにおいて、フィールドアクセスは特定のノードで表現されます。

    • ODOT: 構造体のフィールドアクセス(例: s.X)を表します。
    • ODOTPTR: ポインタを介した構造体のフィールドアクセス(例: p.Xpがポインタの場合)を表します。これは、Goが自動的にポインタのデリファレンスを行うため、(*p).Xと書かなくてもp.Xと書けることに対応します。

技術的詳細

このコミットの核心は、src/cmd/gc/unsafe.cファイル内のunsafenmagic関数におけるOffsetofの処理ロジックの変更です。

以前の実装では、unsafe.Offsetof(r)が呼び出された際、rODOTまたはODOTPTRノードであれば、そのノードのxoffset(フィールドのオフセット)を直接取得していました。しかし、これは埋め込みフィールドを介したアクセスの場合に問題を引き起こしました。

例えば、type S struct { T; int Z }type T struct { X int }という構造体がある場合、unsafe.Offsetof(s.X)という式を考えます。コンパイラはこれをs.T.Xのように内部的に解決します。以前のロジックでは、s.Xが最終的にs.T.Xという形式に解決された際に、T.Xのオフセットのみを考慮し、sからTまでのオフセットが加算されていませんでした。

新しいロジックでは、以下の点が改善されています。

  1. OXDOTのチェック: Offsetofの引数がセレクタ(フィールドアクセス)であることを確認するために、OXDOTノード(型チェック前のドットアクセス)であるかを最初にチェックします。
  2. ベースノードの追跡: セレクタのベースとなるノード(例: s.Xにおけるs)をbase変数で追跡します。これは、型チェックの過程でASTが変異する可能性があるため、r->leftが変更される前に元のベースを特定するためです。
  3. パス上のオフセットの累積: Offsetofの引数が複数の埋め込みフィールドを介してアクセスされる場合(例: s.F1.F2.X)、パス上の各ドットアクセス(ODOTノード)のオフセットを累積して加算するように変更されました。これは、for(r1=r; r1->left!=base; r1=r1->left)ループによって実現されます。このループは、現在のノードr1がベースノードに到達するまで、親ノードを辿りながら各ODOTノードのxoffsetを加算していきます。
  4. 間接参照の禁止: ODOTPTRノードがパス上に存在する場合、それはポインタの間接参照を意味するため、unsafe.Offsetofの引数としては不正であると判断し、エラー(yyerror("invalid expression %N: selector implies indirection of embedded %N", nn, r1->left))を報告するように修正されました。これは、unsafe.Offsetofがコンパイル時に定数オフセットを計算するため、実行時のポインタデリファレンスを伴うオフセットは計算できないという制約に基づいています。
  5. メソッド値の禁止: OCALLPARTノード(メソッド値)が引数として渡された場合も、エラーを報告するように修正されました。unsafe.Offsetofはフィールドのオフセットを計算するものであり、メソッドのオフセットを計算するものではないためです。

これらの変更により、unsafe.Offsetofは埋め込みフィールドを介したアクセスに対しても正確なオフセットを計算できるようになり、また、不正な引数(ポインタ間接参照やメソッド値)に対しては適切なエラーを報告するようになりました。

テストケースとして追加されたtest/fixedbugs/issue4909a.gotest/fixedbugs/issue4909b.goは、この修正が正しく機能することを確認するためのものです。

  • issue4909a.goは、ポインタを介した埋め込みフィールドアクセス(t.Xp.XXが埋め込みポインタフィールドのフィールドの場合)やメソッド値に対するOffsetofがエラーになることを検証します。
  • issue4909b.goは、深くネストされた埋め込みフィールドを持つ構造体を動的に生成し、Offsetofが正しくオフセットを計算し、かつ間接参照を伴う場合にエラーを出すことを検証します。
  • test/sizeof.goは、既存のunsafe.Sizeof, unsafe.Alignof, unsafe.Offsetofのテストに、埋め込みフィールドのオフセットに関する新しいテストケースを追加しています。特に、t2.Ct2.U2.Cのオフセットが期待通りになることを確認しています。

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

変更は主にsrc/cmd/gc/unsafe.cファイル内のunsafenmagic関数に集中しています。

--- a/src/cmd/gc/unsafe.c
+++ b/src/cmd/gc/unsafe.c
@@ -16,7 +16,7 @@
 Node*
 unsafenmagic(Node *nn)
 {
-	Node *r, *n;
+	Node *r, *n, *base, *r1;
 	Sym *s;
 	Type *t, *tr;
 	long v;
@@ -49,11 +49,43 @@ unsafenmagic(Node *nn)
 		goto yes;
 	}
 	if(strcmp(s->name, "Offsetof") == 0) {
-		typecheck(&r, Erv);
-		if(r->op != ODOT && r->op != ODOTPTR)
+		// must be a selector.
+		if(r->op != OXDOT)
 			goto bad;
+		// Remember base of selector to find it back after dot insertion.
+		// Since r->left may be mutated by typechecking, check it explicitly
+		// first to track it correctly.
+		typecheck(&r->left, Erv);
+		base = r->left;
 		typecheck(&r, Erv);
-		v = r->xoffset;
+		switch(r->op) {
+		case ODOT:
+		case ODOTPTR:
+			break;
+		case OCALLPART:
+			yyerror("invalid expression %N: argument is a method value", nn);
+			v = 0;
+			goto ret;
+		default:
+			goto bad;
+		}
+		v = 0;
+		// add offsets for inserted dots.
+		for(r1=r; r1->left!=base; r1=r1->left) {
+			switch(r1->op) {
+			case ODOT:
+				v += r1->xoffset;
+				break;
+			case ODOTPTR:
+				yyerror("invalid expression %N: selector implies indirection of embedded %N", nn, r1->left);
+				goto ret;
+			default:
+				dump("unsafenmagic", r);
+				fatal("impossible %#O node after dot insertion", r1->op);
+				goto bad;
+			}
+		}
+		v += r1->xoffset;
 		goto yes;
 	}
 	if(strcmp(s->name, "Alignof") == 0) {

また、以下のテストファイルが追加・修正されています。

  • test/fixedbugs/issue4909a.go (新規追加)
  • test/fixedbugs/issue4909b.go (新規追加)
  • test/sizeof.go (修正)

コアとなるコードの解説

src/cmd/gc/unsafe.cunsafenmagic関数は、unsafeパッケージの組み込み関数(Sizeof, Alignof, Offsetof)のコンパイル時処理を担当しています。

変更されたOffsetofの処理ブロックを詳しく見ていきます。

  1. Node *r, *n, *base, *r1;: 新たにbaser1というNodeポインタが宣言されています。baseはセレクタの根元のオブジェクト(例: s.Xs)を、r1はオフセットを累積するためにASTを辿る際に使用されます。

  2. if(r->op != OXDOT): Offsetofの引数rOXDOT(型チェック前のドットアクセス)でない場合、goto badでエラー処理に飛びます。これは、Offsetofがフィールドセレクタにのみ適用されるべきであることを保証します。

  3. typecheck(&r->left, Erv); base = r->left;: セレクタの左辺(例: s.Xs)を型チェックし、その結果をbaseに保存します。これは、後続のtypecheck(&r, Erv)によってr->leftが変更される可能性があるため、元のベースノードを正確に追跡するために重要です。

  4. typecheck(&r, Erv);: Offsetofの引数全体(セレクタ)を型チェックします。この型チェックの過程で、埋め込みフィールドの「昇格」が解決され、ASTがs.T.Xのような複数のドットアクセスを含む形に変形される可能性があります。

  5. switch(r->op)ブロック: 型チェック後のrの操作をチェックします。

    • case ODOT:: 通常のフィールドアクセス。
    • case ODOTPTR:: ポインタを介したフィールドアクセス。
    • case OCALLPART:: メソッド値。これはOffsetofの引数としては不正なので、yyerrorでエラーを報告し、goto retで処理を終了します。
    • default:: その他の不正な操作の場合、goto badでエラー処理に飛びます。
  6. v = 0;: オフセットの合計を格納する変数vを0で初期化します。

  7. for(r1=r; r1->left!=base; r1=r1->left)ループ: このループがオフセット計算の核心です。

    • r1=r: r1を現在のセレクタノードrで初期化します。
    • r1->left!=base: r1の左辺(親ノード)が、最初に特定したベースノードに到達するまでループを続けます。これにより、s.T.Xのような多段階のセレクタパスを根元まで辿ることができます。
    • ループ内で、r1の操作をチェックします。
      • case ODOT:: ODOTノードの場合、そのノードのxoffset(このドットアクセスによって追加されるオフセット)をvに加算します。
      • case ODOTPTR:: ODOTPTRノードの場合、これはポインタの間接参照を意味します。unsafe.Offsetofはコンパイル時に定数オフセットを計算するため、実行時の間接参照は許可されません。したがって、yyerrorでエラーを報告し、goto retで処理を終了します。
      • default:: その他の予期せぬノードの場合、fatalエラーでコンパイラを停止させます。
  8. v += r1->xoffset;: ループが終了した後、最後にr1(ベースノードの直前のノード)のxoffsetvに加算します。これにより、パス上のすべてのオフセットが正しく累積されます。

この修正により、unsafe.Offsetofは、埋め込みフィールドの昇格によって生成される複雑なセレクタパスに対しても、構造体の先頭からの正確なオフセットを計算できるようになりました。また、Offsetofのセマンティクスに反する不正な引数(ポインタ間接参照やメソッド値)を適切に検出してエラーを報告するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のunsafeパッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe
  • Go言語の埋め込みフィールドに関する公式ドキュメントやチュートリアル(例: Go Tourの埋め込みフィールドのセクションなど)
  • Goコンパイラの内部構造に関する一般的な情報源(例: Goのソースコード、コンパイラ設計に関する書籍や記事)
  • Go言語のASTに関する情報(例: go/astパッケージのドキュメント)

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

このコミットは、Goコンパイラ(cmd/gc)におけるunsafe.Offsetof関数の計算ロジックの修正に関するものです。具体的には、埋め込みフィールド(embedded field)のオフセット計算が誤っていた問題(Issue #4909)を解決します。

コミット

commit 2d3216f4a860c74eb1973ad08c782cf30363b88b
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Apr 5 21:24:07 2013 +0200

    cmd/gc: fix Offsetof computation.
    
    The offset of an embedded field s.X must be relative to s
    and not to the implicit s.Field of which X is a direct field.
    Moreover, no indirections may happen on the path.
    
    Fixes #4909.
    
    R=nigeltao, ality, daniel.morsing, iant, gri, r
    CC=golang-dev
    https://golang.org/cl/8287043

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

https://github.com/golang/go/commit/2d3216f4a860c74eb1973ad08c782cf30363b88b

元コミット内容

cmd/gc: fix Offsetof computation.

埋め込みフィールド s.X のオフセットは、s からの相対的なものでなければならず、X が直接のフィールドである暗黙的な s.Field からの相対的なものであってはならない。さらに、パス上では間接参照が発生してはならない。

Issue #4909 を修正。

変更の背景

Go言語には、構造体内に他の構造体をフィールド名なしで埋め込む「埋め込みフィールド(embedded fields)」という機能があります。これにより、埋め込まれた構造体のフィールドやメソッドが、外側の構造体のフィールドやメソッドであるかのように直接アクセスできるようになります。これは、Goにおける「継承」のような振る舞いを実現する強力なメカニズムです。

unsafeパッケージのOffsetof関数は、構造体のフィールドが構造体の先頭からどれだけ離れているか(バイト単位のオフセット)を計算するために使用されます。このオフセットは、低レベルのメモリ操作や、C言語との相互運用などで重要になります。

このコミットが行われる前、Goコンパイラ(cmd/gc)のunsafe.Offsetofの実装にはバグがありました。特に、埋め込みフィールドを介してアクセスされるフィールドのオフセットを計算する際に、誤った基準点から計算してしまう問題が存在しました。具体的には、s.Xのようなアクセスにおいて、Xsに埋め込まれた構造体Fieldのフィールドである場合、Offsetof(s.X)sの先頭からのオフセットを返す必要がありますが、実際にはs.Fieldの先頭からのオフセットを計算してしまうことがありました。

また、unsafe.Offsetofは、ポインタの間接参照を伴うパス(例: unsafe.Offsetof(p.X)pがポインタの場合)に対してはエラーを報告すべきですが、これも適切に処理されていませんでした。Issue #4909は、これらのunsafe.Offsetofの誤った挙動を報告したものであり、このコミットはその問題を修正するために導入されました。

前提知識の解説

  • Go言語の構造体と埋め込みフィールド: Goの構造体は、異なる型のフィールドをまとめるためのユーザー定義型です。埋め込みフィールドは、構造体内にフィールド名なしで別の構造体型を宣言することで実現されます。これにより、埋め込まれた型のフィールドやメソッドが、外側の構造体の「昇格された(promoted)」フィールドやメソッドとして直接アクセス可能になります。例えば、type Outer struct { Inner; int Z }という構造体で、InnerX intというフィールドを持つ場合、Outerのインスタンスoからo.XとしてInnerのフィールドXにアクセスできます。

  • unsafeパッケージ: Goのunsafeパッケージは、Goの型システムやメモリ安全性の保証をバイパスする低レベルな操作を可能にするためのパッケージです。これには、ポインタとuintptr間の変換、任意の型のサイズ(Sizeof)、アライメント(Alignof)、そして構造体フィールドのオフセット(Offsetof)を取得する関数が含まれます。unsafeパッケージの使用は、非常に注意深く行われるべきであり、通常は標準ライブラリや特定の高性能なコードでのみ使用されます。

  • unsafe.Offsetof関数: unsafe.Offsetof(expr)は、構造体のインスタンスexpr内のフィールドのオフセットをバイト単位で返します。例えば、type S struct { A int32; B int64 }という構造体がある場合、unsafe.Offsetof(s.B)は、sの先頭からフィールドBまでのバイト数を返します。この関数はコンパイル時に評価される定数式でなければなりません。

  • Goコンパイラ(cmd/gc: cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。unsafeパッケージの関数は、コンパイル時に特別な処理が行われる組み込み関数(intrinsic functions)として扱われます。

  • AST (Abstract Syntax Tree): コンパイラは、ソースコードを解析して抽象構文木(AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。コンパイラの各フェーズ(字句解析、構文解析、型チェック、コード生成など)でASTが操作されます。cmd/gcunsafe.cファイルは、このASTを操作してunsafe関数のセマンティクスを処理する部分に関連しています。

  • ODOTODOTPTRノード: GoコンパイラのASTにおいて、フィールドアクセスは特定のノードで表現されます。

    • ODOT: 構造体のフィールドアクセス(例: s.X)を表します。
    • ODOTPTR: ポインタを介した構造体のフィールドアクセス(例: p.Xpがポインタの場合)を表します。これは、Goが自動的にポインタのデリファレンスを行うため、(*p).Xと書かなくてもp.Xと書けることに対応します。

技術的詳細

このコミットの核心は、src/cmd/gc/unsafe.cファイル内のunsafenmagic関数におけるOffsetofの処理ロジックの変更です。

以前の実装では、unsafe.Offsetof(r)が呼び出された際、rODOTまたはODOTPTRノードであれば、そのノードのxoffset(フィールドのオフセット)を直接取得していました。しかし、これは埋め込みフィールドを介したアクセスの場合に問題を引き起こしました。

例えば、type S struct { T; int Z }type T struct { X int }という構造体がある場合、unsafe.Offsetof(s.X)という式を考えます。コンパイラはこれをs.T.Xのように内部的に解決します。以前のロジックでは、s.Xが最終的にs.T.Xという形式に解決された際に、T.Xのオフセットのみを考慮し、sからTまでのオフセットが加算されていませんでした。

新しいロジックでは、以下の点が改善されています。

  1. OXDOTのチェック: Offsetofの引数がセレクタ(フィールドアクセス)であることを確認するために、OXDOTノード(型チェック前のドットアクセス)であるかを最初にチェックします。
  2. ベースノードの追跡: セレクタのベースとなるノード(例: s.Xにおけるs)をbase変数で追跡します。これは、型チェックの過程でASTが変異する可能性があるため、r->leftが変更される前に元のベースを特定するためです。
  3. パス上のオフセットの累積: Offsetofの引数が複数の埋め込みフィールドを介してアクセスされる場合(例: s.F1.F2.X)、パス上の各ドットアクセス(ODOTノード)のオフセットを累積して加算するように変更されました。これは、for(r1=r; r1->left!=base; r1=r1->left)ループによって実現されます。このループは、現在のノードr1がベースノードに到達するまで、親ノードを辿りながら各ODOTノードのxoffsetを加算していきます。
  4. 間接参照の禁止: ODOTPTRノードがパス上に存在する場合、それはポインタの間接参照を意味するため、unsafe.Offsetofの引数としては不正であると判断し、エラー(yyerror("invalid expression %N: selector implies indirection of embedded %N", nn, r1->left))を報告するように修正されました。これは、unsafe.Offsetofがコンパイル時に定数オフセットを計算するため、実行時のポインタデリファレンスを伴うオフセットは計算できないという制約に基づいています。
  5. メソッド値の禁止: OCALLPARTノード(メソッド値)が引数として渡された場合も、エラーを報告するように修正されました。unsafe.Offsetofはフィールドのオフセットを計算するものであり、メソッドのオフセットを計算するものではないためです。

これらの変更により、unsafe.Offsetofは埋め込みフィールドを介したアクセスに対しても正確なオフセットを計算できるようになり、また、不正な引数(ポインタ間接参照やメソッド値)に対しては適切なエラーを報告するようになりました。

テストケースとして追加されたtest/fixedbugs/issue4909a.gotest/fixedbugs/issue4909b.goは、この修正が正しく機能することを確認するためのものです。

  • issue4909a.goは、ポインタを介した埋め込みフィールドアクセス(t.Xp.XXが埋め込みポインタフィールドのフィールドの場合)やメソッド値に対するOffsetofがエラーになることを検証します。
  • issue4909b.goは、深くネストされた埋め込みフィールドを持つ構造体を動的に生成し、Offsetofが正しくオフセットを計算し、かつ間接参照を伴う場合にエラーを出すことを検証します。
  • test/sizeof.goは、既存のunsafe.Sizeof, unsafe.Alignof, unsafe.Offsetofのテストに、埋め込みフィールドのオフセットに関する新しいテストケースを追加しています。特に、t2.Ct2.U2.Cのオフセットが期待通りになることを確認しています。

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

変更は主にsrc/cmd/gc/unsafe.cファイル内のunsafenmagic関数に集中しています。

--- a/src/cmd/gc/unsafe.c
+++ b/src/cmd/gc/unsafe.c
@@ -16,7 +16,7 @@
 Node*
 unsafenmagic(Node *nn)
 {
-	Node *r, *n;
+	Node *r, *n, *base, *r1;
 	Sym *s;
 	Type *t, *tr;
 	long v;
@@ -49,11 +49,43 @@ unsafenmagic(Node *nn)
 		goto yes;
 	}
 	if(strcmp(s->name, "Offsetof") == 0) {
-		typecheck(&r, Erv);
-		if(r->op != ODOT && r->op != ODOTPTR)
+		// must be a selector.
+		if(r->op != OXDOT)
 			goto bad;
+		// Remember base of selector to find it back after dot insertion.
+		// Since r->left may be mutated by typechecking, check it explicitly
+		// first to track it correctly.
+		typecheck(&r->left, Erv);
+		base = r->left;
 		typecheck(&r, Erv);
-		v = r->xoffset;
+		switch(r->op) {
+		case ODOT:
+		case ODOTPTR:
+			break;
+		case OCALLPART:
+			yyerror("invalid expression %N: argument is a method value", nn);
+			v = 0;
+			goto ret;
+		default:
+			goto bad;
+		}
+		v = 0;
+		// add offsets for inserted dots.
+		for(r1=r; r1->left!=base; r1=r1->left) {
+			switch(r1->op) {
+			case ODOT:
+				v += r1->xoffset;
+				break;
+			case ODOTPTR:
+				yyerror("invalid expression %N: selector implies indirection of embedded %N", nn, r1->left);
+				goto ret;
+			default:
+				dump("unsafenmagic", r);
+				fatal("impossible %#O node after dot insertion", r1->op);
+				goto bad;
+			}
+		}
+		v += r1->xoffset;
 		goto yes;
 	}
 	if(strcmp(s->name, "Alignof") == 0) {

また、以下のテストファイルが追加・修正されています。

  • test/fixedbugs/issue4909a.go (新規追加)
  • test/fixedbugs/issue4909b.go (新規追加)
  • test/sizeof.go (修正)

コアとなるコードの解説

src/cmd/gc/unsafe.cunsafenmagic関数は、unsafeパッケージの組み込み関数(Sizeof, Alignof, Offsetof)のコンパイル時処理を担当しています。

変更されたOffsetofの処理ブロックを詳しく見ていきます。

  1. Node *r, *n, *base, *r1;: 新たにbaser1というNodeポインタが宣言されています。baseはセレクタの根元のオブジェクト(例: s.Xs)を、r1はオフセットを累積するためにASTを辿る際に使用されます。

  2. if(r->op != OXDOT): Offsetofの引数rOXDOT(型チェック前のドットアクセス)でない場合、goto badでエラー処理に飛びます。これは、Offsetofがフィールドセレクタにのみ適用されるべきであることを保証します。

  3. typecheck(&r->left, Erv); base = r->left;: セレクタの左辺(例: s.Xs)を型チェックし、その結果をbaseに保存します。これは、後続のtypecheck(&r, Erv)によってr->leftが変更される可能性があるため、元のベースノードを正確に追跡するために重要です。

  4. typecheck(&r, Erv);: Offsetofの引数全体(セレクタ)を型チェックします。この型チェックの過程で、埋め込みフィールドの「昇格」が解決され、ASTがs.T.Xのような複数のドットアクセスを含む形に変形される可能性があります。

  5. switch(r->op)ブロック: 型チェック後のrの操作をチェックします。

    • case ODOT:: 通常のフィールドアクセス。
    • case ODOTPTR:: ポインタを介したフィールドアクセス。
    • case OCALLPART:: メソッド値。これはOffsetofの引数としては不正なので、yyerrorでエラーを報告し、goto retで処理を終了します。
    • default:: その他の不正な操作の場合、goto badでエラー処理に飛びます。
  6. v = 0;: オフセットの合計を格納する変数vを0で初期化します。

  7. for(r1=r; r1->left!=base; r1=r1->left)ループ: このループがオフセット計算の核心です。

    • r1=r: r1を現在のセレクタノードrで初期化します。
    • r1->left!=base: r1の左辺(親ノード)が、最初に特定したベースノードに到達するまでループを続けます。これにより、s.T.Xのような多段階のセレクタパスを根元まで辿ることができます。
    • ループ内で、r1の操作をチェックします。
      • case ODOT:: ODOTノードの場合、そのノードのxoffset(このドットアクセスによって追加されるオフセット)をvに加算します。
      • case ODOTPTR:: ODOTPTRノードの場合、これはポインタの間接参照を意味します。unsafe.Offsetofはコンパイル時に定数オフセットを計算するため、実行時の間接参照は許可されません。したがって、yyerrorでエラーを報告し、goto retで処理を終了します。
      • default:: その他の予期せぬノードの場合、fatalエラーでコンパイラを停止させます。
  8. v += r1->xoffset;: ループが終了した後、最後にr1(ベースノードの直前のノード)のxoffsetvに加算します。これにより、パス上のすべてのオフセットが正しく累積されます。

この修正により、unsafe.Offsetofは、埋め込みフィールドの昇格によって生成される複雑なセレクタパスに対しても、構造体の先頭からの正確なオフセットを計算できるようになりました。また、Offsetofのセマンティクスに反する不正な引数(ポインタ間接参照やメソッド値)を適切に検出してエラーを報告するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のunsafeパッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe
  • Go言語の埋め込みフィールドに関する公式ドキュメントやチュートリアル(例: Go Tourの埋め込みフィールドのセクションなど)
  • Goコンパイラの内部構造に関する一般的な情報源(例: Goのソースコード、コンパイラ設計に関する書籍や記事)
  • Go言語のASTに関する情報(例: go/astパッケージのドキュメント)