Go 写的 DLL 具有轻量、快速、无依赖的优势,而 aardio 可以方便地将 Go 写的 DLL 内存加载,并生成独立的 EXE 文件。
首先,运行以下 aardio 代码来编译 Go 源码并生成 DLL 文件。aardio 会自动配置编译环境。
import golang;
var go = golang();
go.main = /**********
package main
import "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 空白工程。
然后从『界面控件』拖放文本框、按钮到窗体设计器。
双击按钮切换到代码视图。
然后我们编写 aardio 代码如下:
//加载 DLL
var 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 写图形界面很轻松,调用 Go 语言写的 DLL 也非常简单 。
有很多编程语言操作结构体并不容易,但 aardio 与 Go 语言操作结构体( struct )都很方便。
首先用 aardio 代码调用 Go 语言编译一个 DLL:
import console.int;
import golang;
var go = golang();
go.main = /**********
package main
import "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 写的 DLL
var 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语言与 aardio 操作 JSON 都很方便。
不过想拿 Go 的字符串指针有些麻烦,默认对输出的指针有严格的检查。如果按常规的方法调用 cgo 传字符串指针也是有些繁琐的。
这里需要一点小技巧。
Go 的 DLL 里导出函数如下:
//export TestStringPtr
func 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 传过去的字符串
在 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)()
在 Go 语言里就可以将 aardio 传过来的 JSON 解析为结构体,并且可以修改结构体,在函数退出前能够自动更新 aardio 里的 JSON 。
然后我们看 aardio 语言里的调用代码:
import console.int;
import golang.string;
//加载 DLL
var 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 自带范例里的:
用 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 个参数:
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 语言创建项目,自动安装 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 main
import (
"C"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"fmt"
)
//export TestDispatch
func 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:
//调用 DLL
import console.int;
console.open();
//内存加载 DLL,请先编译 Go 代码生成 DLL
var 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)
相比 C/C++写的 DLL,Go 写的 DLL 有几个需要特别注意的地方:
在主线程加载 Go 写的 DLL,保持 DLL 对象不被释放(避免第二次加载同一 DLL )。其他线程加载同一 DLL 就只会增加引用计数, 不会重复加载。
如果用 $ 操作符,从内存加载 Go 写的 DLL,就必须在第二个参数中指定共享名称,这样 aardio 也不会重复加载内存 DLL,只会增加引用计数。
var dll = raw.loadDll($"/.go/start.dll","start.dll","cdecl");
加载 DLL 的主线程不要退出太快,除了测试,实际开发其实也不太可能这样干,谁会写个软件只有几句代码呢。真要这样干加个 sleep 语句延时一下( 实际上就是等 Go 初始化完成,但 Go 没有提供一个等待初始化或销毁完成的机制 )。
如果不注意上面这些要点,重复加载相同 DLL,退出加载线程太快,Go 有一定机率会崩溃。这是 Go 语言的对有的问题,其他编程语言写的 DLL 也没有相同问题。
其实没太太影响,稍加注意就能规避问题。
不求完美,很多事情就简单。