Go 写的 DLL 小轻快无依赖,是一个极大的优势。而 aardio 又可以方便地内存加载 Go 写的 DLL,生成独立 EXE 文件。
首先执行下面的 aardio 代码编译 Go 源码生成 DLL 文件。aardio 会自动配置好编译环境。
import golang;var go = golang(); go.main = /package mainimport "C" //启用 CGO //下面这句注释指令导出 DLL 函数//export Add func Add(a int32,b int32) int32{ //aardio 中整数默认为 int32 类型,小数默认为 double 类型 return a + b;} //初始化函数,可以重复写多个func init() {}//必须写个空的入口函数,实际不会执行func main() {} ///编译 Go 源码生成同名 DLL 文件go.buildShared("/.go/start.go");
Go 代码有一个好处,几句代码就是一个完整的程序。

要想用 Go 编译 DLL,首先要导入 C 库启用 cgo 。
import "C"
这句代码前面的注释被称为 cgo 前导注释,用于指定 C 编译器指令、C语言代码。所以不要在这里写其他普通注释(可以为空行)。
Go 注释用途不小,例如函数前面的 export 注释被用来声明 DLL 导出函数。
//export Add func Add(a int32,b int32) int32{ //aardio 中整数默认为 int32 类型,小数默认为 double 类型 return a + b;}
其实 aardio 中的注释也有一些特殊用途:例如注释赋值给变量可用于表示复杂的字符串值( 上面的 Go 源码就是放在注释里赋值为字符串 ),aardio 还可以利用注释中的 import 语句引用库到发布程序但又并不实际加载(用于 fastcgi.exe 这种需要后期按需加载库的程序 )。
下面我们用 aardio 调用上面的 DLL:
/$操作符将 DLL 编译到内存(发布后不需要外部 DLL 文件)。注意 cgo 生成的 DLL 要指定 cdecl 调用约定。 /var dll = raw.loadDll($"/.go/start.dll","start.dll","cdecl"); //然后就可以直接调用 DLL 函数了var c = dll.Add(2,3);
aardio 调用 DLL 的语法对别简单,一般不需要声明。
如果要先声明 DLL 的导出函数,这样写:
//加载 DLL,参数 @2 要指定 DLL 共享名避免重复加载(文末解释原因)var dll = raw.loadDll($"/.go/start.dll","start.dll","cdecl");//声明 API,明确指定参数与返回值类型var add = dll.api("Add","int(int a,int b)" );var c = add(2,3);
用起来都是很简单的。
下面我们用 aardio 写个图形界面调用 Go 代码。
先新建一个 aardio 空白工程。
然后从『界面控件』拖放文本框、按钮到窗体设计器:
双击按钮切换到代码视图,编写代码如下:
//加载 DLLvar dll = raw.loadDll("/.go/start.dll","cdecl");//点击按钮触发事件mainForm.btnGo.oncommand = function(id,event){ //获取控件文本,并转换为数值 var a,b = tonumber(mainForm.editX.text),tonumber(mainForm.editY.text); //调用 Go 函数 var c = dll.Add(); //显示函数返回值 mainForm.edit.text = c;}
aardio 写图形界面很轻松,再看个例子:
二、aardio + Go 操作静态结构体
有些编程语言操作结构体很麻烦,但 aardio ,Go 操作静态结构体( struct )都很方便。
首先用 aardio 代码调用 Go 编译一个 DLL:
import console.int;import golang; var go = golang(); go.main = /package mainimport "C"import "unsafe"import "fmt"//声明结构体type Point struct { x int y int }//export SetPoint func SetPoint(p uintptr) { // aardio 结构体转换为 Go 结构体 point := (Point)(unsafe.Pointer(p)) point.x = 1 point.y = 2 fmt.Println( "在 Go 中打印结构体:",point );}func main() {} / go.buildShared("/.go/TestStruct.go");
然后 aardio 调用 DLL 的代码如下:
import console.int;//加载 Go 写的 DLLvar goDll = raw.loadDll("/.go/TestStruct.dll",,"cdecl"); //声明静态结构体 class Point { int x; int y;}//创建结构体var point = Point();//调用 Go 函数,传结构体(结构体总是传址)goDll.SetPoint(point);//打印结构体console.dumpJson(point); //结构体就是表(table),也可以这样直接写goDll.SetPoint({ int x = 1; int y = 2;});
aardio,C 语言,cgo,Go 静态类型对应关系如下:
aardio
C 语言
cgo
Go
BYTE
char
C.char
byte, bool
byte
singed char
C.schar
int8
BYTE
unsigned char
C.uchar
uint8, byte
word
short
C.short
int16
WORD
unsigned short
C.ushort
uint16
int
int
C.int
int32, rune
INT
unsigned int
C.uint
uint32
int
long
C.long
int32
INT
unsigned long
C.ulong
uint32
long
long long
C.longlong
int64
LONG
unsigned long long
C.ulonglong
uint64
float
float
C.float
float32
double
double
C.double
float64
INT
size_t
C.size_t
uint
pointer
void
unsafe.Pointer
注意这里指的是 aardio 静态类型(主要用于 DLL 接口编程)。在 aardio 里大写的整数类型名都表示无符号数(只有正值,没有负值)。
三、aardio + Go 操作 JSON
Go 操作 JSON 很溜,这个要利用一下。
不过想拿 Go 的字符串指针有些麻烦,Go 原则上不让你干这事,默认对输出的指针有严格的检查。如果按常规的方法调用 cgo 传字符串指针,这是有些繁琐的。
这里需要一点小技巧。
Go 的 DLL 里导出函数如下:
//export TestStringPtrfunc TestStringPtr(str string) { fmt.Printf("Go 通过 string 收到 aardio 字符串: %s!\n", str) ; str = "这是新的字符串";}
是不是变简单了?!
然后在 aardio 这样调:
import golang.string; var goStr = golang.string("这是 aardio 字符串,UTF-8 编码");//在 Go 里这个参数应当声明为 string 指针类型(aardio 结构体总是传址)goDll.TestStringPtr(goStr);//不要在 Go 中保存 aardio 传过去的字符串
在得到 goStr 以后要立即调用 tostring( goStr ) 转换为 aardio 字符串(自动释放 Go 的内存指针)。原理我可以看 golang.string 的源码,简单粗暴能用就行。
传字符串方便了,传 JSON 也就简单了。
下面先用 aardio 调用 Go 写一个 DLL:
import golang;var go = golang(); go.main = /package main import "C"import ( "time" "aardio" ) /Go 结构的 JSON 字段要大写首字母,每个字段可以在类型名后面额外添加 tag 字符串声明在 JSON 中的字段名。 /type QueryParam struct { Service string `json:"service"` Domain string `json:"domain"` Timeout time.Duration `json:"timeout"` }//export Query func Query(json string) { //创建结构体 var queryParam = QueryParam{} / 解析 JSON 到结构体, aardio.JsonParam() 返回函数对象用于更新 JSON。 defer 语句用于推迟到函数退出前调用。 / defer aardio.JsonParam(json, &queryParam)() //读取结构体的值,修改结构体的值,aardio 可以自动获取新值 queryParam.Domain = queryParam.Domain + "|www.aardio.com" }func main(){}/go.buildShared("/.go/jsonTest.go");
在 Go 语言里只要下面这一句:
defer aardio.JsonParam(json, &queryParam)()
就可以将 aardio 传过来的 JSON 解析为结构体,然后可以修改结构体,并且在函数退出前自动更新 aardio 里的 JSON 。
然后看 aardio 调用代码:
import console.int;import golang.string; //加载 DLLvar dll = raw.loadDll("/.go/jsonTest.dll",,"cdecl");//参数不是字符串、buffer、null 时会自动转换为 JSON 字符串var jsonParam = golang.string({ service = "_services._dns-sd._udp"; domain = "local"; timeout = 1000;})//调用 Go 函数dll.Query( jsonParam );//获取 Go 修改后的对象var goObject = jsonParam.value; //查看对象的字段值,已经被 Go 修改了console.dumpJson( goObject.domain )
Go 语言只要简单地通过 JSON 就可以获取、更新 aardio 里的对象。
整个代码量都很少。
Go 是一个有趣的编程语言。所以 aardio + Go 有很多有趣的用法,例如aardio 自带范例里的:
aardio.callaardio.callPtraardio.callJson
关于这些今天先跳过,下面先讲重点。
四、aardio 调用 Go 编写的 EXE
用 EXE 代替 DLL 作为运行模块是如今非常流行的一个方式。
不同的 EXE 运行在不同的进程,这种多进程交互的方式首先是非常稳定。一个 EXE 就算崩溃了也不会影响到另一个进程。其次跨进程调用可以兼容 32位、64 位 EXE,代码不需要任何改动。
我之前发布了一个很有意思的扩展库:
process.util`
这个扩展库用到的:
ProcessUtilRpc.dll
实际上就是用 Go 语言写的一个 EXE 程序,只不过后缀名是 DLL ( 后缀名无关紧要,可以随便改 )。
下面我们详细讲解 aardio 如何调用 Go 写的 EXE。
首先在 aardio 中运行下面的 Go 代码生成一个 EXE 程序。没有安装 Go 环境都没有关系,aardio 会自动安装。没有任何复杂步骤。
//导入支持库import golang;//创建 Go 编译器var go = golang();//编写 Go 源码go.main = / package main//导入模块import ( "net/rpc" "aardio/jsonrpc")//定义结构体type Calculator struct{}//定义下面的函数参数结构type Args struct { X, Y int} //定义允许 aardio 调用的远程函数func (t Calculator) Add(args Args, reply int) error { reply = args.X + args.Y return nil}//EXE 主启动函数func main() { //创建 RPC(远程函数调用) 服务端 server := rpc.NewServer() //导出允许客户端调用的对象 server.Register( new(Calculator) ) //运行服务端 jsonrpc.Run(server)}/ //生成 EXE 文件go.buildStrip("/goRpc.go");
改用 go.buildStrip64 可以生成 64 位 EXE( aardio 都可以调用 ) 。
生成的 goRpc.exe 负责运行 RPC (远程函数调用)服务端。
所有 Go 导出的 RPC 函数都必须有 2 个参数:1、args 参数接收 aardio 的调用参数。2、reply 参数用于保存函数返回值。Go 函数的返回值必须是 error 对象名 nil ,返回 nil 表示没有发生错误。
下面用 aardio 调用上面的 Go 程序:
import process.rpc.jsonClient;//启动 Go 服务端 var go = process.rpc.jsonClient("/goRpc.exe"); //调用 Go 函数var reply = go.Calculator.Add({ X = 2; Y = 3;} )//获取函数返回值var result = reply[["result"]];
代码非常简单。
reply 是服务端函数返回的响应对象。调用失败则 reply.error 为错误信息。调用成功则远程函数返回值放在 reply.result 里。
双 [[]] 是 aardio 的直接下标操作符,当写为 reply[["result"]] 时,即使 reply 是 null 或任何不包含 result 的对象都不会报错而是返回 null 值。
借用上面第一个例子里的窗体界面:
双击按钮切换到代码视图,编写代码如下:
import process.rpc.jsonClient;//创建远程函数调用客户端var go = process.rpc.jsonClient("/goRpc.exe"); //点击按钮触发事件mainForm.btnGo.oncommand = function(id,event){ //调用 Go 函数 var reply = go.Calculator.Add({ X = tonumber(mainForm.editX.text); Y = tonumber(mainForm.editY.text); } ) //获取函数返回值 mainForm.editReply.text = reply[["result"]];}
按 F5 运行就能看到效果了。
当然可以将 goRpc.exe 改名为 goRpc.dll ,后缀名无关紧要。
如果不想软件带个 goRpc.exe 文件,可以在 aardio 发布生成 EXE 后弹出的对话框上点击『转换为独立 EXE 』。
五、aardio , Go 语言通过 COM 接口交互
这是我今天刚写的一个例子。
下面用 Go 创建项目,自动安装 go-ole 模块,然后编写一个 DLL:
import console.int; import golang;//参数 @1 指定工作目录,默认为 "/"var go = golang("/go")go.setGoProxy("https://mirrors.aliyun.com/goproxy/,direct");//初始化 GO 项目go.mod("init golang/dispDemo")//安装第三方模块go.get("github.com/go-ole/go-ole") go.main = /package mainimport ( "C" "unsafe" "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" "fmt")//export TestDispatchfunc TestDispatch(dispatchIn uintptr) uintptr { //这里不需要初始化 OLE,aardio 自动支持这些 // 获取传入的 IDispatch 指针 dispatch := (ole.IDispatch)(unsafe.Pointer(dispatchIn)) // 调用 dispatch 对象的方法 result := oleutil.MustCallMethod(dispatch, "Add", 1, 2) defer result.Clear() // 假设 Add 方法返回一个数值,可以这样获取返回值 // value := result.Value() // 返回 interface{} // valueInt := result.ToInt() // 返回 int // valueFloat := result.ToFloat() // 返回 float64 // valueString := result.ToString() // 返回 string // 打印结果(假设返回一个数值) fmt.Println("Result:", result.Value()) // 创建新的 IDispatch 对象 clsid, err := ole.CLSIDFromProgID("Scripting.Dictionary") if err != nil { panic(err) } unknown, err := ole.CreateInstance(clsid, nil) if err != nil { panic(err) } defer unknown.Release() //这里增加引用计数 newDispatch, err := unknown.QueryInterface(ole.IID_IDispatch) if err != nil { panic(err) } // 返回新的 IDispatch 对象的指针(不必释放引用计数,由 aardio 接收时释放) return uintptr(unsafe.Pointer(newDispatch))}func main() { // 需要有一个空的 main 函数以满足 go build}/go.buildShared("/dispDemo.go");
下面在 aardio 里调用上面的 DLL:
//调用 DLLimport console.int; console.open();//内存加载 DLL,请先编译 Go 代码生成 DLLvar dll = raw.loadDll($"/dispDemo.dll",,"cdecl"); //aardio 对象转换为 COM 对象(COM 接口会自动转换,原生 DLL 接口要调用 com.ImplInterface )import com;var disp = com.ImplInterface( //任意表对象或函数都可以转换为 COM 对象(IDispatch 接口对象) Add = function(a,b){ console.log("Add 函数被 Go 语言调用了"); return a + b; } );//调用 Go 函数var pDisp = dll.TestDispatchP(disp);//将 Go 函数返回的 IDispatch 指针转换为 COM 对象var comObj = com.QueryObjectR(pDisp);//转换同时释放一次引用计数//操作 COM 对象comObj.Add("key","value");comObj.Add("key2","value2");//遍历 COM 对象for index,key in com.each(comObj) { //输出字典的键值 console.log( key,comObj.Item(key) )} console.log(ptr)
aardio 操作 COM 对象很方便,不需要额外的封装。
aardio 最常用的表对象自动兼容 COM 接口,在 COM 接口函数里会自动转换为 IDispatch 接口。
但是在 DLL 函数里要明确调用
com.ImplInterface
函数创建 COM 接口对象,例如:
var disp = com.ImplInterface( //任意表对象或函数都可以转换为 COM 对象(IDispatch 接口对象) Add = function(a,b){ console.log("Add 函数被 Go 语言调用了"); return a + b; } );
disp 对象传入 Go 函数就是一个 IDispatch 接口指针,go-ole 操作 IDispatch 指针就很方便:
// 获取传入的 IDispatch 指针dispatch := (ole.IDispatch)(unsafe.Pointer(dispatchIn))// 调用 dispatch 对象的方法result := oleutil.MustCallMethod(dispatch, "Add", 1, 2)
六、Go 编写 DLL 注意事项
相比 C/C++写的 DLL,Go 写的 DLL 有几个需要特别注意的地方:
1、在主线程加载 Go 写的 DLL,保持 DLL 对象不被释放(避免第二次加载同一 DLL )。其他线程加载同一 DLL 就只会增加引用计数, 不会重复加载。
2、如果用 $ 操作符,从内存加载 Go 写的 DLL,就必须在第二个参数中指定共享名称,这样 aardio 也不会重复加载内存 DLL,只会增加引用计数。
var dll = raw.loadDll($"/.go/start.dll","start.dll","cdecl");
3、加载 DLL 的主线程不要退出太快,除了测试,实际开发其实也不太可能这样干,谁会写个软件只有几句代码呢。真要这样干加个 sleep 语句延时一下( 实际上就是等 Go 初始化完成,但 Go 没有提供一个等待初始化或销毁完成的机制 )。
否则,重复加载相同 DLL,退出加载线程太快,Go 有一定机率会崩溃。这是 Go 语言的锅,与 aardio 没有关系。其他编程语言写的 DLL 也没有这问题。
其实没太太影响,稍加注意就能规避问题。
不求完美,很多事情就简单。