calling c++ from golang with swig--windows dll 三
使用動態鏈接庫(DLL)主要有兩種方式:一種通過鏈接導入庫,在代碼中直接調用DLL中的函數;另一種借助LoadLibrary/LoadLibraryEx,GetProcessAddress函數在代碼中間接調用DLL中的函數。這兩種使用方式對應兩種動態鏈接,分別稱為: load-time dynamic link (加載時動態鏈接)和run-time dynamic link (運行時動態鏈接)。
DLL工程編譯后產生*.dll文件和同名的.lib文件, .lib文件是導入庫,使用加載時鏈接的應用工程可以在代碼中顯式調用DLL中的函數,而且必須要用導入庫*.lib鏈接。當系統啟動一個使用加載時動態鏈接的程序時,它將利用鏈接器放置在文件中的信息來定位進程使用的DLL(可執行程序導入的)中的名稱(DLL導出函數)。然后系統搜索DLL文件。如果系統無法找到所需的DLL,則會終止該進程,並向用戶顯示一個報告錯誤的對話框。否則,系統將DLL映射到進程的虛擬地址空間,並增加DLL引用計數。系統調用入口點函數(end-point function)。如果入口點函數沒有返回TRUE,系統將終止進程並報告錯誤。最后,系統使用導入的DLL函數起始地址來修改函數地址表。(參閱msdn:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms684184(v=vs.85).aspx)
使用運行時動態鏈接,當應用程序調用LoadLibrary或LoadLibraryEx函數時,系統將嘗試找到DLL。如果搜索成功,系統將DLL模塊映射到進程的虛擬地址空間,並將引用計數遞增。然后系統在調用LoadLibrary或LoadLibraryEx的線程上下文中調用入口點函數。成功后LoadLibray或LoadLibraryEx返回一個指向DLL的句柄。進程可以在GetProcAddress、FreeLibrary函數調用中使用這個句柄來標識DLL。進程調用GetProcessAddress獲取一個DLL導出函數的地址。當DLL模塊不再需要時,進程可以調用FreeLibrary或FreeLibraryExAndExitThread。這些函數將減少模塊引用計數並從進程的虛擬地址空間中取消映射的DLL代碼。(參閱msdn:https://msdn.microsoft.com/en-us/library/windows/desktop/ms682596(v=vs.85).aspx)
可以用上述兩種動態鏈接在Golang中使用C++ DLL。Go標准庫”syscall”提供了syscall.LoadLibrary,syscall.GetProcAddress,syscall.Syscall系列函數,可以輕松實現運行時動態鏈接,可參閱godoc查看syscall包的用法。
當DLL導出了C++類或者導出了大量函數,使用運行時動態鏈接很難實現或很繁瑣,這種情況下應該使用加載時動態鏈接。下面主要講如何用swig實現golang調用C++ DLL。(環境 windows 7 64位操作系統,安裝64位 tdm-gcc,代碼編譯采用x64)
用VS 2010向導創建一個簡單的動態鏈接庫Simple.dll
主要代碼如下:
Simple.h文件
#ifdef SIMPLE_EXPORTS
#define SIMPLE_API __declspec(dllexport)
#else
#define SIMPLE_API __declspec(dllimport)
#endif
// This class is exported from the Simple.dll
class SIMPLE_API CSimple {
public:
CSimple(void);
// TODO: add your methods here.
void SayHello();
};
extern SIMPLE_API int nSimple;
SIMPLE_API int fnSimple(void);
Simple.cpp文件:
// Simple.cpp : Defines the exported functions for the DLL application.
//
#include "stdafx.h"
#include "Simple.h"
#include <iostream>
using namespace std;
// This is an example of an exported variable
/*SIMPLE_API*/ int nSimple=0;
// This is an example of an exported function.
/*SIMPLE_API*/ int fnSimple(void)
{
return 42;
}
// This is the constructor of a class that has been exported.
// see Simple.h for the class definition
CSimple::CSimple()
{
return;
}
void CSimple::SayHello()
{
cout<<"Hello World"<<endl;
}
這個簡單的DLL導出了一個變量,一個函數和一個類,類中包含一個構造函數和一個SayHello方法。用Dependency Walker查看Simple.dll文件:
Simple.dll導出了5個函數(變量),前面介紹過C++ Name mangling,可以用undname.exe查看undecorated name,多出來的那個函數是CSimple類的拷貝賦值操作符=。
接下來創建一個簡單的go程序來調用Simple.dll,工程目錄是D:\GoSimple,在該目錄下創建simple文件夾,將Simple.dll和Simple.h文件拷貝到D:\GoSimple\simple中,再創建swig程序的輸入文件simple.i
在命令行中切換到 D:\GoSimple\simple,執行 swig -c++ -go -cgo -intgosize 64 simple.i
成功后生成兩個文件 simple_wrap.cxx 和 simple.go
打開simple.go,增加鏈接項
#cgo CFLAGS: -I .
#cgo LDFLAGS:-L . -lSimple
在D:\GoSimple中創建一個簡單的main.go文件調用DLL
go build編譯,可以看到出現一堆錯誤:
全部是鏈接錯誤,找不到定義。最開始實現go調用c++ dll時,費了較多的時間來解決這個編譯錯誤,后來才明白go使用了gcc編譯器,gcc的name mangling與微軟visual c++編譯器的name mangling不同,需要將DLL文件中增加一份按照g++ name mangling的函數名稱。
根據DLL導出函數、dependency walker、go build的錯誤提示,前面講過的C++ name mangling相關的內容,可以整理出如下關系:
函數名 |
Visual C++ decorated name |
g++ mangled name |
nSimple |
?nSimple@@3HA |
nSimple |
fnSimple |
?fnSimple@@YAHXZ |
_Z8fnSimplev |
CSimple::SayHello |
?SayHello@CSimple@@QEAAXXZ |
_ZN7CSimple8SayHelloEv |
CSimple::CSimple |
??0CSimple@@QEAA@XZ |
_ZN7CSimpleC1Ev |
|
|
|
我們需要利用.def文件為DLL文件的導出項部分增加一份g++使用的符號。為Simple DLL工程增加Simple.def,
重新編譯后,再次用dependency walker來查看Simple.dll
增加def文件的作用是為函數入口點增加一個別名,例如?fnSimple@@YAHXZ 和_Z8fnSimplev對應的入口點同為0x0000102D,?fnSimple@@YAHXZ是visual c++ mangled name,_Z8fnSimplev是gcc mangled name。golang使用gcc編譯c++代碼,為DLL增加導出項后,就可以解決函數未定義的鏈接錯誤了。
更新D:\GoSimple\simple目錄的Simple.dll,同時也在D:\GoSimple中拷貝一份Simple.dll
重新go build,編譯成功。
運行
注意:1.go build前,需要在D:\GoSimple中也拷貝一份Simple.dll,否則會出現錯誤:
2.不能將Simple.lib拷貝到go程序目錄中,否則會出現錯誤: