网易首页 > 网易号 > 正文 申请入驻

Go语言编写 Windows 动态链接库(DLL)

0
分享至



转载自IQTester

Golang 在某些领域精巧易用,完成一些常用功能简单快捷。而且是编译为可执行单文件,发布非常方便。但是 Golang 对界面支持非常不好,虽然有解决方案,但是其实很难用。编写桌面端的界面程序还是 C# 来的直接,拖几个控件太方便了。

而网络和某些组件,用 Golang 不要太方便,C#相应的包要么难用,要么没有,自己写工作量是吃不消的。很久以前就有把 Golang 编译为 DLL 的想法,不过直到 1.10 Golang 终于支持编译 Windows 动态链接库了,当然 Linux 版本早就支持了。

go build -ldflags "-s -w" -o ..\..\C++\DllTest\main.dll -buildmode=c-shared main.go

面的 "-s -w" 选项用于减小生成动态链接库的体积,-s 是压缩,-w 是去掉调试信息。当然,这条指令可以简化成:

go build -o ..\..\C++\DllTest\main.dll -buildmode=c-shared main.go

-o 指定目标目录,为了方便,我直接把目标文件编译到了 C++ 测试工程的目录。编译通过后,在目标目录生成一个 main.dll 文件和一个 main.h 文件,没有 .lib 文件。所以测试使用动态加载的方式。

把 Golang 代码编译为动态链接库的时候,main 函数不能省略,因为编译器硬性需要这个函数,当然,它是不会被调用的。但是,每个文件的 init 函数是可以正常被调用的。也就是加载 DLL 的时候,这些 init 会被初始化调用。

整个过程非常简单,但是真正需要注意的是,虽然 Golang 提供了 GoSlice 这样的导出类型对应到 Golang 的数组(Slice),但是通常你不能真正把它用在导出函数上。因为 Golang 是一个有垃圾回收机制的语言,那么一个数组什么时候被回收,显然离开 Golang 的环境是无法解决的,Golang 的垃圾收集器无法知道你在外部对这个数组做了什么,什么时候应该回收它。所以,如果处理不当,这种函数一调用就会出错,因为在函数尚未返回的时候,垃圾收集已经工作了,导致变量已经失效,从而异常。

编写DLL核心的问题并不是编译,而是CGO的相关知识。Golang和C语言的交互很方便,只要 import "C" 后,直接用 C. 前缀调用 C 的函数就可以了。

package main#includevoid CHello(void) {printf("hello world! I from c code!\n");import "C"func main() {C.CHello()}

需要注意的是,import "C" 上面的注释部分,必须紧挨,不能有空行,否则编译就会报错。Go 语言可以把 C 代码写到注释部分有一点诡异。当然,Go 支持和 C 文件混编,可以把 C 代码单独写成 .h .c 文件。

CGO 的交互包含两部分,一是上面的 Go 调用 C 代码,另一个就是 C 调用 Go 的代码。可能是因为 C 语言引用的关系,如果这两种调用都有,就必须是多文件模式,不同部分代码写在不同的文件,否则,就会编译报错(重复定义错误)。

创建三个文件 lib.h lib.c lib.go

//lib.h#includevoid GoHello();void CHello();

//lib.c#include "lib.h"void CHello(void) {printf("hello world! I from c code!\n");//lib.gopackage main#include "lib.h"import "C"import "fmt"//export GoHellofunc GoHello() {fmt.Println("Hello world! I from Golang!")}

修改 main.go 如下:

//main.gopackage main#include "lib.h"import "C"func main() {C.CHello()C.GoHello()//使用 go run . 命令在当前目录下运行,输出如下:Hello world! I from c code!Hello world! I from Golang!

现在还没有涉及到 CGO 最容易踩坑的地方,就是参数传递。一个显然的事情是,Golang 是一个有垃圾回收的语言,所以任何 Golang 的对象指针不应该直接传给 C 语言代码,因为 C 语言可以直接操作内存,会导致 Golang 的运行环境被破坏,产生不可预知的问题。我们用下面的代码来做实验。

修改 lib.h,添加一个结构和两个函数 GetData,SetData 分别用于获取数据和设置数据。

// lib.h#include#includetypedef struct{void* data;int size;}GoMem;void GetData(GoMem* gm);void SetData(GoMem* gm);

修改 lib.go,添加两个函数的实现代码,此时 lib.c 没有用,可以先不管他

//lib.gopackage main#include "lib.h"import "C"import ("fmt""unsafe"//export GetDatafunc GetData(gm *C.GoMem)  {data := []byte("Hello world! I'm a golang data.")gm.data = unsafe.Pointer(&data[0])gm.size = C.int(len(data))//export SetDatafunc SetData(gm *C.GoMem)  {data := make([]byte,gm.size)fmt.Println(string(data))}

修改 main.go,调用这两个函数。

//main.gopackage main#include "lib.h"import "C"func main() {var gm C.GoMemC.GetData(&gm)C.SetData(&gm)}

编译执行,我们会发现,编译通过,但是执行过程中,C.SetData 这句会出错,提示传递了Golang的指针,这是不允许的。这句你可以理解成,CGO 不允许把 Golang 的指针传到外部,你只能传递基本数据类型。但是,第一个 C.GetData 确没有这个提示,虽然我们用它取出了 Golang 的指针。这是因为,在传入的时候,gm 还是初始状态,并没有有效指针,所以 Go 无法检测到。如果我们注释掉 C.SetData 的调用,添加如下代码:

//main.gopackage main#include "lib.h"import "C"import "fmt"func main() {var gm C.GoMemC.GetData(&gm)//C.SetData(&gm)//打印指针数值,正确输出指针地址和大小fmt.Println("return:",gm.data,gm.size)//创建一个同样大小的数组data := make([]byte,gm.size)//使用 C 函数 memcpy 拷贝获取的数据到这个数组,需要添加 string.h 的引用//注意参数的类型转换,unsafe.Pointer 在 C 代码里就是 void*,最后的长度参数,在64位下,//是一个ulonglong 类型,gm.size 是 int 类型,需要转换一下。C.memcpy(unsafe.Pointer(&data[0]),gm.data,C.ulonglong(gm.size))//打印数据,可以看到正确的输出:Hello world! I'm a golang data.fmt.Println(string(data))}

上面的代码其实是绕过了 Golang 的检测,虽然我们得到了正确的结果,但是这是有隐患的。GetData 返回的指针是一个 Golang 的对象,我们可以在 C 代码里使用和修改它,而此时,如果 Golang 的垃圾回收开始工作,这个指针会立即失效,从而造成程序宕机。之所以这个程序可以正确运行,是因为程序太简单,垃圾回收并没有启动。应该说,这是一个任何有垃圾回收的语言都面临的问题。

附上 C 语言类型在 Golang 里面的名称:

C.char,
C.schar (signed char),
C.uchar (unsigned char),
C.short,
C.ushort (unsigned short),
C.int, C.uint (unsigned int),
C.long,
C.ulong (unsigned long),
C.longlong (long long),
C.ulonglong (unsigned long long),
C.float,
C.double

如何改造,其实也很简单,我们需要 Golang 数据的时候,自己使用 C 语言申请一段内存,然后在 Golang 里面拷贝数据到这个内存上,当然,这段内存使用完需要释放,否则就会造成内存泄露。

其实 Cgo 里面有 4 个非常方便的用于字串和字节转换的函数,C.CString,C.CBytes,C.GoString,C.GoBytes,当然还有一个支持非 0 结尾字符串的 C.GoStringN。我们修改 lib.go 如下:

//lib.gopackage main#include "lib.h"import "C"import ("fmt""unsafe"//export GetDatafunc GetData(gm *C.GoMem)  {str := "Hello world! I'm a golang data."//C.CString 返回一个 0 结尾的数组,它的类型是 *C.char,这个指针需要用 C.free 来释放gm.data = unsafe.Pointer(C.CString(str))//C.CBytes 返回一个和源数组同长度的字节数组,它的类型是 C.uchar,这个指针需要用 C.free 来释放//gm.data = unsafe.Pointer(C.CBytes([]byte(str)))gm.size = C.int(len(str))//export SetDatafunc SetData(gm *C.GoMem)  {//从指针直接返回一个字串,这个指针指向的字节数组必须是 0 结尾的。str := C.GoString((*C.char)(gm.data))//从一个字节数组和数组长度构造一个字串,这个自己数组不需要是 0 结尾的。//str = C.GoStringN((*C.char)(gm.data),gm.size)fmt.Println("GoString:",str)//从一个字节数组返回一个 Golang 数组data := C.GoBytes(unsafe.Pointer(gm.data),gm.size)fmt.Println("GoBytes:",string(data))}

修改 main.go 如下:

//main.gopackage main#include "lib.h"import "C"import ("fmt""unsafe"func main() {var gm C.GoMemC.GetData(&gm)C.SetData(&gm)fmt.Println("return:",gm.data,gm.size)//因为使用了 free 函数,lib.h 里面需要添加 #includeC.free(unsafe.Pointer(gm.data))}

至此,我们解决了 cgo 互调,内存数据的传输问题。下一步开始编写 DLL,并且在其它语言测试。

对 lib.go 稍微修改了一下,去掉了额外的测试代码。

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

此时要编译 dll 文件,实际上我们只需要 lib.h 和 lib.go 两个文件,因为全部代码都在这两个文件里,但是 main.go 是必须的,main 函数也是必须的,虽然它不会被调用。此时我们不对它进行改动,因为这里面的代码不会被打包进 dll。

在当前文件夹,使用命令 go build -o main.dll -buildmode=c-shared 编译 main.dll,成功后,生成 main.dll 和 main.h 两个文件,其中 main.h 除了基本的函数和类型定义,还会引用 lib.h 文件。

创建 main.c 文件:

//main.c#include#include#include "main.h"//声明函数类型typedef void (*GETDATA)(GoMem*);typedef void (*SETDATA)(GoMem*);int main() {//加载动态链接库HMODULE hdll = LoadLibrary("main.dll");//获取函数指针GETDATA GetData = (GETDATA)GetProcAddress(hdll, "GetData");SETDATA SetData = (SETDATA)GetProcAddress(hdll, "SetData");//检测指针是否加载成功,如果不为 0,就是加载成功了printf("dll:%x,GetData:%x,SetData:%x\n", hdll, GetData, SetData);//调用 GetData 函数,并且打印取回的指针和大小GoMem gm;GetData(&gm);printf("data:%x,size:%d\n", gm.data, gm.size);//调用 SetData 函数SetData(&gm);//释放内存指针,这一步非常重要free(gm.data);// 输出如下:// dll:6a940000,GetData:6a9d0b70,SetData:6a9d0bb0// data:26f2680,size:123// SetData: 这是一段中文文本,Golang 字符串编码是 utf-8,在 C 语言里需要转换成 GBK 编码才能正确显示。

我们使用 MinGW 编译它,注意是 64 位版本,因为我们的 DLL 是 64位的,执行命令 gcc main.c,gcc自动编译为 a.exe 文件,执行 a.exe,可以看到输出结果。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
电车悄悄取消冰箱、电视,不再吹创新,而偷偷降成本求生存

电车悄悄取消冰箱、电视,不再吹创新,而偷偷降成本求生存

柏铭锐谈
2026-01-28 23:23:20
上海力克深圳男篮,卢伟赛后真情吐露心路历程,贺希宁空砍43分

上海力克深圳男篮,卢伟赛后真情吐露心路历程,贺希宁空砍43分

小犙拍客在北漂
2026-01-30 23:06:27
德国大师赛爆冷:世界第2与世锦赛冠军同遭5-1淘汰

德国大师赛爆冷:世界第2与世锦赛冠军同遭5-1淘汰

铿锵格斗
2026-01-31 00:32:36
小玥和小霖在北京过寒假,张兰每天做早餐,房间摆满玩偶。

小玥和小霖在北京过寒假,张兰每天做早餐,房间摆满玩偶。

手工制作阿歼
2026-01-29 03:04:05
神话实锤?埃及金字塔地下神秘“地下世界”,卫星扫出巨型结构

神话实锤?埃及金字塔地下神秘“地下世界”,卫星扫出巨型结构

Science科学说
2026-01-28 08:05:03
富婆女主持要进去了,少女妈被新的小奶狗缠上,鸭子男星功夫好

富婆女主持要进去了,少女妈被新的小奶狗缠上,鸭子男星功夫好

钱小刀娱乐
2026-01-28 23:03:45
去女友家过年,她让我准备6万红包:爸妈各2万,爷爷奶奶各1万

去女友家过年,她让我准备6万红包:爸妈各2万,爷爷奶奶各1万

千秋文化
2026-01-30 22:01:26
英国的请求,中方当场爽快答应,送给斯塔默的两句话,值得他细品

英国的请求,中方当场爽快答应,送给斯塔默的两句话,值得他细品

南宗历史
2026-01-29 19:18:42
江启臣又喊退党,“党魂”遮羞布下的私欲狂舞!

江启臣又喊退党,“党魂”遮羞布下的私欲狂舞!

达文西看世界
2026-01-24 21:02:44
香港街头突发劫案:两名男子下出租车时被抢走5800万日元,劫匪抢完钱立即上私家车逃走,警方:正通缉

香港街头突发劫案:两名男子下出租车时被抢走5800万日元,劫匪抢完钱立即上私家车逃走,警方:正通缉

极目新闻
2026-01-30 14:30:50
金晨遭遇交通事故后面部受伤,手术仅一个月后亮相北影节,红毯状态被指无手术痕迹,“金晨变美了”当天热搜

金晨遭遇交通事故后面部受伤,手术仅一个月后亮相北影节,红毯状态被指无手术痕迹,“金晨变美了”当天热搜

大风新闻
2026-01-30 22:40:06
卢山任上海市副市长

卢山任上海市副市长

新京报
2026-01-30 18:46:20
春节前带女友回家,她见到我母亲后惊呼:阿姨,怎么是你

春节前带女友回家,她见到我母亲后惊呼:阿姨,怎么是你

小月文史
2024-10-30 15:15:55
要么投降,要么死在洞里——内塔尼亚胡拒绝放走被困地道的哈马斯

要么投降,要么死在洞里——内塔尼亚胡拒绝放走被困地道的哈马斯

桂系007
2025-11-05 23:52:21
老泄残精,人穷寿尽!医生提醒:63岁之后,男性要守好这三道关

老泄残精,人穷寿尽!医生提醒:63岁之后,男性要守好这三道关

健康科普365
2026-01-30 21:26:44
金钟国罕见公开爱妻身份:一直把她当妹妹看! 结婚关键点曝光

金钟国罕见公开爱妻身份:一直把她当妹妹看! 结婚关键点曝光

ETtoday星光云
2026-01-30 15:34:12
总有人纳闷,王健林就算只剩100亿,为啥王思聪花钱还是那么大方

总有人纳闷,王健林就算只剩100亿,为啥王思聪花钱还是那么大方

小光侃娱乐
2025-12-10 22:10:04
感谢印度,帮中国解决大难题,2025年成为中国工业全面崛起里程碑

感谢印度,帮中国解决大难题,2025年成为中国工业全面崛起里程碑

比利
2026-01-19 23:14:03
女孩当小姐,一晚要提供4到5次上门服务,2015年被亲人点到不赴约

女孩当小姐,一晚要提供4到5次上门服务,2015年被亲人点到不赴约

汉史趣闻
2025-11-08 09:27:32
绝了!蒸一蒸这水果,喉咙里的痰“唰唰”消失,全家都抢着喝

绝了!蒸一蒸这水果,喉咙里的痰“唰唰”消失,全家都抢着喝

江江食研社
2025-12-29 14:30:09
2026-01-31 05:07:00
娱乐督察中
娱乐督察中
独乐乐不如众乐乐
219文章数 20555关注度
往期回顾 全部

科技要闻

意念控制机器人不是科幻 1-2年就落地

头条要闻

伊朗总统:若美国寻求谈判 就必须停止挑衅

头条要闻

伊朗总统:若美国寻求谈判 就必须停止挑衅

体育要闻

“假赌黑”的子弹,还要再飞一会儿吗?

娱乐要闻

警方通报金晨交通事故,否认网传骗保

财经要闻

水贝惊雷:揭秘杰我睿百亿黄金赌局的背后

汽车要闻

合资品牌首搭800V/5C快充 东风日产NX8将于3、4月上市

态度原创

数码
艺术
教育
亲子
公开课

数码要闻

延续传统:罗技G PRO X2 SUPERSTRIKE鼠标国行名称为GPW5雪豹

艺术要闻

惊艳!越南摄影师镜头下的妩媚女子!

教育要闻

害群之马!老师因“把分数写在试卷上”被家长投诉,官方要求整改

亲子要闻

心血管风险或始于子宫孕期不良暴露留下"胎儿期烙印"

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版