Go语言调用动态库

golang的OLE调用工具包 https://github.com/mattn/go-ole

GCC环境

https://sourceforge.net/projects/mingw-w64/files/

设置环境变量后在控制台中输入

1
gcc -v

可以看到gcc版本号,说明gcc安装成功

编写go程序

我们这里只是编写一个简单的输出字符串的程序,接受一个字符串参数,然后将拼接成新的字符串并作为返回值返回,在这里,我们将文件命名为libhello.go

1
2
3
4
5
6
7
8
9
10
package main

import "C"

func hello(msg string) string {
return "Hello! " + msg
}

func main() {
}

注意,即使是要编译成动态库,也要有main函数,上面的import "C"一定要有

Windows动态库

执行如下命令生成DLL动态链接库:

1
go build -buildmode=c-shared -o libtest.dll .\test.go

如果控制台没有报错,那么会在当前路径下生成libhello.dll文件

Linux/Unix/macOS动态库

执行如下命令生成SO动态库:

1
go build -buildmode=c-shared -o libtest.so test.go

Go调用windows dll

syscall.Syscall系列方法

当前共5个方法

1
2
3
4
5
syscall.Syscall
syscall.Syscall6
syscall.Syscall9
syscall.Syscall12
syscall.Syscall15

分别对应 3个/6个/9个/12个/15个参数或以下的调用

参数都形如

1
syscall.Syscall(trap, nargs, a1, a2, a3)

第二个参数, nargs 即参数的个数,一旦传错, 轻则调用失败,重者直接APPCARSH,多余的参数, 用0代替,跟Syscall系列一样, Call方法最多15个参数。Must开头的方法, 如不存在,会panic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package main

import (
"fmt"
"syscall"
"time"
"unsafe"
)

const (
MB_OK = 0x00000000
MB_OKCANCEL = 0x00000001
MB_ABORTRETRYIGNORE = 0x00000002
MB_YESNOCANCEL = 0x00000003
MB_YESNO = 0x00000004
MB_RETRYCANCEL = 0x00000005
MB_CANCELTRYCONTINUE = 0x00000006
MB_ICONHAND = 0x00000010
MB_ICONQUESTION = 0x00000020
MB_ICONEXCLAMATION = 0x00000030
MB_ICONASTERISK = 0x00000040
MB_USERICON = 0x00000080
MB_ICONWARNING = MB_ICONEXCLAMATION
MB_ICONERROR = MB_ICONHAND
MB_ICONINFORMATION = MB_ICONASTERISK
MB_ICONSTOP = MB_ICONHAND

MB_DEFBUTTON1 = 0x00000000
MB_DEFBUTTON2 = 0x00000100
MB_DEFBUTTON3 = 0x00000200
MB_DEFBUTTON4 = 0x00000300
)

func abort(funcname string, err syscall.Errno) {
panic(funcname + " failed: " + err.Error())
}


func IntPtr(n int) uintptr {
return uintptr(n)
}

func StrPtr(s string) uintptr {
return uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(s)))
}

// windows下的第一种DLL方法调用
func ShowMessage1(caption, text string, style uintptr) (result int) {
user32, _ := syscall.LoadLibrary("user32.dll")
messageBox, _ := syscall.GetProcAddress(user32, "MessageBoxW")
defer syscall.FreeLibrary(user32)

ret, _, callErr := syscall.Syscall9(messageBox,
4,
0,
StrPtr(text),
StrPtr(caption),
style,
0, 0, 0, 0, 0)
if callErr != 0 {
abort("Call MessageBox", callErr)
}
result = int(ret)
return
}


// windows下的第二种DLL方法调用
func ShowMessage2(title, text string) {
user32 := syscall.NewLazyDLL("user32.dll")
MessageBoxW := user32.NewProc("MessageBoxW")
MessageBoxW.Call(IntPtr(0), StrPtr(text), StrPtr(title), IntPtr(0))
}

// windows下的第三种DLL方法调用
func ShowMessage3(title, text string) {
user32, _ := syscall.LoadDLL("user32.dll")
MessageBoxW, _ := user32.FindProc("MessageBoxW")
MessageBoxW.Call(IntPtr(0), StrPtr(text), StrPtr(title), IntPtr(0))
}

// windows下的第四种DLL方法调用
func ShowMessage4(title, text string) {
user32 := syscall.MustLoadDLL("user32.dll")
MessageBoxW := user32.MustFindProc("MessageBoxW")
MessageBoxW.Call(IntPtr(0), StrPtr(text), StrPtr(title), IntPtr(0))
}

func main() {

num := ShowMessage1("Done Title", "windows下的第一种DLL方法调用", MB_YESNOCANCEL)
fmt.Printf("Get Retrun Value Before MessageBox Invoked: %d\n", num)

ShowMessage2("windows下的第二种DLL方法调用", "HELLO !")
ShowMessage3("windows下的第三种DLL方法调用", "HELLO !")
ShowMessage4("windows下的第四种DLL方法调用", "HELLO !")

time.Sleep(3 * time.Second)
}

func init() {
fmt.Println("Start...")

c实现的dll

foo.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef _FOO_H_
#define _FOO_H_
#include <stdio.h>

#define FOO_EXPORTS
#ifdef FOO_EXPORTS
#define EXPORTS_API __declspec(dllexport)
#else
#define EXPORTS_API __declspec(dllimport)
#endif // FOO_EXPORTS

#ifdef __cplusplus
extern "C" {
#endif

EXPORTS_API int add(int left, int right);
EXPORTS_API void show(char* ptr, int nLen);
EXPORTS_API char* change(char* ptr, int nLen);
EXPORTS_API void callByReference(int &nLen);
EXPORTS_API void callByPtr(int* nLen);
#ifdef __cplusplus
}
#endif

#endif //_FOO_H_

foo.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "foo.h"
int add(int left, int right)
{
return left + right;
}
void show(char* ptr,int nLen)
{
printf("> -------------------\n> Pass `pointer` and `int` data:\n");
printf(">> %s, %d\n", ptr,nLen);
}
char* change(char* ptr, int nLen)
{
if (!ptr || 0 > nLen)
return nullptr; // c语言中没有nullptr,把源文件后缀改为cpp才能编译通过
printf("> -------------------\n> Pass `pointer` and `int` data:\n");
printf("> src strings: %s\n",ptr);
ptr[1] = 'a';
printf("> modify strings: %s\n", ptr);
return ptr;
}
void callByReference(int &nLen) // c语言中&是取地址,不是引用,把源文件后缀改为cpp才能编译通过
{
nLen = 100;
}
void callByPtr(int *nLen)
{
*nLen = 1000;
}

编译

1
2
# 生成window下的动态库和静态库
gcc -shared -o foo.dll foo.c -Wl,--output-def,foo.def,--out-implib,foo.a

go调用代码

编写调用dll的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package main

import (
"fmt"
"strconv"
"syscall"
"unsafe"
"C" // 不导入"C"包无法编译通过
)

var dllPath = "F:\\goproject\\src\\plugin\\foo.dll"


func main() {
call()
}

func IntPtr(n int) uintptr {
return uintptr(n)
}
func Int2IntPtr(n int) uintptr {
return uintptr(unsafe.Pointer(&n))
}
func IntPtr2Ptr(n *int) uintptr {
return uintptr(unsafe.Pointer(n))
}
func BytePtr(s []byte) uintptr {
return uintptr(unsafe.Pointer(&s[0]))
}

func call() error {
left := 4
right := 5
err := Add(left, right)
if err != nil {
fmt.Println("Error:", err)
return err
}

str := []byte("this is a test msg!")
err = Show(str, len(str))
if err != nil {
fmt.Println("Error:", err)
return err
}

err = Change_bytes(str, len(str))
if err != nil {
fmt.Println("Error:", err)
return err
}

n := 0
err = Call_PassByValue(n)
if err != nil {
fmt.Println("Error:", err)
return err
}
fmt.Println("> Call_PassByValue(n)的结果为 n=" + strconv.Itoa(n) + ",期待输出 100")

n = 0
err = Call_PassByPtr(&n)
if err != nil {
fmt.Println("Error:", err)
return err
}
fmt.Println("> Call_PassByPtr(&n)的结果为 n=" + strconv.Itoa(n) + ",期待输出 1000")

return nil
}

func Add(left, right int) error {
handle, err := syscall.LoadLibrary(dllPath)
if err != nil {
fmt.Printf("Error: %s\n", err)
return err
}
defer syscall.FreeLibrary(handle)

add, err := syscall.GetProcAddress(handle, "add")
if err != nil {
fmt.Printf("Error: %s\n", err)
return err
}

ret, _, _ := syscall.Syscall(add, 2, IntPtr(left), IntPtr(right), 0)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
fmt.Println("> Add(4,5)的结果为:", ret)
return nil
}

func Show(str []byte, l int) error {

handle := syscall.NewLazyDLL(dllPath)
show := handle.NewProc("show")

show.Call(BytePtr(str), IntPtr(l))
return nil
}

func Change_bytes(str []byte, l int) error {
handle := syscall.NewLazyDLL(dllPath)
change := handle.NewProc("change")
change.Call(BytePtr(str), IntPtr(l))
return nil
}

func Call_PassByValue(n int) error {
handle := syscall.NewLazyDLL(dllPath)
test := handle.NewProc("callByReference")
test.Call(Int2IntPtr(n))
return nil
}

func Call_PassByPtr(n *int) error {
handle := syscall.NewLazyDLL(dllPath)
test := handle.NewProc("callByPtr")
test.Call(IntPtr2Ptr(n))
return nil
}

1、当值传递时并没有修改传入的值;只有指针传递时修改了传入的值。
2、指针传递时golang侧使用的是byte切片

结论

1、需要修改参数的值时,必须使用指针类型

1
2
3
func Call_PassByPtr(n *int) error{
return nil
}

2、需要修改指针的内容时,必须使用指针类型

1
2
3
func Change_bytes(str []byte, l int) error {
return nil
}

3、golang传递指针给c接口函数时,必须使用[] byte的类型,不能使用string类型

1
2
3
4
5
6
7
8
9
func Show(str []byte, l int) error {
dllPath := "F:\\goproject\\src\\plugin\\foo.dll"

handle := syscall.NewLazyDLL(dllPath)
show := handle.NewProc("show")

show.Call(BytePtr(str), IntPtr(l))
return nil
}

Go中嵌入C代码

Golang中嵌入C代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main                                                               
//#include <stdio.h>
//#include <stdlib.h> //下面不能有空行,否则编译时cgo会报错
/*
void Hello(char *str) {
printf("%s\n", str);
}
*/
import "C" //上面不能有空行,否则编译时cgo会报错
import "unsafe" //C指针的使用,在C代码中申请的空间,GC垃圾回收机制不会管理,所以需要自己手动free申请的空间

func main() {
s := "Hello Cgo"
cs := C.CString(s)
C.Hello(cs)
C.free(unsafe.Pointer(cs))
}

Go调用C的动态库so

C库源程序代码:

1
2
3
4
5
6
7
8
9
//foo.c                                         
#include <stdio.h>
int Num = 8;
void foo(){
printf("I dont have new line.");
printf("I have new line\n");
printf("I have return\r");
printf("I have return and new line\r\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//foo.h                    
#ifndef __FOO_H
#define __FOO_H

#ifdef __cplusplus
extern "C" {
#endif

extern int Num;
extern void foo();

#ifdef __cplusplus
}
#endif

#endif

编译

1
2
3
4
5
6
# 动态库的编译
gcc foo.c -fPIC -shared -o libfoo.so

# 静态库的编译
gcc -c foo.c -o foo.o //产生目标文件
ar rcv libfoo.a foo.o //将目标文件打包成静态库

Go调用C库的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -L. -lfoo
#include <stdio.h>
#include <stdlib.h>
#include "foo.h"
*/
import "C"
import "unsafe"
import "fmt"

func Prin(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.fputs(cs, (*C.FILE)(C.stdout))
//C.free(unsafe.Pointer(cs))
C.fflush((*C.FILE)(C.stdout))
}

func main() {
fmt.Println("vim-go")
fmt.Printf("rannum:%x\n", C.random())
Prin("Hello CC")
fmt.Println(C.Num)
C.foo()
}

如果c库的程序改为下面这样:

1
2
3
4
5
6
7
8
9
10
11
//foo.c                                                   
#include <stdio.h>

int Num = 8;

void foo(){
printf("I dont have new line.");
//printf("I have new line\n");
//printf("I have return\r");
//printf("I have return and new line\r\n");
}

此时,go中调用foo时没有任何反应,即不会输出,不会输出的原因是printf后面没有加换行符,但是如果加了8,9,10这些测试行后,第7行也会显示,原因是第10行最后有一个换行符,这个应该是向stdout输出时,字符流是放在buffer中,如果没有换行,buffer中的数据不会立即输出。在go调用C的测试程序中有写了一个测试小函数用来测试stdout,验证了没有fflush,stdout上不会显示输出信息。但平时在写C程序的时候,似乎没有这样的问题,这个是因为C的运行环境自动做了fflush的动作。

Go调用C的静态库a

Go调用的代码与动态库一样

Go参数类型转换为C参数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int EC60789803D1(const char *CompanyTag, char *dataIn, int inlen, unsigned char *dataout, int outlen);
// Encrypt
{
var byteOutBuffer []byte
byteOutBuffer = make([]byte, 2048)
var outlen int = 2048
var strKey string
strKey = "test"
var inString string
inString = "fdsafdsafdsa"
fmt.Println("inlen=", len(inString))

InKey := C.CString(strKey)
InData := C.CString(inString)
InInLen := C.int(len(inString))
InOut := (*C.uchar)( &byteOutBuffer[0])
InOutLen := C.int(outlen)
iRet := C.EC60789803D1(InKey, InData, InInLen, InOut, InOutLen)
fmt.Println("C.EC60789803D1 ret=", iRet, " OutBuffer=", util.Bytes2str(byteOutBuffer))
}

cgo可以在go语言中夹杂着C函数或数据,在使用cgo时,有一些需要注意的:
1、go中的int/int32/int64/uint32/uint64和C语言中的int/int32等是不同的,因此,C语言的函数的参数不能是go语言的int,需要转换,同理,go函数的int也不能使用C的int,需要转换。

C.int(n)。还有一点,C的函数调用中,有很多参数是size_t类型,其实就是一个整型,但如果使用C.int()作为C函数的参数,就会编译出错:

1
cannot use _Ctype_int(100) (type C.int) as type C.size_t in function argument

go编译器严格限制参数类型必须一致,因此必须是size_t类型的参数。这是因为go语言没有C语言里面的强制转换的概念,你可以使用C.size_t(n)来得到C语言中的sizt_t类型。
2、go语言中的字符串和C语言中的字符串转换

1
2
C.Cstring
C.GoString

3、结构体
使用C.struct_xxxx
如果使用struct中的成员变量,可以直接用.来访问。
4、指针
使用unsafe.Pointer()来转换,例如需要转换为 int *:

1
(*C.int)(unsafe.Pointer(&v))

在c语言中,指针即数组,可以使用ptr[n]来获得指针的第n个偏移,但在go中,这样不行,会报错:invalid operation:xxxx,go语言中,指针没有这样的操作。
需要使用unsafe.Pointeruintptr配合来获取指针的偏移。
5、函数调用
C.func(),文档中说,go调用所有的C函数都会返回两个值,后一个值为error类型,即使是void函数。文档表述如下:

1
2
n, err := C.sqrt(-1)
_, err := C.voidFunc()

但发现,C.malloc似乎只返回一个值。
6、C语言中的NULL在go中是nil
例如

1
2
3
4
s := C.malloc(C.sizeof(100))
if s == nil {

}

这个很重要,在没发现nil可以比较c的指针前,是这样比较的:

1
(*C.char)(unsafe.Pointer(uintptr(0)))

不过,cgo的文档还很匮乏,很多都需要阅读代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

/*
#include
#include
#include

struct t {
char *s;
};

*/
import “C”
import “unsafe”
import “fmt”

func main() {
var t C.struct_t
var s = “hello world”
var ch *C.char
var tmp *C.char

// 分配空间, 并判断是否分配成功
t.s = (*C.char)(C.malloc(C.size_t(100)))
if t.s == nil {
//if t.s == (*C.char)(unsafe.Pointer(uintptr(0))) {
panic(“malloc failed!\n”)
}

// 释放内存
defer C.free(unsafe.Pointer(t.s))
// 将go的字符串转为c的字符串,并自动释放
ch = C.CString(s)
defer C.free(unsafe.Pointer(ch))

// 调用C的strncpy函数复制
C.strncpy(t.s, ch, C.size_t(len(s)))
// C的指针操作
for i := C.size_t(0); i < C.strlen(t.s); i ++ {
tmp = (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(t.s)) + uintptr(i)))
*tmp = C.char(C.toupper(C.int(*tmp)))
}

fmt.Printf(“%s\n”, C.GoString(t.s))
fmt.Printf(“sizeof struct t is %v\n”, unsafe.Sizeof(t))
}

http://www.361way.com/go-plugin/5925.html