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

[インデックス 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.cDebug=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つのワード(ポインタ)で構成されています。

  1. itab (interface table): 具象型(concrete type)の型情報と、その具象型が実装するメソッドのポインタの配列を含む構造体へのポインタです。これにより、インターフェース変数がどの具象型を保持しているか、そしてその具象型のメソッドをどのように呼び出すかが決定されます。
  2. data (iword): インターフェースが保持する具象値のデータ部分へのポインタ、または具象型の値自体(例えば、intboolのような小さい値の場合)です。このコミットで言及されている「iword」は、このデータ部分を指します。

インターフェースメソッドの呼び出しは、itabを通じて具象型のメソッドを間接的に呼び出すことで実現されます。

funcLayoutの役割

funcLayoutはGoランタイム内部の関数で、特定の関数(またはメソッド)の引数と戻り値がメモリ上でどのように配置されるか(スタックフレームのレイアウト)を計算します。この関数は、スタックフレームのサイズ、引数のオフセット、戻り値のオフセットなどの情報を提供し、動的な関数呼び出しやリフレクションのメカニズムにおいて重要な役割を果たします。

methodReceiverの役割

methodReceiverreflectパッケージ内で使用されるヘルパー関数で、reflect.Valueが表すメソッドのレシーバの型、メソッドのシグネチャ(型)、およびメソッドのコードポインタを取得します。これは、メソッドの動的な呼び出しを準備する際に必要となる情報を提供します。

技術的詳細

問題の核心

このコミットが修正する問題の核心は、Goのreflectパッケージがインターフェースメソッドを動的に呼び出す際の、レシーバの型情報の不整合にありました。

  1. 引数フレーム内のレシーバ: インターフェースメソッドが呼び出される際、Goランタイムは、そのメソッドのレシーバ(つまり、インターフェースが保持する具象値)のデータ部分である「iword」を、関数呼び出しの引数フレームに配置します。この「iword」は、具象値へのポインタ、または具象値そのものです。
  2. funcLayoutの誤解釈: しかし、funcLayout関数は、この引数フレーム内の「iword」を、あたかも「完全なインターフェース値」(itabiwordの両方を含む2ワードの構造体)であるかのように扱おうとしていました。
  3. 型ディスクリプタの不一致: funcLayoutは、関数の引数と戻り値のメモリレイアウトを記述する「型ディスクリプタ」を生成します。このディスクリプタは、ガベージコレクション(GC)などのランタイム操作がスタックフレームを正しくスキャンするために不可欠です。funcLayoutがレシーバを誤って解釈したため、生成される型ディスクリプタが、実際の引数フレーム内のレシーバ(iword)の表現と一致しませんでした。
  4. 結果としての問題: この不一致は、ガベージコレクタがスタックをスキャンする際に誤ったメモリ領域を読み取ったり、ポインタを誤って解釈したりする原因となり、メモリ破壊、クラッシュ、または不正なGC動作につながる可能性がありました。

iwordと完全なインターフェース値の違い

  • iword: インターフェースが内部的に保持する具象値のデータ部分のみを指します。これは1ワード(ポインタまたは直接の値)です。
  • 完全なインターフェース値: インターフェース変数が持つitab(型情報)とiword(データ)の2つの部分全体を指します。これは2ワードの構造体です。

funcLayoutは、スタックフレームのレイアウトを正確に計算するために、レシーバの「具象型」に関する情報が必要です。インターフェースメソッドの場合、レシーバはインターフェース値そのものではなく、そのインターフェースが保持する具象値の「iword」として渡されます。したがって、funcLayoutは「iword」が指す具象型の情報に基づいてレイアウトを計算する必要がありました。以前の実装では、この点が誤解釈されていたため、問題が発生していました。

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

このコミットでは、主にsrc/pkg/reflect/type.gosrc/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()

この変更により、funcLayoutrcvr(レシーバの型)としてインターフェース型を受け取った場合に、即座にパニックが発生するようになりました。これは、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.callcallMethod内で、methodReceiverからの戻り値rcvrtypeを適切に受け取り、funcLayoutに渡すように修正されました。
  • 特に、インターフェースメソッドの場合、methodReceiver内でiface.itab.typrcvrtypeとして設定することで、インターフェースが保持する具象型の情報を正確に取得できるようになりました。

コアとなるコードの解説

このコミットの核心は、funcLayoutがインターフェースメソッドのレシーバを正しく解釈できるように、methodReceiverからより正確な型情報を提供するように変更された点にあります。

  1. funcLayoutpanic追加:

    • このpanicは、funcLayoutがインターフェース型のレシーバを直接受け取ることを防ぐためのガードです。
    • funcLayoutは、スタックフレームのレイアウトを計算するために、レシーバの「具象型」に関する詳細な情報が必要です。インターフェース型そのものでは、その内部にどのような具象型が格納されているか不明なため、正確なレイアウト計算ができません。
    • この変更により、funcLayoutを呼び出す側は、インターフェースから具象型を抽出し、その具象型をfuncLayoutに渡す責任を負うことが明確になりました。これにより、ランタイムの型安全性が向上します。
  2. methodReceiverのシグネチャ変更とrcvrtypeの導入:

    • これが最も重要な変更です。以前のmethodReceiverは、メソッドの型シグネチャ(t)とコードポインタ(fn)のみを返していました。
    • しかし、funcLayoutが正確なスタックフレームレイアウトを計算するためには、メソッドが実際に呼び出されるレシーバの「具象型」の情報が必要でした。
    • 新しい戻り値rcvrtypeが追加されたことで、methodReceiverは、インターフェースメソッドの場合でも、そのインターフェースが保持する具象値の型(iface.itab.typ)を正確に返すことができるようになりました。
    • これにより、funcLayoutは、インターフェースのiwordが指す実際の具象型に基づいて、正しいスタックフレームレイアウトを計算できるようになります。
  3. Value.callcallMethodの修正:

    • これらの関数は、reflectパッケージ内で動的なメソッド呼び出しを処理する主要な部分です。
    • methodReceiverから返される新しいrcvrtypeを適切に利用するように更新されました。
    • 特に、callMethodでは、funcLayoutに渡すレシーバの型として、methodReceiverから取得した正確なrcvrtypeを使用するようになりました。
    • これにより、インターフェースメソッドの呼び出し時に、funcLayoutに渡されるレシーバの型情報が常に正確になり、以前発生していた型ディスクリプタの不一致が解消されました。

これらの変更により、Goのreflectパッケージを用いたインターフェースメソッドの動的な呼び出しが、より堅牢で正確になり、ランタイムの安定性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のreflectパッケージに関する公式ドキュメントやブログ記事
  • Go言語のインターフェースの内部実装に関する資料(例: "Go Data Structures: Interfaces" by Russ Cox)
  • Goランタイムのスタックフレームとガベージコレクションに関する技術文書