百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-06-24 19:22 12 浏览 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 也没有这问题。

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

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

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码