gccgo如何实现golang运行时向特定interface的动态conversion(及和C++虚函数表的对比)

intro

在阅读k8s的源代码时,发现代码中有一个(一组)很简单粗暴的interface转换:将一个interface(storage对象)尝试向各种类型转换。golang中每个结构的method并不是在结构内定义,在类型转换的时候,运行时只要判断转换源实现了目的类型的所有method就可以成功完成转换。

k8s的例子里,总体来说是对一个struct所有可能实现的method都逐个尝试判断是否有对应实现。在这里的典型场景中,这个storage对象可能的确是实现了所有的interface(rest.Creater, rest.Lister, etc...)。

在之前的这篇文章,说明了gcc的对于interface的转换,本质上也是返回一个包含函数指针的数组(类似于C++的虚函数表。注意:函数表中函数的位置顺序要和目标interface的顺序一致)。

但是和C++需函数定义在一个类内部不同,一个golang struct如果实现了n个method,那么所有组合可能的不同interface类型数量就有(n + 1)!个那么多。面对如此众多的“可能的”interface,golang不可能和C++一样,在编译时为所有类都生成一个徐函数表。

那么:golang如何应对这么多中可能的转换呢?

///@file: kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, *storageversion.ResourceInfo, error) {
	admit := a.group.Admit
///...
	// what verbs are supported by the storage, used to know what verbs we support per path
	creater, isCreater := storage.(rest.Creater)
	namedCreater, isNamedCreater := storage.(rest.NamedCreater)
	lister, isLister := storage.(rest.Lister)
	getter, isGetter := storage.(rest.Getter)
	getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)
	gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
	collectionDeleter, isCollectionDeleter := storage.(rest.CollectionDeleter)
	updater, isUpdater := storage.(rest.Updater)
	patcher, isPatcher := storage.(rest.Patcher)
	watcher, isWatcher := storage.(rest.Watcher)
	connecter, isConnecter := storage.(rest.Connecter)
	storageMeta, isMetadata := storage.(rest.StorageMetadata)
	storageVersionProvider, isStorageVersionProvider := storage.(rest.StorageVersionProvider)
	gvAcceptor, _ := storage.(rest.GroupVersionAcceptor)
///...
}

测试

下面是在compiler explorer上的简单测试代码,就是单纯的做一下接口转换:

/// https://godbolt.org/z/x9Ts8v458

// Type your code here, or load an example.
package p
type IF interface {
     get() int
     set() int
}
type ANY interface {

}

func square(x ANY) int {
    y := x.(IF)
    return y.get()
}

生成gimple代码,关键的接口类型转换调用的是runtime.assertitab,函数调用传入的两个参数是编译时可以确定的目标interface类型(go.p,IF..d)和转换源对象自带的类型描述符(x.__type_descriptor;)。

p.square (struct ANY x)
{
  int D.417;
  int $ret0;

  $ret0 = 0;
  {
    struct IF y;

    try
      {
        _1 = x.__type_descriptor;
        _2 = runtime.assertitab (&go.p.IF..d, _1);
        y.__methods = _2;
        _3 = x.__object;
        y.__object = _3;
        {
          _4 = y.__methods;
          _5 = _4->get;
          _6 = y.__object;
          GOTMP.0 = _5 (_6);
          $ret0 = GOTMP.0;
          D.417 = $ret0;
          return D.417;
        }
      }
    finally
      {
        y = {CLOBBER};
      }
  }
}

assertitab

gcc包含的golang运行时代码assertitab=>>getitab==>>init也比较简单,就是拿两个类型表进行类型比较,在运行时根据转换目标需要的method来填充定制的函数表。

///@file: /home/tsecer/source/gcc/libgo/go/runtime/iface.go
// init fills in the m.methods array with all the code pointers for
// the m.inter/m._type pair. If the type does not implement the interface,
// it sets m.methods[1] to nil and returns the name of an interface function that is missing.
// It is ok to call this multiple times on the same m, even concurrently.
func (m *itab) init() string {
	inter := m.inter
	typ := m._type()
	ni := len(inter.methods) + 1
	methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.methods[0]))[:ni:ni]
	var m1 unsafe.Pointer

	ri := 0
	for li := range inter.methods {
		lhsMethod := &inter.methods[li]
		var rhsMethod *method

		for {
			if ri >= len(typ.methods) {
				m.methods[1] = nil
				return *lhsMethod.name
			}

			rhsMethod = &typ.methods[ri]
			if (lhsMethod.name == rhsMethod.name || *lhsMethod.name == *rhsMethod.name) &&
				(lhsMethod.pkgPath == rhsMethod.pkgPath || *lhsMethod.pkgPath == *rhsMethod.pkgPath) {
				break
			}

			ri++
		}

		if !eqtype(lhsMethod.typ, rhsMethod.mtyp) {
			m.methods[1] = nil
			return *lhsMethod.name
		}

		if li == 0 {
			m1 = rhsMethod.tfn // we'll set m.methods[1] at the end
		} else {
			methods[li+1] = rhsMethod.tfn
		}
		ri++
	}
	m.methods[1] = m1
	return ""
}

在函数表生成之后还会将新生成的接口表(itab)缓存起来,从而避免下次转换时再重新生成。

///@file: /home/tsecer/source/gcc/libgo/go/runtime/iface.go
// itabAdd adds the given itab to the itab hash table.
// itabLock must be held.
func itabAdd(m *itab) {
	// Bugs can lead to calling this while mallocing is set,
	// typically because this is called while panicing.
	// Crash reliably, rather than only when we need to grow
	// the hash table.
	if getg().m.mallocing != 0 {
		throw("malloc deadlock")
	}

	t := itabTable
	if t.count >= 3*(t.size/4) { // 75% load factor
		// Grow hash table.
		// t2 = new(itabTableType) + some additional entries
		// We lie and tell malloc we want pointer-free memory because
		// all the pointed-to values are not in the heap.
		t2 := (*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize, nil, true))
		t2.size = t.size * 2

		// Copy over entries.
		// Note: while copying, other threads may look for an itab and
		// fail to find it. That's ok, they will then try to get the itab lock
		// and as a consequence wait until this copying is complete.
		iterate_itabs(t2.add)
		if t2.count != t.count {
			throw("mismatched count during itab table copy")
		}
		// Publish new hash table. Use an atomic write: see comment in getitab.
		atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
		// Adopt the new table as our own.
		t = itabTable
		// Note: the old table can be GC'ed here.
	}
	t.add(m)
}

outro

如果从C++的角度来看golang把method散落在结构之外的写法有些诡异,但是从这个例子可以看到:这种实现配合上golang的反射机制,也可以实现跟C++相比更加灵活一些的接口转换机制。尽管是在C++的基础上增加了运行时性能为代价,但在一些性能不是最核心需求的长江下,这点“牺牲”是可以接受的。

基于这个机制可以想到的一个重要机制就是“解耦”:一个结构只要定义了某个interface的所有method,它就能在“运行时”向这个interface 作类型转换,运行时会动态创建这个接口需要的“函数表”; 并且一个结构可以转换的目标类型,也不仅仅必须是它的基类,从而将不同类型的关系从“继承”关系中解耦出来。

posted on 2025-10-29 21:22  tsecer  阅读(0)  评论(0)    收藏  举报

导航