1. 程式人生 > >linux下go的動態連結庫的使用

linux下go的動態連結庫的使用

在使用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進行傳輸。

由於是自己摸索出來的東西,所以可能有一些地方不正確,希望大家指正,我隨時修改。