[インデックス 19169] ファイルの概要
コミット
reflect: correct type descriptor for call of interface method
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fcf8a775259b491de580048df8720be7acb5799c
元コミット内容
reflect: correct type descriptor for call of interface method
When preparing a call with an interface method, the argument
frame holds the receiver "iword", but funcLayout was being
asked to write a descriptor as if the receiver were a complete
interface value. This was originally caught by running a large
program with Debug=3 in runtime/mgc0.c, but the new panic
in funcLayout suffices to catch the mistake with the existing
tests.
Fixes #7748.
LGTM=bradfitz, iant
R=golang-codereviews, bradfitz, iant
CC=golang-codereviews, khr
https://golang.org/cl/88100048
変更の背景
このコミットは、Go言語のreflect
パッケージにおけるインターフェースメソッドの動的な呼び出しに関するバグを修正するものです。具体的には、インターフェースメソッドを呼び出す際に、ランタイムが引数フレームにレシーバの「iword」(インターフェースが保持する具象値のデータ部分)を配置するにもかかわらず、funcLayout
関数がこのレシーバを「完全なインターフェース値」(型情報とデータポインタの両方を含む構造体)であるかのように扱おうとしていた問題に対処しています。
この不整合により、funcLayout
が生成する型ディスクリプタ(メモリレイアウト情報)が実際の引数フレームの内容と一致せず、ランタイムの整合性が損なわれる可能性がありました。このバグは、当初runtime/mgc0.c
でDebug=3
を設定して大規模なプログラムを実行した際に発見されましたが、このコミットで導入されたfuncLayout
内の新しいパニック(panic
)によって、既存のテストでもこの間違いを捕捉できるようになりました。
この修正は、Goのreflect
パッケージが提供する強力な動的機能の正確性と堅牢性を保証するために不可欠です。
前提知識の解説
Goのreflect
パッケージ
Goのreflect
パッケージは、プログラムの実行時に変数や関数の型情報を動的に検査・操作するための機能を提供します。これにより、Go言語の静的型付けの制約を超えて、柔軟なプログラミングが可能になります。
reflect.Type
: Goの型そのものを表すインターフェースです。型の種類(Kind
)、名前、メソッドなどの情報を提供します。reflect.Value
: Goの変数の値を表す構造体です。値の取得・設定、メソッドの呼び出しなど、値に対する操作を可能にします。reflect.Value.Call
メソッド:reflect.Value
が関数を表す場合、このメソッドを使ってその関数を動的に呼び出すことができます。引数を[]reflect.Value
として渡し、戻り値も[]reflect.Value
として受け取ります。
Goのインターフェースの内部表現
Goのインターフェースは、内部的には2つのワード(ポインタ)で構成されています。
itab
(interface table): 具象型(concrete type)の型情報と、その具象型が実装するメソッドのポインタの配列を含む構造体へのポインタです。これにより、インターフェース変数がどの具象型を保持しているか、そしてその具象型のメソッドをどのように呼び出すかが決定されます。data
(iword): インターフェースが保持する具象値のデータ部分へのポインタ、または具象型の値自体(例えば、int
やbool
のような小さい値の場合)です。このコミットで言及されている「iword」は、このデータ部分を指します。
インターフェースメソッドの呼び出しは、itab
を通じて具象型のメソッドを間接的に呼び出すことで実現されます。
funcLayout
の役割
funcLayout
はGoランタイム内部の関数で、特定の関数(またはメソッド)の引数と戻り値がメモリ上でどのように配置されるか(スタックフレームのレイアウト)を計算します。この関数は、スタックフレームのサイズ、引数のオフセット、戻り値のオフセットなどの情報を提供し、動的な関数呼び出しやリフレクションのメカニズムにおいて重要な役割を果たします。
methodReceiver
の役割
methodReceiver
はreflect
パッケージ内で使用されるヘルパー関数で、reflect.Value
が表すメソッドのレシーバの型、メソッドのシグネチャ(型)、およびメソッドのコードポインタを取得します。これは、メソッドの動的な呼び出しを準備する際に必要となる情報を提供します。
技術的詳細
問題の核心
このコミットが修正する問題の核心は、Goのreflect
パッケージがインターフェースメソッドを動的に呼び出す際の、レシーバの型情報の不整合にありました。
- 引数フレーム内のレシーバ: インターフェースメソッドが呼び出される際、Goランタイムは、そのメソッドのレシーバ(つまり、インターフェースが保持する具象値)のデータ部分である「iword」を、関数呼び出しの引数フレームに配置します。この「iword」は、具象値へのポインタ、または具象値そのものです。
funcLayout
の誤解釈: しかし、funcLayout
関数は、この引数フレーム内の「iword」を、あたかも「完全なインターフェース値」(itab
とiword
の両方を含む2ワードの構造体)であるかのように扱おうとしていました。- 型ディスクリプタの不一致:
funcLayout
は、関数の引数と戻り値のメモリレイアウトを記述する「型ディスクリプタ」を生成します。このディスクリプタは、ガベージコレクション(GC)などのランタイム操作がスタックフレームを正しくスキャンするために不可欠です。funcLayout
がレシーバを誤って解釈したため、生成される型ディスクリプタが、実際の引数フレーム内のレシーバ(iword)の表現と一致しませんでした。 - 結果としての問題: この不一致は、ガベージコレクタがスタックをスキャンする際に誤ったメモリ領域を読み取ったり、ポインタを誤って解釈したりする原因となり、メモリ破壊、クラッシュ、または不正なGC動作につながる可能性がありました。
iword
と完全なインターフェース値の違い
iword
: インターフェースが内部的に保持する具象値のデータ部分のみを指します。これは1ワード(ポインタまたは直接の値)です。- 完全なインターフェース値: インターフェース変数が持つ
itab
(型情報)とiword
(データ)の2つの部分全体を指します。これは2ワードの構造体です。
funcLayout
は、スタックフレームのレイアウトを正確に計算するために、レシーバの「具象型」に関する情報が必要です。インターフェースメソッドの場合、レシーバはインターフェース値そのものではなく、そのインターフェースが保持する具象値の「iword」として渡されます。したがって、funcLayout
は「iword」が指す具象型の情報に基づいてレイアウトを計算する必要がありました。以前の実装では、この点が誤解釈されていたため、問題が発生していました。
コアとなるコードの変更箇所
このコミットでは、主にsrc/pkg/reflect/type.go
とsrc/pkg/reflect/value.go
の2つのファイルが変更されています。
src/pkg/reflect/type.go
func funcLayout
関数に新しいpanic
が追加されました。
--- a/src/pkg/reflect/type.go
+++ b/src/pkg/reflect/type.go
@@ -1833,7 +1833,10 @@ var layoutCache struct {
// the name for possible debugging use.
func funcLayout(t *rtype, rcvr *rtype) (frametype *rtype, argSize, retOffset uintptr) {
if t.Kind() != Func {
- panic("reflect: funcSignature of non-func type")
+ panic("reflect: funcLayout of non-func type")
+ }
+ if rcvr != nil && rcvr.Kind() == Interface {
+ panic("reflect: funcLayout with interface receiver " + rcvr.String())
}
k := layoutKey{t, rcvr}
layoutCache.RLock()
この変更により、funcLayout
がrcvr
(レシーバの型)としてインターフェース型を受け取った場合に、即座にパニックが発生するようになりました。これは、funcLayout
がインターフェースの具象型を期待していることを明確にするためのガードです。
src/pkg/reflect/value.go
func methodReceiver
関数のシグネチャが変更され、新しい戻り値rcvrtype
が追加されました。また、Value.call
メソッドとcallMethod
関数内で、methodReceiver
の呼び出しと戻り値の処理が更新されました。
--- a/src/pkg/reflect/value.go
+++ b/src/pkg/reflect/value.go
@@ -440,9 +440,8 @@ func (v Value) call(op string, in []Value) []Value {
rcvrtype *rtype
)
if v.flag&flagMethod != 0 {
- rcvrtype = t
rcvr = v
- tt, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift)
+ rcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift)
} else if v.flag&flagIndir != 0 {
fn = *(*unsafe.Pointer)(v.ptr)
} else {
@@ -529,8 +528,7 @@ func (v Value) call(op string, in []Value) []Value {
y := (*methodValue)(fn)
if y.fn == methodValueCallCode {
rcvr = y.rcvr
- rcvrtype = rcvr.typ
- tt, fn = methodReceiver("call", rcvr, y.method)
+ rcvrtype, t, fn = methodReceiver("call", rcvr, y.method)
}
// Compute frame type, allocate a chunk of memory for frame
@@ -668,9 +666,10 @@ func callReflect(ctxt *makeFuncImpl, frame unsafe.Pointer) {
// described by v. The Value v may or may not have the
// flagMethod bit set, so the kind cached in v.flag should
// not be used.\n+// The return value rcvrtype gives the method\'s actual receiver type.
// The return value t gives the method type signature (without the receiver).
// The return value fn is a pointer to the method code.
-func methodReceiver(op string, v Value, methodIndex int) (t *rtype, fn unsafe.Pointer) {
+func methodReceiver(op string, v Value, methodIndex int) (rcvrtype, t *rtype, fn unsafe.Pointer) {
i := methodIndex
if v.typ.Kind() == Interface {
tt := (*interfaceType)(unsafe.Pointer(v.typ))
@@ -685,9 +684,11 @@ func methodReceiver(op string, v Value, methodIndex int) (t *rtype, fn unsafe.Po
if iface.itab == nil {
panic("reflect: " + op + " of method on nil interface value")
}
+ rcvrtype = iface.itab.typ
fn = unsafe.Pointer(&iface.itab.fun[i])
t = m.typ
} else {
+ rcvrtype = v.typ
ut := v.typ.uncommon()
if ut == nil || i < 0 || i >= len(ut.methods) {
panic("reflect: internal error: invalid method index")
@@ -746,8 +747,7 @@ func align(x, n uintptr) uintptr {\n // The gc compilers know to do that for the name \"reflect.callMethod\".
func callMethod(ctxt *methodValue, frame unsafe.Pointer) {
rcvr := ctxt.rcvr
- rcvrtype := rcvr.typ
- tt, fn := methodReceiver("call", rcvr, ctxt.method)
+ rcvrtype, t, fn := methodReceiver("call", rcvr, ctxt.method)
frametype, argSize, retOffset := funcLayout(t, rcvrtype)
// Make a new frame that is one word bigger so we can store the receiver.
主な変更点は以下の通りです。
methodReceiver
の戻り値にrcvrtype *rtype
が追加され、メソッドの実際のレシーバの具象型を返すようになりました。Value.call
とcallMethod
内で、methodReceiver
からの戻り値rcvrtype
を適切に受け取り、funcLayout
に渡すように修正されました。- 特に、インターフェースメソッドの場合、
methodReceiver
内でiface.itab.typ
をrcvrtype
として設定することで、インターフェースが保持する具象型の情報を正確に取得できるようになりました。
コアとなるコードの解説
このコミットの核心は、funcLayout
がインターフェースメソッドのレシーバを正しく解釈できるように、methodReceiver
からより正確な型情報を提供するように変更された点にあります。
-
funcLayout
のpanic
追加:- この
panic
は、funcLayout
がインターフェース型のレシーバを直接受け取ることを防ぐためのガードです。 funcLayout
は、スタックフレームのレイアウトを計算するために、レシーバの「具象型」に関する詳細な情報が必要です。インターフェース型そのものでは、その内部にどのような具象型が格納されているか不明なため、正確なレイアウト計算ができません。- この変更により、
funcLayout
を呼び出す側は、インターフェースから具象型を抽出し、その具象型をfuncLayout
に渡す責任を負うことが明確になりました。これにより、ランタイムの型安全性が向上します。
- この
-
methodReceiver
のシグネチャ変更とrcvrtype
の導入:- これが最も重要な変更です。以前の
methodReceiver
は、メソッドの型シグネチャ(t
)とコードポインタ(fn
)のみを返していました。 - しかし、
funcLayout
が正確なスタックフレームレイアウトを計算するためには、メソッドが実際に呼び出されるレシーバの「具象型」の情報が必要でした。 - 新しい戻り値
rcvrtype
が追加されたことで、methodReceiver
は、インターフェースメソッドの場合でも、そのインターフェースが保持する具象値の型(iface.itab.typ
)を正確に返すことができるようになりました。 - これにより、
funcLayout
は、インターフェースのiword
が指す実際の具象型に基づいて、正しいスタックフレームレイアウトを計算できるようになります。
- これが最も重要な変更です。以前の
-
Value.call
とcallMethod
の修正:- これらの関数は、
reflect
パッケージ内で動的なメソッド呼び出しを処理する主要な部分です。 methodReceiver
から返される新しいrcvrtype
を適切に利用するように更新されました。- 特に、
callMethod
では、funcLayout
に渡すレシーバの型として、methodReceiver
から取得した正確なrcvrtype
を使用するようになりました。 - これにより、インターフェースメソッドの呼び出し時に、
funcLayout
に渡されるレシーバの型情報が常に正確になり、以前発生していた型ディスクリプタの不一致が解消されました。
- これらの関数は、
これらの変更により、Goのreflect
パッケージを用いたインターフェースメソッドの動的な呼び出しが、より堅牢で正確になり、ランタイムの安定性が向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/fcf8a775259b491de580048df8720be7acb5799c
- Go Code Review (CL): https://golang.org/cl/88100048
参考にした情報源リンク
- Go言語の
reflect
パッケージに関する公式ドキュメントやブログ記事 - Go言語のインターフェースの内部実装に関する資料(例: "Go Data Structures: Interfaces" by Russ Cox)
- Goランタイムのスタックフレームとガベージコレクションに関する技術文書