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 作类型转换,运行时会动态创建这个接口需要的“函数表”; 并且一个结构可以转换的目标类型,也不仅仅必须是它的基类,从而将不同类型的关系从“继承”关系中解耦出来。
浙公网安备 33010602011771号