linux下go的动态链接库的使用

转自:http://blog.csdn.net/xtxy/article/details/21328143

在使用lua进行服务器端游戏逻辑开发时,发现了LUA的各种不方便的地方,不能编译检查,不能断点调试,笔误的函数和变量不提示出错等等,所以有了全部使用go来做服务器端开发的想法。

如果不需要热更新,那使用go开发服务器逻辑是很轻松的,而游戏服务器特别是页游,一般都需要支持热更新,所以我决定使用go的动态链接库方式来实现,也就是底层框架是go,上层逻辑是go的动态链接库。go原生不支持动态链接库,在查阅了很多文章之后,决定使用gccgo来实现。

经过了大约一周的时间,终于把框架搭建起来了,期间遇到了一些比较坑的问题,记录在此,以便以后不会再犯,也可以帮助其他有同样需求的同学快速搭建这样的框架。

这个例子需要了解go目录构建和环境变量的知识,如果不了解,可以先看看网上的文章,很简单的。

 

整个框架搭建好了之后的方式是GOCCGO,就是GO -> C -- dll --> C -> GO。底层和最上层都是GO,中间使用了C来提供动态链接库的方式。

 

1 首先来看底层特殊的地方,就是GO->C的部分。

go文件如下:

// script.go

package script

// 1
//extern initDll
func c_initDll(string, string)

//extern runDll
func c_runDll(string, map[string]interface{}) string

var dataMap map[string]interface{} // 2

func Init(fileName string, funcName string) {
    dataMap = make(map[string]interface{})

    return c_initDll(fileName, funcName)
}

func Run(buf []byte) string {
    str := string(buf)

    retStr := c_runDll(str, dataMap)

    return retStr
}

其中1处是gccgo的特殊写法,//extern funcName就是说有一个c的函数,在go中使用c_funcName来调用它。(是不是能使用其他名字我没有试过)

 

2处是声明了一个map结构,这个后面再说,此时先不管。

文件中的Init函数在程序启动时初始化时调用,用来加载dll;Run函数就是调用dll来处理每次的游戏逻辑了。

 

对应的c文件如下:

// script.c

#include <dlfcn.h>

struct __go_string {    // 1
    const unsigned char *__data;
    int __length;
};

struct __go_string (*dllEntry)(struct __go_string, int); // 2

void loadDll(struct __go_string fileName, struct __go_string funcName)
{
    void *dll = dlopen(fileName.__data, RTLD_LAZY); // 3
    if(!dll)
    {
        // error
    }
    
    dlerror();
    
    *(void **)(&dllEntry) = dlsym(dll, funcName.__data); // 4
    if(NULL != dlerror())
    {
        // error
    }
}

struct __go_string runDll(struct __go_string inData, void *dataMap)
{
    return dllEntry(inData, dataMap); // 5
}

其中1处是go中string类型在c中的表示,string在c中是一个结构体。

 

2处是动态链接库的主函数,动态链接库导出这个函数来对游戏逻辑进行处理。

3处和4处分别是加载动态链接库和获取函数地址的代码。

5处就是调用动态链接库的主函数的地方,例子中传递参数为string和一个指针(其实就是Map的地址),返回值也是一个string。

 

编译他们的方法如下:

gccgo -o testc.o -c test.c

gccgo -o testgo.o -c test.go

ar cr libtest.a testgo.o testc.o

最后编译出来的libtest.a,需要放到pkg目录下面相应的地方。也就是你使用go install命令时它将生成出来的库放到哪里,你就讲libtest.a拷贝到哪里。

GO->C的部分就已经完成了。

 

2 动态链接库的接口, C->GO的部分

c文件如下:

// dll.c

struct __go_string {
    const unsigned char *__data;
    int __length;
};

extern struct __go_string go_entry(struct __go_string, int, void *) __asm__ ("GameLogic_ctrl.Entry"); // 1

// void __attach(void) __attribute__ ((constructor)); // 2

struct __go_string centry(struct __go_string input, void *dataMap)
{
    return go_entry(input, dataMap);
}

1处是C调用GO函数的声明,Entry是函数名称,GamLogic_ctrl是包名称,如果这个地方出错的话,你可以用nm命令查看go生成的lib文件,将包名称修改正确。

 

2是动态链接库的初始化函数声明,如果需要,就可以添加一个,调用go函数,做一些初始化操作。

 

对应的go文件如下:

// dllMain.go

package ctrl

func Entry(inData string, index int, dataMap map[string]interface{}) string {
    return ""
}

和普通的go文件写法是一样的。

 

 

编译他们的方法如下:

go install -compiler=gccgo -gccgoflags=‘-fPIC‘ // 将所有的go文件编译出来

gccgo -o libcdll.a -c dll.c -fPIC // 将dll入口的c文件编译出来

然后将libcdll.a拷贝到其他go生成的库的目录下,执行:

gccgo -shared -o dllMain.so *.a // 将单个库文件编译成最终的动态链接库

 

整个框架动态链接库部分就完成了。

 

下面我说一下使用这个框架需要注意的地方:

1. 编译动态链接库时除了c部分外,其他不要直接使用gccgo命令,因为一些外部库,比如访问mongodb的mgo库,你是无法使用gccgo编译出来的(或者说很困难),最简单的方法就是用go install -compiler=gccgo的方式;

2. 动态链接库不会执行初始化部分,也就是说在package里面的init函数并不会被调用;

3. 和上面一条比较像,import的外部库(包括系统库),比如fmt并不会被import进来。解决方法是在基础框架部分(也就是可执行文件),将动态链接库需要用到的库全部import进来;

4. 动态链接库里面申请的内存在调用完成后会被释放。比如在动态链接库主函数文件(上面最后一个包含Entry函数的文件)里面加一个包里面的全局变量:var tmpMap = make(map[string]interface{}),如果在Entry函数里面访问tmpMap,你会发现tmpMap指向的map空间已经被释放了,访问它会报段错误。解决方法是在基础框架里声明一个map结构,然后传递给动态链接库使用,这个map数据就不会被释放。也就是在我列举的第一个文件(script.go)的注释2处,声明了一个dataMap,就是起这个作用。

5. 在gccgo的说明文档里,slice是可以在c和go之间作为函数参数传递的,但是我试过了之后,发现有问题。首先是go的slice到c里面之后,并不是一个struct,而是一个struct *,就是说是一个指针,而且我将指针指向的内存打印出来,和文档上说的完全对不上,找不到字符数组,找不到len的位置,cap的位置似乎在这个指针向上偏移4个字节……我被搞蒙了,就没有研究了,用string来代替slice进行传输。

 

由于是自己摸索出来的东西,所以可能有一些地方不正确,希望大家指正,我随时修改。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。