win10 64位下go编译dll给C#调用

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 触发...");
            }
        }
    }

 win10 64位下go编译dll给C#调用_第1张图片

毫无违和感......

 至此,一切都回归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类,编程时只要注意非托管的内存一定要负责好释放就可以了。

你可能感兴趣的:(go)