这个库听说是哪个版本才加的?是用来加载插件的,简单使用了一下,似乎只能加载 go 自己编译的动态链接库,而且还有核心函数是在 runtime 实现的,这里只能看到表面的简单处理
plugin.go
首先是数据结构
// src/plugin/plugin.go ---- line 20// Plugin is a loaded Go plugin.type Plugin struct {pluginpath stringerr string // set if plugin failed to loadloaded chan struct{} // closed when loadedsyms map[string]interface{}}// src/plugin/plugin.go ---- line 72type Symbol interface{}
Plugin 的 loaded 用处挺大的,相当于是锁,但在这里还讲不太清楚,得看另一个文件的函数才知道。
plugin_dlopen.go
是基于 cgo 实现的噢,先贴出前面的 C 代码
// src/plugin/plugin_dlopen.go ---- line 9/*#cgo linux LDFLAGS: -ldl#include <dlfcn.h>#include <limits.h>#include <stdlib.h>#include <stdint.h>#include <stdio.h>static uintptr_t pluginOpen(const char* path, char** err) {void* h = dlopen(path, RTLD_NOW|RTLD_GLOBAL);if (h == NULL) {*err = (char*)dlerror();}return (uintptr_t)h;}static void* pluginLookup(uintptr_t h, const char* name, char** err) {void* r = dlsym((void*)h, name);if (r == NULL) {*err = (char*)dlerror();}return r;}*/
可以看出其实动态链接库的加载和其中 symbols 的查找都是通过 C 的函数实现的,然后 Go 负责调用和管理。
那么来看一下 Go 是怎么调用和管理的。
// src/plugin/plugin_dlopen.go ---- line 42func open(name string) (*Plugin, error) {cPath := make([]byte, C.PATH_MAX+1)cRelName := make([]byte, len(name)+1)copy(cRelName, name)if C.realpath((*C.char)(unsafe.Pointer(&cRelName[0])),(*C.char)(unsafe.Pointer(&cPath[0]))) == nil {return nil, errors.New(`plugin.Open("` + name + `"): realpath failed`)}filepath := C.GoString((*C.char)(unsafe.Pointer(&cPath[0])))pluginsMu.Lock()if p := plugins[filepath]; p != nil {pluginsMu.Unlock()if p.err != "" {return nil, errors.New(`plugin.Open("` + name + `"): ` + p.err + ` (previous failure)`)}<-p.loadedreturn p, nil}var cErr *C.charh := C.pluginOpen((*C.char)(unsafe.Pointer(&cPath[0])), &cErr)if h == 0 {pluginsMu.Unlock()return nil, errors.New(`plugin.Open("` + name + `"): ` + C.GoString(cErr))}// TODO(crawshaw): look for plugin note, confirm it is a Go plugin// and it was built with the correct toolchain.if len(name) > 3 && name[len(name)-3:] == ".so" {name = name[:len(name)-3]}if plugins == nil {plugins = make(map[string]*Plugin)}pluginpath, syms, errstr := lastmoduleinit()if errstr != "" {plugins[filepath] = &Plugin{pluginpath: pluginpath,err: errstr,}pluginsMu.Unlock()return nil, errors.New(`plugin.Open("` + name + `"): ` + errstr)}// This function can be called from the init function of a plugin.// Drop a placeholder in the map so subsequent opens can wait on it.p := &Plugin{pluginpath: pluginpath,loaded: make(chan struct{}),}plugins[filepath] = ppluginsMu.Unlock()initStr := make([]byte, len(pluginpath)+len("..inittask")+1) // +1 for terminating NULcopy(initStr, pluginpath)copy(initStr[len(pluginpath):], "..inittask")initTask := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&initStr[0])), &cErr)if initTask != nil {doInit(initTask)}// Fill out the value of each plugin symbol.updatedSyms := map[string]interface{}{}for symName, sym := range syms {isFunc := symName[0] == '.'if isFunc {delete(syms, symName)symName = symName[1:]}fullName := pluginpath + "." + symNamecname := make([]byte, len(fullName)+1)copy(cname, fullName)p := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&cname[0])), &cErr)if p == nil {return nil, errors.New(`plugin.Open("` + name + `"): could not find symbol ` + symName + `: ` + C.GoString(cErr))}valp := (*[2]unsafe.Pointer)(unsafe.Pointer(&sym))if isFunc {(*valp)[1] = unsafe.Pointer(&p)} else {(*valp)[1] = p}// we can't add to syms during iteration as we'll end up processing// some symbols twice with the inability to tell if the symbol is a functionupdatedSyms[symName] = sym}p.syms = updatedSymsclose(p.loaded)return p, nil}// src/plugin/plugin_dlopen.go ---- line 144var (pluginsMu sync.Mutexplugins map[string]*Plugin)// lastmoduleinit is defined in package runtimefunc lastmoduleinit() (pluginpath string, syms map[string]interface{}, errstr string)// doInit is defined in package runtime//go:linkname doInit runtime.doInitfunc doInit(t unsafe.Pointer) // t should be a *runtime.initTask
逐块分析一下这个函数。以下的行号都是指上述代码块的行号。
3 - 12 行是调用 C 的 realpath 函数将参数的相对路径转换成绝对路径(这个功能不是直接 Go 也可以完成吗,为啥还要调 C 的,难道其中有什么奥秘?当然我没调试过没有发言权)。
14 - 22 行是检查当前要加载的这个 plugin 在 plugins 中是否已经存在,如果已存在且没有错误且已加载完成,就直接返回,避免重复加载。注意怎么判断已加载完成的,如果没有加载完成那么第 20 行会一直阻塞,直到插件加载完成在第 93 关闭这个 channel,第 20 行才会成功读到一个空值,成功返回已加载的插件。所以说这个加载过程,其实是并发安全的。
37 行调用了一个 runtime 中的函数。这个函数的具体实现也不清楚,得等到看 runtime 的时候再看,在测试的时候,如果加载的不是 Go 的动态链接库就会在这个函数直接 fatal error(至少加载 C 的动态链接库确实直接 fatal error 了)
如果 37 行没有 panic,那基本就是加载成功了,于是接下来的代码就是找出链接库中所有的 symbols 并且保存在一个 map 中。全部完成后在 93 行关闭 channel,于是重复加载也可以正确返回了。
test
似乎 plugin_test.go 中并没有实际的测试代码。于是就给出自己写的简单的测试吧
// add.go// go build --buildmode=plugin -o add.so add.gopackage mainimport "fmt"func Add(a, b int) int {fmt.Printf("In Add: a = %d, b = %d, a + b = %d\n", a, b, a+b)return a+b}
需要注意的是 package main,否则在编译时会得到以下报错 :::warning -buildmode=plugin requires exactly one main package :::
// main.go// go run main.gopackage mainimport ("plugin")func main() {plg, err := plugin.Open("add.so")if err != nil {panic(err)}if sym, err := plg.Lookup("Add"); err == nil {if f, ok := sym.(func(int, int) int); ok {result := f(3, 4)println("3 + 4 =", result)}} else {panic(err)}}
