[インデックス 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
のようなアクセスにおいて、X
がs
に埋め込まれた構造体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 }
という構造体で、Inner
がX 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/gc
のunsafe.c
ファイルは、このASTを操作してunsafe
関数のセマンティクスを処理する部分に関連しています。 -
ODOT
とODOTPTR
ノード: GoコンパイラのASTにおいて、フィールドアクセスは特定のノードで表現されます。ODOT
: 構造体のフィールドアクセス(例:s.X
)を表します。ODOTPTR
: ポインタを介した構造体のフィールドアクセス(例:p.X
、p
がポインタの場合)を表します。これは、Goが自動的にポインタのデリファレンスを行うため、(*p).X
と書かなくてもp.X
と書けることに対応します。
技術的詳細
このコミットの核心は、src/cmd/gc/unsafe.c
ファイル内のunsafenmagic
関数におけるOffsetof
の処理ロジックの変更です。
以前の実装では、unsafe.Offsetof(r)
が呼び出された際、r
がODOT
または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
までのオフセットが加算されていませんでした。
新しいロジックでは、以下の点が改善されています。
OXDOT
のチェック:Offsetof
の引数がセレクタ(フィールドアクセス)であることを確認するために、OXDOT
ノード(型チェック前のドットアクセス)であるかを最初にチェックします。- ベースノードの追跡: セレクタのベースとなるノード(例:
s.X
におけるs
)をbase
変数で追跡します。これは、型チェックの過程でASTが変異する可能性があるため、r->left
が変更される前に元のベースを特定するためです。 - パス上のオフセットの累積:
Offsetof
の引数が複数の埋め込みフィールドを介してアクセスされる場合(例:s.F1.F2.X
)、パス上の各ドットアクセス(ODOT
ノード)のオフセットを累積して加算するように変更されました。これは、for(r1=r; r1->left!=base; r1=r1->left)
ループによって実現されます。このループは、現在のノードr1
がベースノードに到達するまで、親ノードを辿りながら各ODOT
ノードのxoffset
を加算していきます。 - 間接参照の禁止:
ODOTPTR
ノードがパス上に存在する場合、それはポインタの間接参照を意味するため、unsafe.Offsetof
の引数としては不正であると判断し、エラー(yyerror("invalid expression %N: selector implies indirection of embedded %N", nn, r1->left)
)を報告するように修正されました。これは、unsafe.Offsetof
がコンパイル時に定数オフセットを計算するため、実行時のポインタデリファレンスを伴うオフセットは計算できないという制約に基づいています。 - メソッド値の禁止:
OCALLPART
ノード(メソッド値)が引数として渡された場合も、エラーを報告するように修正されました。unsafe.Offsetof
はフィールドのオフセットを計算するものであり、メソッドのオフセットを計算するものではないためです。
これらの変更により、unsafe.Offsetof
は埋め込みフィールドを介したアクセスに対しても正確なオフセットを計算できるようになり、また、不正な引数(ポインタ間接参照やメソッド値)に対しては適切なエラーを報告するようになりました。
テストケースとして追加されたtest/fixedbugs/issue4909a.go
とtest/fixedbugs/issue4909b.go
は、この修正が正しく機能することを確認するためのものです。
issue4909a.go
は、ポインタを介した埋め込みフィールドアクセス(t.X
やp.X
でX
が埋め込みポインタフィールドのフィールドの場合)やメソッド値に対するOffsetof
がエラーになることを検証します。issue4909b.go
は、深くネストされた埋め込みフィールドを持つ構造体を動的に生成し、Offsetof
が正しくオフセットを計算し、かつ間接参照を伴う場合にエラーを出すことを検証します。test/sizeof.go
は、既存のunsafe.Sizeof
,unsafe.Alignof
,unsafe.Offsetof
のテストに、埋め込みフィールドのオフセットに関する新しいテストケースを追加しています。特に、t2.C
とt2.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.c
のunsafenmagic
関数は、unsafe
パッケージの組み込み関数(Sizeof
, Alignof
, Offsetof
)のコンパイル時処理を担当しています。
変更されたOffsetof
の処理ブロックを詳しく見ていきます。
-
Node *r, *n, *base, *r1;
: 新たにbase
とr1
というNode
ポインタが宣言されています。base
はセレクタの根元のオブジェクト(例:s.X
のs
)を、r1
はオフセットを累積するためにASTを辿る際に使用されます。 -
if(r->op != OXDOT)
:Offsetof
の引数r
がOXDOT
(型チェック前のドットアクセス)でない場合、goto bad
でエラー処理に飛びます。これは、Offsetof
がフィールドセレクタにのみ適用されるべきであることを保証します。 -
typecheck(&r->left, Erv); base = r->left;
: セレクタの左辺(例:s.X
のs
)を型チェックし、その結果をbase
に保存します。これは、後続のtypecheck(&r, Erv)
によってr->left
が変更される可能性があるため、元のベースノードを正確に追跡するために重要です。 -
typecheck(&r, Erv);
:Offsetof
の引数全体(セレクタ)を型チェックします。この型チェックの過程で、埋め込みフィールドの「昇格」が解決され、ASTがs.T.X
のような複数のドットアクセスを含む形に変形される可能性があります。 -
switch(r->op)
ブロック: 型チェック後のr
の操作をチェックします。case ODOT:
: 通常のフィールドアクセス。case ODOTPTR:
: ポインタを介したフィールドアクセス。case OCALLPART:
: メソッド値。これはOffsetof
の引数としては不正なので、yyerror
でエラーを報告し、goto ret
で処理を終了します。default:
: その他の不正な操作の場合、goto bad
でエラー処理に飛びます。
-
v = 0;
: オフセットの合計を格納する変数v
を0で初期化します。 -
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
エラーでコンパイラを停止させます。
-
v += r1->xoffset;
: ループが終了した後、最後にr1
(ベースノードの直前のノード)のxoffset
をv
に加算します。これにより、パス上のすべてのオフセットが正しく累積されます。
この修正により、unsafe.Offsetof
は、埋め込みフィールドの昇格によって生成される複雑なセレクタパスに対しても、構造体の先頭からの正確なオフセットを計算できるようになりました。また、Offsetof
のセマンティクスに反する不正な引数(ポインタ間接参照やメソッド値)を適切に検出してエラーを報告するようになりました。
関連リンク
- Go Issue #4909:
cmd/gc: unsafe.Offsetof(t.x) where x is field of embedded pointer field
- https://github.com/golang/go/issues/4909 - Go CL 8287043:
cmd/gc: fix Offsetof computation.
- https://golang.org/cl/8287043 (これはコミットメッセージに記載されているリンクと同じです)
参考にした情報源リンク
- 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
のようなアクセスにおいて、X
がs
に埋め込まれた構造体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 }
という構造体で、Inner
がX 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/gc
のunsafe.c
ファイルは、このASTを操作してunsafe
関数のセマンティクスを処理する部分に関連しています。 -
ODOT
とODOTPTR
ノード: GoコンパイラのASTにおいて、フィールドアクセスは特定のノードで表現されます。ODOT
: 構造体のフィールドアクセス(例:s.X
)を表します。ODOTPTR
: ポインタを介した構造体のフィールドアクセス(例:p.X
、p
がポインタの場合)を表します。これは、Goが自動的にポインタのデリファレンスを行うため、(*p).X
と書かなくてもp.X
と書けることに対応します。
技術的詳細
このコミットの核心は、src/cmd/gc/unsafe.c
ファイル内のunsafenmagic
関数におけるOffsetof
の処理ロジックの変更です。
以前の実装では、unsafe.Offsetof(r)
が呼び出された際、r
がODOT
または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
までのオフセットが加算されていませんでした。
新しいロジックでは、以下の点が改善されています。
OXDOT
のチェック:Offsetof
の引数がセレクタ(フィールドアクセス)であることを確認するために、OXDOT
ノード(型チェック前のドットアクセス)であるかを最初にチェックします。- ベースノードの追跡: セレクタのベースとなるノード(例:
s.X
におけるs
)をbase
変数で追跡します。これは、型チェックの過程でASTが変異する可能性があるため、r->left
が変更される前に元のベースを特定するためです。 - パス上のオフセットの累積:
Offsetof
の引数が複数の埋め込みフィールドを介してアクセスされる場合(例:s.F1.F2.X
)、パス上の各ドットアクセス(ODOT
ノード)のオフセットを累積して加算するように変更されました。これは、for(r1=r; r1->left!=base; r1=r1->left)
ループによって実現されます。このループは、現在のノードr1
がベースノードに到達するまで、親ノードを辿りながら各ODOT
ノードのxoffset
を加算していきます。 - 間接参照の禁止:
ODOTPTR
ノードがパス上に存在する場合、それはポインタの間接参照を意味するため、unsafe.Offsetof
の引数としては不正であると判断し、エラー(yyerror("invalid expression %N: selector implies indirection of embedded %N", nn, r1->left)
)を報告するように修正されました。これは、unsafe.Offsetof
がコンパイル時に定数オフセットを計算するため、実行時のポインタデリファレンスを伴うオフセットは計算できないという制約に基づいています。 - メソッド値の禁止:
OCALLPART
ノード(メソッド値)が引数として渡された場合も、エラーを報告するように修正されました。unsafe.Offsetof
はフィールドのオフセットを計算するものであり、メソッドのオフセットを計算するものではないためです。
これらの変更により、unsafe.Offsetof
は埋め込みフィールドを介したアクセスに対しても正確なオフセットを計算できるようになり、また、不正な引数(ポインタ間接参照やメソッド値)に対しては適切なエラーを報告するようになりました。
テストケースとして追加されたtest/fixedbugs/issue4909a.go
とtest/fixedbugs/issue4909b.go
は、この修正が正しく機能することを確認するためのものです。
issue4909a.go
は、ポインタを介した埋め込みフィールドアクセス(t.X
やp.X
でX
が埋め込みポインタフィールドのフィールドの場合)やメソッド値に対するOffsetof
がエラーになることを検証します。issue4909b.go
は、深くネストされた埋め込みフィールドを持つ構造体を動的に生成し、Offsetof
が正しくオフセットを計算し、かつ間接参照を伴う場合にエラーを出すことを検証します。test/sizeof.go
は、既存のunsafe.Sizeof
,unsafe.Alignof
,unsafe.Offsetof
のテストに、埋め込みフィールドのオフセットに関する新しいテストケースを追加しています。特に、t2.C
とt2.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.c
のunsafenmagic
関数は、unsafe
パッケージの組み込み関数(Sizeof
, Alignof
, Offsetof
)のコンパイル時処理を担当しています。
変更されたOffsetof
の処理ブロックを詳しく見ていきます。
-
Node *r, *n, *base, *r1;
: 新たにbase
とr1
というNode
ポインタが宣言されています。base
はセレクタの根元のオブジェクト(例:s.X
のs
)を、r1
はオフセットを累積するためにASTを辿る際に使用されます。 -
if(r->op != OXDOT)
:Offsetof
の引数r
がOXDOT
(型チェック前のドットアクセス)でない場合、goto bad
でエラー処理に飛びます。これは、Offsetof
がフィールドセレクタにのみ適用されるべきであることを保証します。 -
typecheck(&r->left, Erv); base = r->left;
: セレクタの左辺(例:s.X
のs
)を型チェックし、その結果をbase
に保存します。これは、後続のtypecheck(&r, Erv)
によってr->left
が変更される可能性があるため、元のベースノードを正確に追跡するために重要です。 -
typecheck(&r, Erv);
:Offsetof
の引数全体(セレクタ)を型チェックします。この型チェックの過程で、埋め込みフィールドの「昇格」が解決され、ASTがs.T.X
のような複数のドットアクセスを含む形に変形される可能性があります。 -
switch(r->op)
ブロック: 型チェック後のr
の操作をチェックします。case ODOT:
: 通常のフィールドアクセス。case ODOTPTR:
: ポインタを介したフィールドアクセス。case OCALLPART:
: メソッド値。これはOffsetof
の引数としては不正なので、yyerror
でエラーを報告し、goto ret
で処理を終了します。default:
: その他の不正な操作の場合、goto bad
でエラー処理に飛びます。
-
v = 0;
: オフセットの合計を格納する変数v
を0で初期化します。 -
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
エラーでコンパイラを停止させます。
-
v += r1->xoffset;
: ループが終了した後、最後にr1
(ベースノードの直前のノード)のxoffset
をv
に加算します。これにより、パス上のすべてのオフセットが正しく累積されます。
この修正により、unsafe.Offsetof
は、埋め込みフィールドの昇格によって生成される複雑なセレクタパスに対しても、構造体の先頭からの正確なオフセットを計算できるようになりました。また、Offsetof
のセマンティクスに反する不正な引数(ポインタ間接参照やメソッド値)を適切に検出してエラーを報告するようになりました。
関連リンク
- Go Issue #4909:
cmd/gc: unsafe.Offsetof(t.x) where x is field of embedded pointer field
- https://github.com/golang/go/issues/4909 - Go CL 8287043:
cmd/gc: fix Offsetof computation.
- https://golang.org/cl/8287043 (これはコミットメッセージに記載されているリンクと同じです)
参考にした情報源リンク
- Go言語の
unsafe
パッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe - Go言語の埋め込みフィールドに関する公式ドキュメントやチュートリアル(例: Go Tourの埋め込みフィールドのセクションなど)
- Goコンパイラの内部構造に関する一般的な情報源(例: Goのソースコード、コンパイラ設計に関する書籍や記事)
- Go言語のASTに関する情報(例:
go/ast
パッケージのドキュメント)