QRust(三)编程框架

news/2024/11/14 17:00:05/文章来源:https://www.cnblogs.com/dyf029/p/18539581

把Rust作为动态库或静态库链接到Qt环境中,本就是一件复杂的工作,在此基础上还要引入QRust更是难上加难,因此在这一章我将手把手的引导你向前迈进,并跨过我曾经遇到的坑。

编程环境

Qt环境:Qt6,没错不支持Qt5。因为我发现struct的类型推导在Qt5环境下有错误。

Rust环境:理论上没有限制,但在windows环境下存在Qt链接Rust静态库时链接符号找不到的问题,此问题发生在Rust高版本上,因此我的Rust版本一直保持在1.65.0-nightly,不敢升级,而在MacOS和Linux环境中没有发现问题。

下面列出的是经过我实测的环境,特别说明不在表中并不意味着不支持,只是我个人没有能力进行更多的测试。

系统 发行版 C++环境 CMake版本 Qt版本 Rust版本
Windows X86 windows11 MSVC2019 CMake3.29.3 Qt6.7.2 1.65.0-nightly
Linux X86 Kylin V10(SP1) Kylin 9.3.0 CMake3.29.3 Qt6.7.2 1.81.0
MacOS M2 Sonoma 14.5 Apple clang 15 CMake3.24.2 Qt6.5.3 1.71.1

以上环境中Qt6的安装以及相关配置稍微复杂一些,特别是社区版的Qt6,每次安装可能会遇到不同的问题,耐心多试是成功的关键。

构建项目

QRust源代码中有两个项目:

  • 目录rust是Rust端项目,可直接进行cargo build –release的构建。
  • 目录qt-window是Qt端项目,为了能更好演示QRust的异步调用,这个项目是个窗口程序,而不是控制台程序。

这两个项目一起构建了演示Qrust的demo程序,强烈建议你的第一个实现在这两个项目基础上进行构建。

Rust端项目

serde_qrust目录是当前crate包含的一个子crate,是serde的序列化和反序列化的实现,如果你对serde的实现不感兴趣,可以略过这里的代码,我们把眼光聚焦在当前crate中,首先是项目配置文件Cargo.toml:

……
[dependencies]
libc = "*"log = "0.4"
log4rs = "0.10.0"serde = {version = "1.0", features = ["derive"]}
serde_qrust = {path = "./serde_qrust"}[lib]
crate-type = ["staticlib"]
name = "qrust"
[profile.release]
panic = "abort"
[profile.dev]
panic = "abort"

log4rs可以替换成你喜欢的日志库,请聚焦[lib]下的配置:

  • crate-type = [“staticlib”] 这一句表示编译的最终目的是静态链接库
  • name = “qrust” 这一句表示编译生成的库的名称,输出路径在target目录下,不同系统下库的扩展名可能不同,在windows下最终的输出文件是qrust.lib

src目录中有两个文件,lib.rs和api.rs

api.rs是完全和QRust解耦的,里面是demo函数的实现和所传递struct的定义,没有什么特殊性,当你将程序运行后,结合Qt端的调用,很容易就能理解文件中的每一行代码。这里为了讲述方便,将api.rs中的函数称之为“业务函数”,业务函数是你自己根据需求要实现的部分。现在聚焦到和QRust紧密相关的lib.rs。

fn invoke(fun_name: &str, mut args: Vec<&[u8]>) -> Result<Option<Vec<u8>>>
{match fun_name{"foo" =>    //无参数、无返回的函数调用示例
        {api::foo();     //调用函数
            Ok(None)} "foo1" =>   //有1个参数、无返回的函数调用示例
        {let a1 = de::from_pack(args.pop().unwrap())?; //反序列化获得参数
            api::foo1(a1);  Ok(None)}"foo2" =>   //有多个参数、有返回的函数调用示例
        {let a1 = de::from_pack(args.pop().unwrap())?;let a2 = de::from_pack(args.pop().unwrap())?;let a3 = de::from_pack(args.pop().unwrap())?;let ret = api::foo2(a1, a2, a3); let pack = ser::to_pack(&ret)?;     //返回值打包(序列化)
            Ok(Some(pack))}"foo_struct" => //自定义struct类型的示例  
        {let arg = de::from_pack(args.pop().unwrap())?;let count = de::from_pack(args.pop().unwrap())?;let ret = api::foo_struct(arg, count);let pack = ser::to_pack(&ret)?; Ok(Some(pack))}……
}

invoke函数我称之为“手动的运行时反射函数”,在这里做了3件事:

  • 根据Qt请求的代表函数的名称字符串,匹配(match)对应的业务函数
  • 执行业务函数前,通过from_pack() 解析出函数的参数(反序列化)
  • 执行业务函数后,通过to_pack()打包返回值(序列化)

对于一些支持运行时反射的语言(如Java),匹配和调用函数可以做到完全自动化,但Rust是没有这个功能的,所以需要在这里为每一个业务函数手工编写match代码。

如果业务函数数量很多match分支就显得过长,或者业务函数需要分模块处理,统一写在一个match中不太适合,在OnTHeSSH中我是这样处理的:

pub fn invoke(fun_name: &str, args: Vec<&[u8]>) -> Result<Option<Vec<u8>>>
{if fun_name.starts_with("rhost::")              //远端主机管理
    {crate::rhost::api_host::invoke_host(fun_name, args)}else if fun_name.starts_with("group::")        //远端主机分组管理
    {crate::rhost::api_group::invoke_group(fun_name, args)}else if fun_name.starts_with("workhosts::")     //multi工作中的远端主机
    {crate::rhost::api_workhosts::invoke(fun_name, args)}else if fun_name.starts_with("base::")        //基本能力
    {crate::base::api::invoke(fun_name, args)}......
}

在请求的函数名称字符串前添加模块前缀,比如遇到以”rhost::”开始的请求转到rhost::api_host::invoke_host函数,这样实现了分模块的处理方式。

仔细观察一下这几行代码(从vscode中截图):

三个参数a1,a2,a3的反序列化代码是完全相同的,但解析出的变量类型分别是Vec<i32>,HashMap<i32,String>和HashMap<String, String>,这是怎么做到的?实际上是由后面这一句 api::foo2(a1, a2, a3)决定的,因为 a1,a2,a3作为业务函数foo2的参数,而foo2的函数定义决定了他们的数据类型,因此在前面的反序列化过程中按对应的类型来求值,这种改变历史行为是否很奇妙?

这可不是量子力学,在Rust中叫做类型推导。类型推导的发生时刻并不在程序的运行期,而是编译期间,这也是Rust编译速度较慢的一个原因,因为编译器做了大量的附加工作。

同理,QRust的Qt端因为泛型模板的使用,编译速度也变得比较缓慢。

FFI接口

Rust端编译成链接库后,面向Qt端有5个定义好的,伪装成C函数的接口,他们的实现代码都在lib.rs文件中:

  • pub unsafe extern “C” fn qrust_init()
  • pub unsafe extern “C” fn qrust_call(in_ptr: *const c_uchar, size: c_int) -> Ret
  • pub unsafe extern “C” fn qrust_free_ret(ret: Ret)
  • pub unsafe extern “C” fn qrust_free_str(ptr: *const c_char)
  • pub unsafe extern “C” fn qrust_free_bytes(ptr: *const u8, len: c_int)

这5个接口是原始的支持C语言调用Rust的,符合FFI标准的函数,能看到一点FFI的复杂性了吧 :)

如果你只是QRust的使用者,而不关注QRust的设计和实现,这5个接口函数掌握第一个就可以了,下面逐一介绍:

1)qrust_init()  – 这个函数的作用是在调用Rust端业务函数之前,可以做点什么预先的事情。在demo中Qt端在开始的main()中调用了此函数,通过进程环境变量给Rust传递了一个路径,Rust端将log4rs产生的日志输出到这个路径下的日志文件中。Qt和Rust各有各的日志框架,不大可能将Rust的日志合并到Qt的qDebug()中,因此日志输出各走各的路。

2) qrust_call() 和 qrust_free_ret()  –  这两个函数是QRust的核心。Qt端在调用Rust端业务函数时,将业务函数的名称字符串、序列化后的参数进行二进制打包,并将二进制包的地址作为参数发送给Rust端;业务函数的返回值经过打包(序列化)后,将二进制包地址封装到Ret的结构中返还给Qt端调用者,这个过程由qrust_call()函数实现。

QRust的函数请求包和返回包,都是以地址方式传递的,请求包由Qt端生成,返回包由Rust端生成,在内存布局上这两个包都在堆上,因此函数调用后他们占用的内存需要释放掉。Qt和Rust对内存申请和释放的机制可能存在差异,因此不要去释放对方申请的内存。请求包由Qt端生成,在函数返回时Qt端自己可释放,但返回包由Rust端生成,且Rust端无法知道返回包在Qt端什么时间已使用完毕,因此无法进行释放。所以需要Qt通知Rust:“我已经使用完毕,你可以安全的释放内存了”,qrust_free_ret()函数就是干这个用的。

内存操作总是复杂且容易出错的,这也是C调用Rust的难点之一,好在QRust将这些操作封装在内部,使用者不会接触到这些底层代码。

3)qrust_free_str() 和  qrust_free_bytes() – 这两个接口函数在QRust当前版本中还没有完全实现,他们是配合大数据调用的接口。比如上百兆的字符串或二进制数据在Qt和Rust之间传递时,就不适合采用序列化机制,应该以地址方式直接传递,这两个接口是释放内存使用的。

Qt端项目

我们再把眼光聚焦到Qt端:

Qt端项目是在Qt Creator中按新建窗口项目方式创建出来的,因此你如果想从一个空项目开始,跟着下面的步骤就可以。

metastruct.h,qrust_de.h|.cpp, qrust_ser.h|.cpp,rust.h|.cpp 这7个文件是QRust在Qt端的实现源码,如果你对QRust的底层实现不感兴趣,可以忽略他们,只需要将这7个文件导入到项目中,且确保不要编辑修改。

首先,聚焦到CMake的配置文件CMakeLists.txt。

1)在find_package中添加’Concurrent’组件,此组件支持异步调用。

2)添加LINK_DIRECTORIES配置,定义qrust静态库路径,注意这个路径在Rust端项目的编译输出路径中。

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Concurrent)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Concurrent)LINK_DIRECTORIES(D:/MySelf/project/QRust/code/rust/target/release)

3)相应的在target_link_libraries中添加Concurrent组件和qrust静态库:

target_link_libraries(qt_window PRIVATEQt${QT_VERSION_MAJOR}::WidgetsQt${QT_VERSION_MAJOR}::Concurrentqrust
)

4)复制metastruct.h,qrust_de.h|.cpp, qrust_ser.h|.cpp,rust.h|.cpp 7个文件到项目中,并在cmake配置文件中加载:

set(PROJECT_SOURCESmain.cppmainwindow.cppmainwindow.hmainwindow.uimetastruct.hqrust_de.cpp qrust_de.hqrust_ser.cpp qrust_ser.hrust.cpp rust.h
)

再聚焦到main.cpp文件:

1)在main.cpp中需加载ws2_32.lib和Bcrypt.lib,这是windows环境下编译时链接qrust所依赖的库文件。注意在非windows环境中需要将这两行注释掉。

2)在main()中调用rust端函数qrust_init(),此调用并非必须,但如果想做一些初始化工作,比如rust端的日志配置。

#include "mainwindow.h"
#include <QApplication>#pragma comment (lib, "ws2_32.lib")
#pragma comment (lib, "Bcrypt.lib")//qrust_init()是rust端函数,因此声明为 extern "C"
extern "C" {void qrust_init();
}int main(int argc, char *argv[])
{QApplication a(argc, argv);//在这里通过调用qrust_init(),设置rust的日志输出路径QString log_path = "d:/TMP/log";  //这个路径要修改qputenv("LOG_PATH", log_path.toStdString().c_str());  //设置环境变量qrust_init();  //qrust初始化
 MainWindow w;w.show();return a.exec();
}

Mainwindow.h|.cpp文件是QRust带的demo,我在下一章讲解。

编译和运行:

编译时需要先编译Rust端,因为它要作为Qt端的链接库。Rust编译命令:

cargo build --release

注意在编译调试的过程中,Qt并不能识别Rust生成的静态链接库的变动,当你重新编译Rust端后紧接着编译Qt,Qt仍按老版本运行。解决这个问题可以在Qt端调用Qrust的代码文件中添加或删除一个空格,让文件发生改变,触发Qt对此文件的重新编译,连带对链接库的重新链接。

Qt端项目编译时,Linux下CMake可能会发生以下错误:

  undefined reference to symbol ‘dlsym@@GLIBC_2.4’

解决方法是在target_link_libraries下添加dl库,如下:

target_link_libraries(qt_window PRIVATEQt${QT_VERSION_MAJOR}::WidgetsQt${QT_VERSION_MAJOR}::Concurrentqrustdl
)

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/831773.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ARL356-ASEMI车用整流二极管ARL356

ARL356-ASEMI车用整流二极管ARL356编辑:ll ARL356-ASEMI车用整流二极管ARL356 型号:ARL356 品牌:ASEMI 封装:BUTTON 特性:车用整流二极管 正向电流:35A 反向耐压:600V 恢复时间:ns 引脚数量:2 芯片个数:2 芯片尺寸:MIL 浪涌电流:500A 漏电流:10ua 工作温度:-65℃…

信息收集(下)

IP端口信息收集4.1 IP信息收集4.1.1 IP反查域名http://stool.chinaz.com/samehttp://tools.ipip.net/ipdomain.php如果渗透目标为虚拟主机,那么通过IP反查到的域名信息很有价值,因为一台物理服务器上面可能运行多个虚拟主机。这些虚拟主机有不同的域名,但通常共用一个IP地址…

web(xss漏洞作业)

web安全作业(xss漏洞)需要掌握的知识XSS漏洞(原理、分类、危害、常用的payload)XSS平台的搭建与使用理论作业 1.xss 漏洞原理? 2.XSS的类型? 3.反射型XSS和存储型XSS的区别? 4.XSS的攻击过程? 5.XSS常用的payload构造方法? 1.原理:攻击者在有漏洞的前端页面嵌入恶意代…

QRust(一) 简介

QRust是一个开源组件,是Qt和Rust两种语言的混合编程中间件,是Qt调用Rust函数的支持技术。QRust来源于工具软件OnTheSSH,OnTheSSH软件由Qt和Rust两种语言共同构建,Rust实现了SSH通讯底层协议,Qt搭建程序界面,Qt调用Rust的技术需求催生出了QRust。 一个使用QRust的例子: R…

服务器漏洞修复:TLS 1.0 已启用、HSTS、CSP

1、TLS 1.0 已启用 描述: 此 Web 服务器支持通过 TLS 1.0 加密。TLS 1.0 不被认为是“强密码术”。根据 PCI 数据安全标准 3.2(.1) 的定义和要求,在保护从网站往返的敏感信息时,TLS 1.0 并不被认为是 "强加密"。根据 PCI,"2018 年 6 月 30 日是禁用 SSL/早前…

一款 C# 编写的神经网络计算图框架

前言 深度学习技术的不断发展,神经网络在各个领域得到了广泛应用。为了满足 .NET 开发的需求,推荐一款使用 C# 编写的神经网络计算图框架。 框架的使用方法接近 PyTorch,提供了丰富的示例和详细的文档,帮助大家快速上手。 框架介绍 项目完全使用 C# 编写,提供了一个透明的…

安全通道异常识别系统

安全通道异常识别系统通过安装在消防通道附近的监控摄像头,安全通道异常识别系统对安全通道进行24小时不间断的监控。当系统检测到防火门被异常开启或安全通道被堵塞时,会立即启动警告机制,通过声音、灯光等方式提醒相关人员进行处理。同时,系统还会将异常信息实时传输到监…

creator2.4.5 astc问题排除

最近将creator2.4.5 发布web版本, 需要将所有的图片转astc, 但是不能无脑转, 需求去除预乘alpha的图片。 也就是 { "__type__": "cc.Texture2D", "content": "0,9729,9729,33071,33071,1,0,1" }这个content.split(",")[5…

工地施工机械设备识别检测系统

工地施工机械设备识别检测系统基于AI人工智能机器视觉分析识别技术,工地施工机械设备识别检测系统利用现场安装的监控摄像头,通过深度学习和图像识别技术,实现对工地上重型机械车辆的自动识别。这些重型机械车辆包括但不限于挖掘机、推土机、吊车等。一旦系统识别到这些车辆…

山体滑坡泥石流预警监测系统

山体滑坡泥石流预警监测系统基于AI人工智能机器视觉分析识别技术,山体滑坡泥石流预警监测系统通过利用现场监控摄像头,实现了对高危路段山体的实时监测。这种技术不仅取代了传统的人工巡查,提高了工作效率,守护人们的生命安全。山体滑坡泥石流预警监测系统的核心在于其强大…

数据库设计心得-实习空间

数据库设计心得——实习空间 前言 在软件工程导论以及数据库实验课程中,我们学习了如何通过分析业务需求来构建数据库实体对象以及PowerDesigner的使用。最终通过PowerDesigner完成了本项目的数据库概念模型、物理模型的设计。以下是我们团队的数据库设计过程以及一些心得体会…

智启新篇 | 您的专属AI大模型解决方案!

随着汽车智能化程度的不断提升,智能座舱作为汽车的重要组成部分,其功能和复杂度也在不断增加。从多屏交互、增强现实(AR)技术到个性化设置和语音交互,智能座舱为驾驶者提供了前所未有的驾驶体验。然而,这些新技术的引入也使得智能座舱的测试变得更加复杂和困难。在这样的…