Go 语言 + aardio 快速开发图形化桌面软件,简单生成独立 EXE
liebian365 2024-11-18 14:19 27 浏览 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 也没有这问题。
其实没太太影响,稍加注意就能规避问题。
不求完美,很多事情就简单。
- 上一篇:C#调用solidworks
- 下一篇:计算IE的旋转角度与滤镜详解
相关推荐
- 月薪 4K 到 4W 的运维工程师都经历了什么?
-
运维工程师在前期是一个很苦逼的工作,在这期间可能干着修电脑、掐网线、搬机器的活,显得没地位!时间也很碎片化,各种零碎的琐事围绕着你,很难体现个人价值,渐渐的对行业很迷茫,觉得没什么发展前途。这些枯燥无...
- 计算机专业必须掌握的脚本开发语言—shell
-
提起Shell脚本很多都有了解,因为无论是windows的Dom命令行还是Linux的bash都是它的表现形式,但是很多人不知道它还有一门脚本编程语言,就是ShellScript,我们提起的Shel...
- Linux/Shell:排名第四的计算机关键技能
-
除了编程语言之外,要想找一份计算机相关的工作,还需要很多其他方面的技能。最近,来自美国求职公司Indeed的一份报告显示:在全美工作技能需求中,Linux/Shell技能仅次于SQL、Java、P...
- 使用Flask应用框架在Centos7.8系统上部署机器学习模型
-
安装centos7.8虚拟环境1、镜像链接...
- shell编程
-
简介:Shell是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。...
- 14天shell脚本入门学习-第二天#脚本和参数#排版修正
-
脚本是一种包含一系列命令的文本文件,通常用于自动化任务。Shell脚本是用Shell命令编写的脚本,可以在命令行中执行。掌握脚本的基础知识和变量的使用是编写高效脚本的关键。...
- 嵌入式Linux开发教程:Linux Shell
-
本章重点介绍Linux的常用操作和命令。在介绍命令之前,先对Linux的Shell进行了简单介绍,然后按照大多数用户的使用习惯,对各种操作和相关命令进行了分类介绍。对相关命令的介绍都力求通俗易懂,都给...
- 实现SHELL中的列表和字典效果
-
大家好,我是博哥爱运维。编写代码,很多情况下我们需要有种类型来存储数据,在python中有列表和字典,golang中有切片slice和map,那么在shell中,我们能否实现列表和字典呢,答案是肯定的...
- 14天shell脚本入门学习-第二天#脚本和变量
-
脚本是一种包含一系列命令的文本文件,通常用于自动化任务。Shell脚本是用Shell命令编写的脚本,可以在命令行中执行。掌握脚本的基础知识和变量的使用是编写高效脚本的关键。...
- shell常用命令之awk用法介绍
-
一、awk介绍awk的强大之处,在于能生成强大的格式化报告。数据可以来自标准输入,一个或多个文件,或者其他命令的输出。他支持用户自定义函数和动态正则表达式等先进功能,是Linux/unix一个强大的文...
- Linux编程Shell之入门——Shell数组拼接与合并
-
在Shell中,可以使用不同的方式实现数组拼接和合并。数组拼接指将两个数组中的元素合并成一个数组,而数组合并指将两个数组逐个组合成一个新数组。以下是关于Shell数组拼接和合并的详细介绍:数...
- shell中如何逆序打印数组的内容,或者反转一个数组?
-
章节索引图首先请注意,有序的概念仅适用于索引数组,而不适用于关联数组。如果没有稀疏数组,答案会更简单,但是Bash的数组可以是稀疏的(非连续索引)。因此,我们需要引入一个额外的步骤。...
- 如何学好大数据开发?---shell基本语法
-
昨天我们初步了解到了shell的一些基本知识,比如shell的分类,常用的shell类型。今天就带来大数据开发之shell基本语法,掌握好基础才是最重要的,那接下来就开始学习shell的基本语法。一、...
- Linux编程Shell之入门——Shell关联数组
-
关联数组是Shell中一种特殊的数组类型,它使用字符串作为下标。在关联数组中,每个元素都被标识为一个唯一的字符串键值,也称为关联数组的索引。在Shell中,可以使用declare或typeset命令...
- 从编译器视角看数组和指针
-
虽然有单独的文章描述数组和指针,但二者的关系实在值得再写一篇文章。...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)