接上篇

使用 MinGW 编译 C 程序调用 Golang 编译的 DLL 是兼容性最强的,使用 VS C++ 调用 DLL 兼容性次之,毕竟微软的东西和标准化的东西还是有些区别,但是在 C++ 方面这个区别并不大,都比容易就解决,比如头文件有些区别,有些 MS 平台没有。而我的需求的用 C# 来调用 DLL,为了安全起见,先用 32 位来测试。为了把 DLL 编译为 32 位,代码做了一些小的变动。

lib.h 文件:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt32 GoInt;
typedef GoUint32 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;

typedef struct{
    void* data;
    GoUint size;
}GoMem;
void GetData(GoMem* gm);
void SetData(GoMem* gm);
void* Malloc(GoUint size);
void* Realloc(void* mem,GoUint size);
void Free(void* mem);

主要变动是加入了类型声明,这写声明是从自动生成的 main.h 里面复制出来的,如果 C++ 调用,需要注意不要重复声明。

然后加入了 3 个函数: Malloc, Realloc, Free,这是因为 DLL 分配的内存,在 MinGW 编译时,可以在当前代码里直接调用 free 来释放,但是在 MS 的编译器下,调用 free 会出错,可能是两个编译器,在内存分配方面代码是不兼容的,DLL 内部是用标准编译器的代码(和MinGW相同),而程序里面是 MS 库的代码。这样,需要在释放内存时仍然调用 DLL 内部的释放方法来完成。

lib.go 文件:

package main

/*
#include "lib.h"
*/
import "C"
import (
	"fmt"
	"unsafe"
)
//export GetData
func GetData(gm *C.GoMem)  {
	str := "这是一段中文文本,Golang 字符串编码是 utf-8,在 C 语言里需要转换成 GBK 编码才能正确显示。"
	gm.data = unsafe.Pointer(C.CString(str))
	gm.size = C.GoUint(len(str))
}
//export SetData
func SetData(gm *C.GoMem)  {
	var str string
	if gm.size==0{
		str = C.GoString((*C.char)(gm.data))
	}else{
		str = C.GoStringN((*C.char)(gm.data),C.int(gm.size))
	}
	fmt.Println("SetData:",str)
}
//export Malloc
func Malloc(size C.GoUint) unsafe.Pointer{
	return C.malloc(size)
}
//export Realloc
func Realloc(mem unsafe.Pointer,size C.GoUint) unsafe.Pointer{
	return C.realloc(mem,size)
}
//export Free
func Free(mem unsafe.Pointer){
	C.free(mem)
}

main.go 文件:

package main

/*
#include "lib.h"
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
}

有这三个文件,就可以正常编译了,我们编写一个编译批处理文件 dll.cmd,方便重复使用。

dll.cmd 文件:

set GOARCH=386
set CGO_ENABLED=1
go build -o ..\..\C#\DllTest\bin\debug\main.dll -buildmode=c-shared

主要是添加了两个环境变量 GOARCH=386 表示编译为 32 位 DLL,CGO_ENABLED=1 表示启用CGO,加了 GOARCH=386 后必须设置,否则无法编译通过。

我们把编译成功的 main.dll 复制到 C# 工程的 bin\debug 目录下,C# 工程是一个标准的 Windows 窗体程序。Form1 下添加如下代码:

	[StructLayout(LayoutKind.Sequential)]
	public class GoMem {
		public IntPtr data;
		public UInt32 size;
	}
	[DllImport("main.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
	private static extern void GetData(GoMem gm);
	[DllImport("main.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
	private static extern void SetData(GoMem gm);
	[DllImport("main.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
	private static extern void GoMemTest(GoMem gm);
	[DllImport("main.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
	private static extern IntPtr Malloc(UInt32 size);
	[DllImport("main.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
	private static extern void Free(IntPtr p);

需要特别说明的是,很多网上的教程说,GoMem 这种传指针的参数,需要加 ref。但是各种调试不过之后发现,去掉 ref 才是正确的,C# 万物皆指针,默认就是传指针进去,加了 ref 应该是传了个指针的指针。

还有一个需要注意的是,打印字串,同样的 dll,调用 SetData 的时候打印的都是 utf-8 编码的字串,在 C++ 下和 Golang 环境里都能够正确打印,但是 C# 里面打印出来就是乱码,问题不是出在打印函数,而是控制台设置,如果控制台支持输入 utf-8 编码,就可以正确输出,否则就是乱码。

添加如下代码:

private void Form1_Load(object sender, EventArgs e) {
	GoMem gm = new GoMem();
	GetData(gm);
	SetData(gm);
	var data = new byte[gm.size];
	Marshal.Copy(gm.data, data, 0, gm.size);
	this.Text = System.Text.Encoding.UTF8.GetString(data, 0, gm.size);
	Free(gm.data);
}

gm.data 是一段 utf-8 编码的字串数据,C#下需要转成字串。