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

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

このコミットは、Go言語の reflect パッケージにおける「メソッド値 (method values)」の実装に関するものです。具体的には、reflect パッケージが構造体やインターフェースのメソッドを Value 型として表現し、それを関数のように呼び出せるようにするための変更が含まれています。

変更された主なファイルは以下の通りです。

  • src/pkg/reflect/all_test.go: メソッド値の新しいテストケースが多数追加されています。特に TestMethodValue 関数が新設され、様々な型(値、ポインタ、インターフェース)に対するメソッド値の挙動が検証されています。
  • src/pkg/reflect/asm_386.s, src/pkg/reflect/asm_amd64.s, src/pkg/reflect/asm_arm.s: 各アーキテクチャ向けのアセンブリコードで、methodValueCall という新しいスタブが追加されています。これは、リフレクションによって呼び出されるメソッド値の実際の実行パスを担う部分です。
  • src/pkg/reflect/deepequal.go: わずかな変更で、panic("Not reached") が削除されています。これは直接的な機能変更ではなく、コードの整理と思われます。
  • src/pkg/reflect/makefunc.go: makeMethodValue という新しい関数と、methodValue 構造体が追加されています。makeMethodValue は、reflect.Value からメソッド値を作成する中心的なロジックを実装しています。
  • src/pkg/reflect/value.go: reflect.Value 型のメソッド(Call, Interface, Method, Pointer, assignTo, Convert など)が大幅に修正されています。特に、flagMethod という新しいフラグが導入され、Value がメソッド値を表す場合の内部的な処理が変更されています。また、methodReceiver, align, frameSize, callMethod といったヘルパー関数が追加され、メソッド呼び出しの内部メカニズムが再構築されています。

コミット

  • コミットハッシュ: 3be703665eab5cd2bac22e3c928820f58b590c57
  • 作者: Russ Cox rsc@golang.org
  • 日付: Thu Mar 21 16:59:16 2013 -0400

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

https://github.com/golang/go/commit/3be703665eab5cd2bac22e3c928820f58b590c57

元コミット内容

reflect: implement method values

Fixes #1517.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7906043

変更の背景

このコミットの主な目的は、Go言語の reflect パッケージにおいて、メソッド値(method values)の完全なサポートを実装することです。元のコミットメッセージにある Fixes #1517 は、この変更がGoのIssue 1517を解決することを示しています。

Issue 1517は、「reflect.Value.Method が返す ValueInterface() で取得できない」という問題提起でした。Go言語では、T.Method(i)T.MethodByName("Name") のように型からメソッドを取得するだけでなく、v.Method(i)v.MethodByName("Name") のように具体的な値 v からメソッドを取得することができます。後者の場合、取得されたメソッドはレシーバ v にバインドされた関数として振る舞います。これを「メソッド値」と呼びます。

例えば、p := Point{3, 4} という構造体があり、Point 型に Dist(scale int) int というメソッドがあるとします。このとき、p.Distfunc(int) int 型のメソッド値として扱えます。しかし、このメソッド値を reflect.Value として取得した場合、以前の reflect パッケージでは Interface() メソッドを使って実際の func 型のインターフェース値に変換することができませんでした。これは、リフレクションAPIの使い勝手を著しく制限するものでした。

このコミットは、この制約を取り除き、reflect.Value がメソッド値を適切に表現し、Interface()Convert() といった操作を通じて、通常の関数値と同様に扱えるようにすることで、リフレクションの柔軟性と表現力を向上させることを目指しています。これにより、Goの動的なプログラミング能力がさらに強化されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と reflect パッケージの基本的な知識が必要です。

  1. Goの型システムとメソッド:

    • Goの型は、構造体、インターフェース、プリミティブ型など多岐にわたります。
    • メソッドは、特定の型に関連付けられた関数です。レシーバ(receiver)と呼ばれる引数を持ち、そのレシーバの型がメソッドの所属を定義します。
    • 値レシーバとポインタレシーバ: メソッドは値レシーバ (func (t T) M()) またはポインタレシーバ (func (t *T) M()) を持つことができます。
      • 値レシーバのメソッドは、値とポインタの両方から呼び出せます。
      • ポインタレシーバのメソッドは、ポインタからのみ直接呼び出せますが、値から呼び出す場合はGoが自動的にアドレスを取得してポインタに変換します(アドレス可能である場合)。
    • メソッド式 (Method Expressions): T.M のように型からメソッドを参照する形式です。これは func(T, args...) のような通常の関数として扱われ、最初の引数にレシーバを明示的に渡す必要があります。
    • メソッド値 (Method Values): v.M のように具体的な値 v からメソッドを参照する形式です。これはレシーバ v がバインドされた関数として扱われ、レシーバを明示的に渡す必要はありません。例えば、func(args...) のようなシグネチャになります。
  2. reflect パッケージ:

    • reflect パッケージは、Goプログラムが実行時に自身の構造を検査(introspection)し、操作(manipulation)するための機能を提供します。
    • reflect.Type: Goの型の情報を表します。TypeOf(x) で任意のGoの値 xType を取得できます。
    • reflect.Value: Goの値そのものを表します。ValueOf(x) で任意のGoの値 xValue を取得できます。Value は、その値の型、実際のデータ、および様々なフラグ(アドレス可能か、エクスポートされているかなど)を内部に保持します。
    • Value.Call(in []Value) []Value: Value が関数を表す場合、このメソッドを使って関数を呼び出し、引数を []Value で渡し、結果を []Value で受け取ります。
    • Value.Method(i int) Value / Value.MethodByName(name string) Value: Value が構造体やインターフェースを表す場合、これらのメソッドを使ってその型が持つメソッドを reflect.Value として取得します。このコミットの主題である「メソッド値」を生成する部分です。
    • Value.Interface() interface{}: Value が表す値を interface{} 型に変換して返します。このメソッドが、メソッド値に対して正しく機能しないという問題がIssue 1517でした。
    • unsafe パッケージ: reflect パッケージは、Goの型システムを迂回してメモリを直接操作するために unsafe パッケージを多用します。これは、Goの型安全性を損なう可能性があるため、通常は使用を避けるべきですが、reflect のような低レベルなパッケージでは必要不可欠です。
    • アセンブリコード (.s ファイル): reflect パッケージは、動的に生成された関数を呼び出すために、特定のアセンブリコード(スタブ)を利用します。これは、Goの関数呼び出し規約とCの関数呼び出し規約の間の橋渡しや、リフレクションによる動的なディスパッチを実現するために用いられます。

これらの概念を理解することで、コミットがGoのランタイムとリフレクションの内部でどのようにメソッド値が表現され、操作されるかを深く掘り下げていることがわかります。

技術的詳細

このコミットの技術的詳細を理解するには、reflect.Value の内部構造と、メソッド値がどのように「関数」として振る舞うように実装されたかを把握する必要があります。

reflect.Value の内部構造と flagMethod

reflect.Value は、Goの値を抽象化するための構造体です。このコミット以前は、Value がメソッドを表す場合、その Value はレシーバとメソッドのインデックスを内部に保持していました。しかし、これは通常の関数値とは異なる特殊な表現であり、Interface() メソッドで func 型に変換できない原因となっていました。

このコミットでは、reflect.Valueflag フィールドに flagMethod という新しいビットが導入されました。

  • flagMethod がセットされている Value は、特定のレシーバにバインドされたメソッド値であることを示します。
  • この flagMethod がセットされた Value は、その val フィールドにレシーバのデータへのポインタを保持し、flag フィールドの特定のビット(flagMethodShift でシフトされた部分)にメソッドのインデックスを保持します。

makeMethodValue 関数の導入

src/pkg/reflect/makefunc.go に追加された makeMethodValue 関数は、このコミットの核心部分です。この関数は、flagMethod がセットされた reflect.Value(つまり、レシーバにバインドされたメソッドを表す Value)を受け取り、それを実際のGoの関数値に変換します。

変換のプロセスは以下の通りです。

  1. methodValue という新しい内部構造体を定義します。この構造体は、メソッドのコードアドレス (fn)、メソッドのインデックス (method)、およびレシーバの reflect.Value (rcvr) を保持します。
  2. methodValueCall というアセンブリスタブのコードアドレスを取得します。このスタブは、リフレクションによってメソッドが呼び出されたときに実際に実行されるコードです。
  3. methodValue 構造体のインスタンスを作成し、その fn フィールドに methodValueCall のコードアドレスを設定します。
  4. この methodValue 構造体へのポインタを、新しい reflect.Valueval フィールドに設定します。この新しい Value の型は、メソッドの実際の関数型(レシーバを除いた引数と戻り値の型)になります。
  5. これにより、reflect.Value がメソッド値として振る舞う際に、内部的には通常の関数値と同様の表現を持つことができるようになります。

methodValueCall アセンブリスタブと callMethod

src/pkg/reflect/asm_*.s ファイルに追加された methodValueCall は、makeMethodValue によって生成された関数値が呼び出されたときに実行されるアセンブリスタブです。このスタブは、Goの関数呼び出し規約に従って引数を受け取り、最終的に callMethod というGo関数を呼び出します。

src/pkg/reflect/value.go に追加された callMethod 関数は、methodValueCall から呼び出され、実際のメソッド呼び出しを実行します。

  • callMethod は、methodValue 構造体からレシーバとメソッドのインデックスを取得します。
  • methodReceiver ヘルパー関数を使って、レシーバの型、メソッドの実際の関数ポインタ、およびレシーバのデータへのポインタを取得します。
  • frameSize ヘルパー関数を使って、メソッド呼び出しに必要なスタックフレームのサイズを計算します。
  • 引数とレシーバをスタックフレームにコピーし、call 関数(Goの内部的な関数呼び出しメカニズム)を使って実際のメソッドを呼び出します。
  • 戻り値をスタックフレームからコピーして返します。

Value.Interface()Value.Convert() の変更

このコミットの重要な変更点の一つは、Value.Interface()Value.Convert() メソッドが、flagMethod がセットされた Value(メソッド値)を適切に処理するようになったことです。

  • 以前は、メソッド値に対してこれらのメソッドを呼び出すとパニックを起こしていました。
  • 変更後、これらのメソッドは内部的に makeMethodValue を呼び出し、メソッド値を通常の関数値に変換してから interface{} や指定された型に変換するようになりました。これにより、Issue 1517が解決されました。

Value.Method()Value.Pointer() の変更

  • Value.Method() は、メソッド値を作成する際に flagMethod をセットするようになりました。また、nilインターフェースに対する Method 呼び出しでパニックを起こすようになりました。
  • Value.Pointer() は、メソッド値に対して呼び出された場合、methodValueCall アセンブリスタブのコードアドレスを返すようになりました。これは、メソッド値が内部的にこのスタブを介して実行されるためです。

これらの変更により、Goのリフレクションは、メソッド値をファーストクラスの関数値として扱い、より柔軟な動的プログラミングを可能にしました。

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

このコミットのコアとなるコードの変更箇所は、主に src/pkg/reflect/makefunc.gosrc/pkg/reflect/value.go に集中しています。

src/pkg/reflect/makefunc.go の追加

// makeMethodValue converts v from the rcvr+method index representation
// of a method value to an actual method func value, which is
// basically the receiver value with a special bit set, into a true
// func value - a value holding an actual func. The output is
// semantically equivalent to the input as far as the user of package
// reflect can tell, but the true func representation can be handled
// by code like Convert and Interface and Assign.
func makeMethodValue(op string, v Value) Value {
	if v.flag&flagMethod == 0 {
		panic("reflect: internal error: invalid use of makePartialFunc")
	}

	// Ignoring the flagMethod bit, v describes the receiver, not the method type.
	fl := v.flag & (flagRO | flagAddr | flagIndir)
	fl |= flag(v.typ.Kind()) << flagKindShift
	rcvr := Value{v.typ, v.val, fl}

	// v.Type returns the actual type of the method value.
	funcType := v.Type().(*rtype)

	// Indirect Go func value (dummy) to obtain
	// actual code address. (A Go func value is a pointer
	// to a C function pointer. http://golang.org/s/go11func.)
	dummy := methodValueCall
	code := **(**uintptr)(unsafe.Pointer(&dummy))

	fv := &methodValue{
		fn:     code,
		method: int(v.flag) >> flagMethodShift,
		rcvr:   rcvr,
	}

	// Cause panic if method is not appropriate.
	// The panic would still happen during the call if we omit this,
	// but we want Interface() and other operations to fail early.
	methodReceiver(op, fv.rcvr, fv.method)

	return Value{funcType, unsafe.Pointer(fv), v.flag&flagRO | flag(Func)<<flagKindShift}
}

// methodValueCall is an assembly function that is the code half of
// the function returned from makeMethodValue. It expects a *methodValue
// as its context register, and its job is to invoke callMethod(ctxt, frame)
// where ctxt is the context register and frame is a pointer to the first
// word in the passed-in argument frame.
func methodValueCall()

src/pkg/reflect/value.go の変更

Value.Interface() メソッドの変更:

--- a/src/pkg/reflect/value.go
+++ b/src/pkg/reflect/value.go
@@ -916,11 +916,9 @@ func valueInterface(v Value, safe bool) interface{} {
 	if v.flag == 0 {
 		panic(&ValueError{"reflect.Value.Interface", 0})
 	}
-	if v.flag&flagMethod != 0 {
-		panic("reflect.Value.Interface: cannot create interface value for method with bound receiver")
-	}
-
 	if safe && v.flag&flagRO != 0 {
 		// Do not allow access to unexported values via Interface,
 		// because they might be pointers that should not be
 		// writable or methods or function that should not be callable.
 		panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
 	}
+	if v.flag&flagMethod != 0 {
+		v = makeMethodValue("Interface", v)
+	}

Value.Convert() メソッドの変更:

--- a/src/pkg/reflect/value.go
+++ b/src/pkg/reflect/value.go
@@ -2061,7 +2061,7 @@ func (v Value) Convert(t Type) Value {
 // of the value v to type t, Convert panics.
 func (v Value) Convert(t Type) Value {
 	if v.flag&flagMethod != 0 {
-		panic("reflect.Value.Convert: cannot convert method values")
+		v = makeMethodValue("Convert", v)
 	}
 	op := convertOp(t.common(), v.typ)
 	if op == nil {

Value.Method() メソッドの変更:

--- a/src/pkg/reflect/value.go
+++ b/src/pkg/reflect/value.go
@@ -1105,7 +1105,10 @@ func (v Value) Method(i int) Value {
  	if v.flag&flagMethod != 0 || i < 0 || i >= v.typ.NumMethod() {
  		panic("reflect: Method index out of range")
  	}
-	fl := v.flag & (flagRO | flagAddr | flagIndir)
+	if v.typ.Kind() == Interface && v.IsNil() {
+		panic("reflect: Method on nil interface value")
+	}
+	fl := v.flag & (flagRO | flagIndir)
  	fl |= flag(Func) << flagKindShift
  	fl |= flag(i)<<flagMethodShift | flagMethod
  	return Value{v.typ, v.val, fl}

新しいヘルパー関数 methodReceiver, align, frameSize, callMethod の追加:

// methodReceiver returns information about the receiver
// 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.
func methodReceiver(op string, v Value, methodIndex int) (t *rtype, fn unsafe.Pointer, rcvr iword) {
	i := methodIndex
	if v.typ.Kind() == Interface {
		tt := (*interfaceType)(unsafe.Pointer(v.typ))
		if i < 0 || i >= len(tt.methods) {
			panic("reflect: internal error: invalid method index")
		}
		m := &tt.methods[i]
		if m.pkgPath != nil {
			panic("reflect: " + op + " of unexported method")
		}
		t = m.typ
		iface := (*nonEmptyInterface)(v.val)
		if iface.itab == nil {
			panic("reflect: " + op + " of method on nil interface value")
		}
		fn = unsafe.Pointer(&iface.itab.fun[i])
		rcvr = iface.word
	} else {
		ut := v.typ.uncommon()
		if ut == nil || i < 0 || i >= len(ut.methods) {
			panic("reflect: internal error: invalid method index")
		}
		m := &ut.methods[i]
		if m.pkgPath != nil {
			panic("reflect: " + op + " of unexported method")
		}
		fn = unsafe.Pointer(&m.ifn)
		t = m.mtyp
		rcvr = v.iword()
	}
	return
}

// align returns the result of rounding x up to a multiple of n.
// n must be a power of two.
func align(x, n uintptr) uintptr {
	return (x + n - 1) &^ (n - 1)
}

// frameSize returns the sizes of the argument and result frame
// for a function of the given type. The rcvr bool specifies whether
// a one-word receiver should be included in the total.
func frameSize(t *rtype, rcvr bool) (total, in, outOffset, out uintptr) {
	if rcvr {
		// extra word for receiver interface word
		total += ptrSize
	}

	nin := t.NumIn()
	in = -total
	for i := 0; i < nin; i++ {
		tv := t.In(i)
		total = align(total, uintptr(tv.Align()))
		total += tv.Size()
	}
	in += total
	total = align(total, ptrSize)
	nout := t.NumOut()
	outOffset = total
	out = -total
	for i := 0; i < nout; i++ {
		tv := t.Out(i)
		total = align(total, uintptr(tv.Align()))
		total += tv.Size()
	}
	out += total

	// total must be > 0 in order for &args[0] to be valid.
	// the argument copying is going to round it up to
	// a multiple of ptrSize anyway, so make it ptrSize to begin with.
	if total < ptrSize {
		total = ptrSize
	}

	// round to pointer
	total = align(total, ptrSize)

	return
}

// callMethod is the call implementation used by a function returned
// by makeMethodValue (used by v.Method(i).Interface()).
// It is a streamlined version of the usual reflect call: the caller has
// already laid out the argument frame for us, so we don't have
// to deal with individual Values for each argument.
// It is in this file so that it can be next to the two similar functions above.
// The remainder of the makeMethodValue implementation is in makefunc.go.
func callMethod(ctxt *methodValue, frame unsafe.Pointer) {
	t, fn, rcvr := methodReceiver("call", ctxt.rcvr, ctxt.method)
	total, in, outOffset, out := frameSize(t, true)

	// Copy into args.
	//
	// TODO(rsc): This will need to be updated for any new garbage collector.
	// For now make everything look like a pointer by allocating
	// a []unsafe.Pointer.
	args := make([]unsafe.Pointer, total/ptrSize)
	args[0] = unsafe.Pointer(rcvr)
	base := unsafe.Pointer(&args[0])
	memmove(unsafe.Pointer(uintptr(base)+ptrSize), frame, in)

	// Call.
	call(fn, unsafe.Pointer(&args[0]), uint32(total))

	// Copy return values.
	memmove(unsafe.Pointer(uintptr(frame)+outOffset-ptrSize), unsafe.Pointer(uintptr(base)+outOffset), out)
}

コアとなるコードの解説

makeMethodValue

この関数は、reflect.Value がメソッド値(特定のレシーバにバインドされたメソッド)を表す場合に、それを通常のGoの関数値に変換する役割を担います。

  1. v.flag&flagMethod == 0 のチェック: 入力された Value が本当にメソッド値であるかを確認します。そうでなければ内部エラーとしてパニックします。
  2. rcvr の作成: 入力 v からレシーバの Value を再構築します。flagMethod ビットはメソッド値であることを示すものであり、レシーバ自体の型情報とは関係ないため、このビットを除外してレシーバの Value を作成します。
  3. funcType の取得: v.Type() は、メソッド値の実際の関数型(レシーバを除いた引数と戻り値の型)を返します。この型が、新しく作成される関数値の型となります。
  4. methodValueCall のコードアドレス取得: methodValueCall はアセンブリで書かれたスタブ関数で、リフレクション経由でメソッドが呼び出されたときに実行されるエントリポイントです。unsafe.Pointer を使ってそのコードアドレスを取得し、fv.fn に設定します。
  5. methodValue 構造体の初期化: methodValue は、メソッドの実行に必要な情報(コードアドレス、メソッドインデックス、レシーバ)を保持する内部構造体です。このインスタンスを作成し、fv に代入します。
  6. methodReceiver による事前チェック: methodReceiver を呼び出すことで、メソッドがエクスポートされているか、nilインターフェースのメソッドでないかなどのチェックを事前に行い、不適切な場合は早期にパニックさせます。これにより、Interface() などの操作が失敗する前に問題を検出できます。
  7. 新しい Value の返却: 最終的に、funcType を型とし、fv へのポインタを値として持つ新しい reflect.Value を作成して返します。この Value は、flagMethod がセットされていない通常の関数値として扱えるようになります。

Value.Interface() および Value.Convert() の変更

これらの変更は非常にシンプルですが、機能的には重要です。以前は、Value がメソッド値である場合、これらのメソッドは「メソッド値のインターフェース値を作成できない」といったパニックを起こしていました。

変更後、v.flag&flagMethod != 0 の条件が真の場合、つまり v がメソッド値である場合、makeMethodValue を呼び出して v を通常の関数値に変換してから、それぞれの処理(interface{} への変換や型変換)を続行するようになりました。これにより、メソッド値が他の関数値と同様に扱えるようになり、Issue 1517が解決されました。

Value.Method() の変更

Value.Method() は、指定されたインデックスのメソッドに対応する reflect.Value を返します。

  • 変更前は、返される Value は特殊な内部表現を持っていました。
  • 変更後、返される ValueflagflagMethod ビットがセットされるようになりました。これにより、この Value がメソッド値であることを明示的に示し、後続の makeMethodValue による変換のトリガーとなります。
  • また、v.typ.Kind() == Interface && v.IsNil() のチェックが追加され、nilインターフェースに対するメソッド呼び出しがパニックするようになりました。これは、Goの通常のセマンティクスに合わせた挙動です。

methodReceiver, align, frameSize, callMethod

これらの関数は、リフレクションによるメソッド呼び出しの低レベルな詳細を処理するためのヘルパーです。

  • methodReceiver: Value とメソッドインデックスから、メソッドの実際の型、関数ポインタ、レシーバのデータポインタを取得します。インターフェース型と具象型で異なるロジックを持ちます。
  • align: メモリのアライメントを調整するためのユーティリティ関数です。
  • frameSize: 関数呼び出しに必要なスタックフレームのサイズ(引数と戻り値を含む)を計算します。レシーバの有無も考慮します。
  • callMethod: makeMethodValue によって生成された関数値が呼び出されたときに、methodValueCall アセンブリスタブから呼び出されるGo関数です。
    1. methodValue コンテキストからレシーバとメソッドインデックスを取得します。
    2. methodReceiver を使って、呼び出すべき実際のメソッドの情報を取得します。
    3. frameSize を使って、呼び出しに必要なスタックフレームのサイズを計算します。
    4. 引数とレシーバをスタックフレームにコピーします。
    5. Goの内部的な call 関数を使って、実際のメソッドを呼び出します。
    6. 戻り値をスタックフレームからコピーして返します。

これらの低レベルな変更により、Goの reflect パッケージは、メソッド値をファーストクラスの関数として扱い、動的なメソッド呼び出しや変換をよりシームレスに行えるようになりました。

関連リンク

参考にした情報源リンク