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 pathcreater, 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() intset() 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.intertyp := m._type()ni := len(inter.methods) + 1methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.methods[0]))[:ni:ni]var m1 unsafe.Pointerri := 0for li := range inter.methods {lhsMethod := &inter.methods[li]var rhsMethod *methodfor {if ri >= len(typ.methods) {m.methods[1] = nilreturn *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] = nilreturn *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] = m1return ""
}
在函数表生成之后还会将新生成的接口表(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 := itabTableif 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 作类型转换,运行时会动态创建这个接口需要的“函数表”; 并且一个结构可以转换的目标类型,也不仅仅必须是它的基类,从而将不同类型的关系从“继承”关系中解耦出来。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/950267.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!