Golang动态加载方案踩坑记

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 
#include 
#include 
#include 
#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还是有很长的路要走啊。

你可能感兴趣的:(Go)