aardio 文档

aardio 调用 Go 语言入门指南

一. aardio 调用 Go 编写的 DLL #

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 代码如下:

//加载 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 语言如何操作结构体 #

有很多编程语言操作结构体并不容易,但 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 原生类型

三. aardio 调用 Go 语言通过 JSON 交互 #

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 自带范例里的:

四. 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"]] 时,即使 replynull 或任何不包含 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 接口交互 #

我们首先用 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)

六. 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 语言的对有的问题,其他编程语言写的 DLL 也没有相同问题。

其实没太太影响,稍加注意就能规避问题。

不求完美,很多事情就简单。

Markdown 格式