1、首先下载安装mingw-w64-install.exe,安装的时候根据go的架构选择64位或i686,安装后将mingw下的bin加入到PATH环境变量,打开控制台,输入gcc,查看是否安装成功。
2、编写go代码:
package main
import "C"
import "fmt"
//export PrintBye
func PrintBye(){
fmt.Println("From dll:Bye!")
}
//export Sum
func Sum(a int, b int) int{
return a + b;
}
func main(){
// Need a main function to make CGO compile package as C shared library
}
注意两个函数上面的注释//export XXX,这是函数导出的标记,不加的话导致生成的dll无任何导出内容。
3、运行命令行编译dll:go build -buildmode=c-shared -o 生成的DLL名称.dll go源码文件名称.go
4、确保安装的go版本支持编译动态链接库,如果版本低,会提示-buildmode=c-shared not supported on windows/amd64等错误,这时下载最新的go,当前是go 1.12版本可以正常编译。
5、在C#中导入并调用:
public partial class Form1 : Form
{
[DllImport("exportgo.dll", EntryPoint = "PrintBye")]
static extern void PrintBye();
[DllImport("exportgo.dll", EntryPoint = "Sum")]
static extern int Sum(int a, int b);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
PrintBye();
Debug.WriteLine(Sum(22, 33));
}
}
6、生成dll的同时还会生成一个C的头文件,查看这个文件可以帮助我们了解如何导入dll进行传参、调用:
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package command-line-arguments */
#line 1 "cgo-builtin-export-prolog"
#include /* for ptrdiff_t below */
#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif
#endif
/* Start of preamble from import "C" comments. */
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
#line 1 "cgo-gcc-export-header-prolog"
#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_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 GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
#endif
/* End of boilerplate cgo prologue. */
#ifdef __cplusplus
extern "C" {
#endif
extern void PrintBye();
extern GoInt Sum(GoInt p0, GoInt p1);
#ifdef __cplusplus
}
#endif
也可以在目录下新建一个bat文件,输入
@echo off
set filepath=%1
set dllpath=%~dp1%~n1.dll
IF NOT EXIST "%filepath%" (
goto err
) ELSE (
goto make
)
:err
echo 文件不存在!
set/p exepath=请拖拽文件这里或输入文件路径然后按下回车
IF NOT EXIST "%filepath%" (
goto err
) ELSE (
goto make
)
:make
echo 编译 %~nx1 -^> %~n1.dll
call go build -ldflags "-s -w" -buildmode=c-shared -o "%dllpath%" "%filepath%"
echo 编译完成!按下回车关闭窗口
:end
pause
把go文件拖上去就会自动生成dll和.h文件
7、C#调用go的dll,传递回调函数的尝试
作为一个成熟的应用,应该实现主程序和dll的相互调用;但是go函数如果有一个回调函数作为参数,在go内部是ok的,但是作为dll导出,头文件中的导出函数签名里,回调函数参数被修改为void*。C#中使用Intptr或委托,编译可以通过,但执行时报错。还没有找到确切的解决方案,网上貌似还没有相关资料。因此可以考虑使用Windows的Api函数SendMessage、PostMessage实现go的dll与C#的交互(限于winform、wpf)。从github.com/lxn/win上下载了一个比较全的win API包,go代码如下:
package main
import "C"
import "fmt"
import "./github.com/lxn/win"
var _handle uintptr
//export SetMainHandle
func SetMainHandle(handle uintptr){
_handle = handle
}
//export Echo
func Echo(msg string){
fmt.Println(msg)
win.SendMessage(win.HWND(_handle), uint32(win.WM_USER + 1000), 0, 0)
}
func main(){
// Need a main function to make CGO compile package as C shared library
}
编译生成dll后,在C#中调用:
public partial class Form1 : Form
{
[DllImport("exportgo.dll", EntryPoint ="Echo", CallingConvention = CallingConvention.Cdecl, ExactSpelling =false)]
static extern unsafe void Echo(GoString msg);
[DllImport("exportgo.dll", EntryPoint = "SetMainHandle", CallingConvention = CallingConvention.Cdecl, ExactSpelling = false)]
static extern unsafe void SetMainHandle(UIntPtr handle);
public Form1()
{
InitializeComponent();
}
private void button2_Click(object sender, EventArgs e)
{
//触发go dll中调用SendMessage
Echo("agc");
}
private void Form1_Load(object sender, EventArgs e)
{
//给go的dll传递窗体句柄
SetMainHandle((UIntPtr)(Handle.ToInt64()));
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if(m.Msg == 1024 + 1000)
{
MessageBox.Show("go 触发...");
}
}
}
毫无违和感......
至此,一切都回归win编程的老套路上,可以考虑把go引入到公司项目开发中啦,毕竟纯C#开发,牵涉反编译,而混淆又有其他风险。tcp通信、加解密等方面的功能可以用go来实现,提高开发效率。
8、C#和go的dll之间传参的验证,根据编译dll时生成的头文件,可以创建C#的对象。例如:
go中的slice(数组)在头文件中提示为:typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
C#中定义为:
public struct GoSlice
{
public IntPtr Data;
public long Len;
public long Cap;
}
如C#要给go的dll传递一个GoSlice:
//byte[]转换为Intptr
public static IntPtr BytesToIntptr(byte[] bytes)
{
int size = bytes.Length;
IntPtr buffer = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(bytes, 0, buffer, size);
return buffer;
}
finally
{
//Marshal.FreeHGlobal(buffer); //这里不能直接释放了,否则传递给go的数据可能发生改变
}
}
private void button3_Click(object sender, EventArgs e)
{
var p = new GoSlice { Len = 3, Cap = 3 };
var data = new byte[3];
data[0] = 2;
data[1] = 3;
data[2] = 4;
p.Data = BytesToIntptr(data);
BytesTest(p); //调用go dll中的函数
Marshal.FreeHGlobal(p.Data); //释放非托管堆上的空间
}
go中的dll给C#发送数组:
var a [10]byte
for i := 0; i < 10 ; i++ {
a[i] = byte(i)
}
wparam := &a[0]
win.SendMessage(win.HWND(_handle), uint32(win.WM_USER + 1000), uintptr(unsafe.Pointer(wparam)), 10)
C#中接收数据:
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 1024 + 1000)
{
var bytes = new byte[(int)m.LParam];
Marshal.Copy(m.WParam, bytes, 0, (int)m.LParam);
MessageBox.Show("go 触发..." + m.LParam + " " + bytes.Aggregate("", (sum, i) => sum = sum + "," + i.ToString()));
}
}
C#中的Marshal、GCHandle提供了托管与非托管内存的处理函数集合。
C# 托管内存与非托管内存之间的转换
1.c#的托管代码和非托管代码
c#有自己的内存回收机制,所以在c#中我们可以只new,不用关心怎样delete,c#使用gc来清理内存,这部分内存就是managed memory,大部分时候我们工作于c#环境中,都是在使用托管内存,然而c#毕竟运行在c++之上,有的时候,(比如可能我们需要引入一些第三方的c++或native代码的库,在Unity3d开发中很常见)我们需要直接在c#中操纵非托管的代码,这些non-managed memory我们就需要自己去处理他们的申请和释放了, c# 中提供了一些接口,完成托管和非托管之间的转换,以及对这部分内存的操作。基本上有以下几种:
2.managed mem-> un-managed mem
比如在c#中调用第三方的某个c++库,库中有个函数是void func(float * data, int length).我们需要传入给data的就应该是一个非托管的代码(why?首先传入托管的内存,c#层很可能会把它gc掉,而c++还在使用,而且托管的mem它的指针地址可能会发生改变,因此直接传给c++可能拿到的地址是错误的)
代码如下:
using System.Runtime.InteropServices;
float[] _managed_data =... // this is the c# managed data
GCHandle unmanaged_data_handle = GCHandle.Alloc(_managed_data, GCHandleType.Pinned); //这里将标记_managed_data暂时不能被gc回收,并且固定对象的地址
func(unmanaged_data_handle.AddrOfPinnedObject(),_managed_data.Length);//这里将拿到非托管内存的固定地址,传给c++
unmanaged_data_handle.Free();//使用完毕后,将其handle free,这样c#可以正常gc这块内存
3.un-managed mem->managed mem, 在c++中返回一个un-managed mem给c#使用
有时需要在c++中分配一块处理好的内存,然后返回给c#来使用,如c++中某个接口 int func(int** data) (注意这里要使用指针的指针,因为data是得到的结果)
IntPtr unmanaged_ptr=IntPtr.Zero; //定义这个c#中用来接收c++返回数据的指针类型
int length = func(out unmanaged_ptr );//调用c++的函数,使unmanaged_ptr指向c++里分配的内存,注意这里用out ,才能与c++里面的**匹配。
byte[] managed_data = new byte[length];
Marshal.Copy(unmanaged_ptr, managed_data, 0, length);//将非托管内存拷贝成托管内存,才能在c#里面使用
Marshal.FreeHGlobal(unmanaged_ptr);//释放非托管的内存
4.在c#直接申请一个un-managed mem传给c++
有时需要直接在c#开辟一块非托管的内存,传给c++用,这块内存同样可以在c#中用后销毁。代码如下
IntPtr unmanaged_data_prt = Marshal. AllocHGlobal(100);// 直接分配100 byte的内存
func(unmanaged_data_prt);//传给c++使用
Marshal.FreeHGlobal(unmanaged_data_prt);使用后销毁非托管内存
此外 Marshal类里面还有很多处理非托管内存的方法。
托管内存和非托管内存在c#里面可以互相自由的转化,主要通过Marshal类和GCHandle类,编程时只要注意非托管的内存一定要负责好释放就可以了。