Blog的high玩页,准备采用插件的方式完成,就需要能够本地编写插件代码,然后上传到服务端编译(也可以选择不编译),然后做成插件的方式放到服务端,后续有点击的时候,才会加载到内存中,访问完毕自动卸载。这样有几个好处:
比较省内存,有的插件比较巨大,比如后续介绍的搭建的tensorfow深度平台,会占用大量的内存,所以不可能静态加载。
不会和Blog的代码有重叠,blog是blog、插件是插件,两者互相不干预。
以插件的方式,可以和blog运行隔离,在加载插件的时候,可以让插件以多进程或者多线程的方式运行,在上层做隔离,不会让插件的运行状态影响blog的运行状态,比如插件的crash引起blog服务器的crash。
方便做成集群的方式,比如擦肩单独放在某几台服务器上,通过blog通过RPC访问插件。
基于这点,所以想做成动态加载的策略,由于blog是用Golang写的,golang本身是静态语言,想动态加载就要依靠动态加载库(so或者dll文件),在Golang 1.5之前的版本是不支持golang编译成so的,在1.5后,增加了-buildmode=c-shared,支持将go编译成so,于是就写了两个工程进行测试。
so部分代码如下:
package main
import "C"
import "unsafe"
type TestStruct struct {
A int
B string
C func() string
}
func NewTestStruct() *TestStruct {
return &TestStruct{A: 1234567890, B: "哟哟,切克闹,煎饼果子来一套。", C: func() string { return "我是一个粉刷匠,粉刷本领强" }}
}
type Test interface {
TestReturnString() string
TestReturnInt() int
TestReturnFloat64() float64
TestReturnStruct() TestStruct
}
type TestImpl struct {
}
func (a *TestImpl) TestReturnString() string {
return "ReturnInterface"
}
func (a *TestImpl) TestReturnInt() int {
return 9876543210
}
func (a *TestImpl) TestReturnFloat64() float64 {
return 123.456789
}
func (a *TestImpl) TestReturnStruct() TestStruct {
return *NewTestStruct()
}
//export ReturnInterface
func ReturnInterface() *C.char {
a := (Test)(new(TestImpl))
m := &a
return (*C.char)(unsafe.Pointer(m))
}
func main() {
}
因为之前测试过基本类型(比如string、int等)通过golang进行转化,是可以的,所以想测试下复杂类型(interface{})的转化。
执行go build -v -x -buildmode=c-shared -o lib.so,查看编译产物,发现生成了so和一个.h文件,.h文件代码如下
/* Created by "go tool cgo" - DO NOT EDIT. */
/* package _/home/wind/Project/go-lib/lib */
/* Start of preamble from import "C" comments. */
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
#endif
/* End of boilerplate cgo prologue. */
#ifdef __cplusplus
extern "C" {
#endif
extern char* ReturnInterface();
#ifdef __cplusplus
}
#endif
里面可以看到有导出ReturnInterface,为了是char*,因为我在cgo暂时没有找到C.void这个结构,所以用char*代替,本质上使用void*或者char*,都是一样的。
同样也可以看到里面定义了GoString, GoChan, GoInterface等go的基本类型,看到这里,感觉插件有戏,然后翻了cgo的文档,发现只有基本类型的C/GO互相转换,然后都没有提到GoInterface等结构。然后想的方法是在C中执行ReturnInterface,然后在Golang中强制转换为Golang的interface{},再转为Test,代码如下:
package main
/*
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#cgo LDFLAGS: -ldl -s -w
typedef void* (*ReturnInterface)();
void* ReturnCPointer(void* t) {
return ((ReturnInterface)t)();
}
*/
import "C"
import "fmt"
import "unsafe"
type TestStruct struct {
A int
B string
C func() string
}
type Test interface {
TestReturnString() string
TestReturnInt() int
TestReturnFloat64() float64
TestReturnStruct() TestStruct
}
func LoadLibrary_Interface(path string, functionName string) {
handle := C.dlopen(C.CString(path), C.RTLD_LAZY)
defer C.dlclose(handle)
if handle == nil {
panic("dlopen error")
}
function := C.dlsym(handle, C.CString(functionName))
if function == nil {
panic("dlsym error")
}
test := unsafe.Pointer(C.ReturnCPointer(function))
m := (*Test)(test)
fmt.Println((*m).TestReturnString())
}
func main() {
LoadLibrary_Interface("./lib/lib.so", "ReturnInterface")
}
然后编译该文件,执行,发现在mac上根本就跑不起来,提示runtime/cgo: could not obtain pthread_keys,然后查资料,翻代码,发现编译so不是为go与go互调准备的,只要是C与go互调,崩溃的地方在tls部分,大概是起了两个golang的core,然后互相冲突了,导致了第二次创建tls key的时候崩溃了。
然后将代码移到centos上执行,发现调用so成功了,但是又报了其他的错:
panic: runtime error: cgo result has Go pointer
goroutine 17 [running, locked to thread]:
panic(0x7feaf069a2a0, 0x1c42000c160)
/home/wind/Application/go/src/runtime/panic.go:500 +0x1a5
main._cgoexpwrap_2164936ba859_ReturnInterface.func1(0x1c420036e80)
_/home/wind/Project/go-lib/lib/_obj/_cgo_gotypes.go:48 +0x3c
main._cgoexpwrap_2164936ba859_ReturnInterface(0x1c42000c150)
_/home/wind/Project/go-lib/lib/_obj/_cgo_gotypes.go:50 +0xa4
已放弃
意思是说,在cgo中执行的函数的结果,不能含有Go的指针,这样就堵死了通过这种方式进行互调的路。
然后准备换一条路,找了一个三方库github.com/rainycape/dl,里面不是使用cgo来调用go方法获取指针,是直接通过汇编来获取函数的指针进行执行,然后返回golang的函数,通过golang调用。下载了该库,在mac上依旧报错,centos上依旧跑不起来。
最后翻到一个哥们说的话:
By the way, I want to be clear that -buildmode=c-shared is not
intended to build a plugin library for a Go program. Even if you fix
this problem there may be future problems. Those problems are bugs
that should be fixed, but I want to caution you that you need to be
prepared to run into problems.
说的是-buildmode=c-shared本身就不是给go编译的so,会出各种问题。不过不准备放弃,准备尝试其他方式动态调用。
Golang玩下去,还是会发现很多坑的,比如我碰到的这个问题。在其他语言,比如Java, C#, Objc等主流语言,都能很好的通过动态加载方案进行互调,Golang还是有很长的路要走啊。