在使用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进行传输。
由于是自己摸索出来的东西,所以可能有一些地方不正确,希望大家指正,我随时修改。