百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Go 语言 + aardio 快速开发图形化桌面软件,简单生成独立 EXE

liebian365 2024-11-18 14:19 8 浏览 0 评论

一、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(#34;/.go/start.dll","start.dll","cdecl");
 
//然后就可以直接调用 DLL 函数了
var c = dll.Add(2,3);

aardio 调用 DLL 的语法对别简单,一般不需要声明。

如果要先声明 DLL 的导出函数,这样写:

//加载 DLL,参数 @2 要指定 DLL 共享名避免重复加载(文末解释原因)
var dll = raw.loadDll(#34;/.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 写图形界面很轻松,再看个例子:

二、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 + Go 操作 JSON

Go 操作 JSON 很溜,这个要利用一下。

不过想拿 Go 的字符串指针有些麻烦,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 传过去的字符串

在得到 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; 

//加载 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.call
aardio.callPtr
aardio.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 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(#34;/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(#34;/.go/start.dll","start.dll","cdecl");

3、加载 DLL 的主线程不要退出太快,除了测试,实际开发其实也不太可能这样干,谁会写个软件只有几句代码呢。真要这样干加个 sleep 语句延时一下( 实际上就是等 Go 初始化完成,但 Go 没有提供一个等待初始化或销毁完成的机制 )。

否则,重复加载相同 DLL,退出加载线程太快,Go 有一定机率会崩溃。这是 Go 语言的锅,与 aardio 没有关系。其他编程语言写的 DLL 也没有这问题。

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

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

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: