【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

从 React 的视角谈谈 Rust 和 GTK

  • 2020-03-24
  • 本文字数:5543 字

    阅读完需:约 18 分钟

从React的视角谈谈Rust和GTK

最近我尝试了多种框架,想要制作出既易用又容易安装的应用程序,但是都以失败告终;最后我决定转向 Rust 和 GTK,开始拥抱原生软件开发。


虽说以前我也短暂尝试过 GTK,但它对我来说还是很陌生的。在此之前,我在用户界面上的大部分经验都来自于 React 应用程序的构建。从 React 到 GTK 的过渡带来了一些挑战,其中多数是小部件原理上的差异造成的。用 Rust 写 GTK 是尤其困难的事情,因为 Rust 强制执行一些额外的规则来防止内存管理错误,并避免在线程上下文中执行不安全的操作。


在本文中,我将主要讨论如何将 React 的理念应用到 GTK 中,并重点介绍一些使 GTK 符合 Rust 规则所必需的技巧。Rust 制订了一些不好对付的强制规则,这些规则对于大多数开发人员来说都是陌生的;规则主要涉及值的共享方式,但在可变性方面也有严格的限制。我将在本文中遇到这些场景时指出它们。


本文中的所有示例均来自 FitnessTrax(https://github.com/luminescent-dreams/fitnesstrax/),这是一款遵循隐私优先原则的健身追踪应用程序。用户可以在他们的 PC 上的一处存储空间内收集健身和生物识别数据,而不必依赖那些可能无法持续保护用户数据的公司。


关于这款应用程序的外观我要说句抱歉,因为 0.4 版(https://savanni.luminescent-dreams.com/2020/01/03/weekly-ramblings/)发布的时候,我还没去花时间了解 GTK 是如何处理样式的。我保证会尽快改进用户界面。

框架哲学上的一些差异

Conrod(https://github.com/PistonDevelopers/conrod)是针对 Rust 的一个图形工具包,它试着将函数式响应编程(https://en.wikipedia.org/wiki/Functional_reactive_programming)技术应用到了图形编程上;它的开发者它描述了两种有着明显区别的图形组件管理模式(https://docs.rs/conrod/0.61.1/conrod/guide/chapter_1/index.html#immediate-mode)。在大多数原生图形编程采用的通用模式,亦即“保留模式(retained mode)”下,开发人员将创建一个个屏幕组件,然后在它们的整个生命周期内一次次更新。在“立即模式(immediate mode)”下,组件将具有一个绘制(draw)方法,其中组件会立即实例化自身的所有子级。然后,框架将对比这棵树与上一棵树,来判断该如何更新屏幕。


React 完全运行在即时模式下,而 GTK 完全运行在保留模式下。在 Web 开发行业中流行的数据可视化库 D3(https://d3js.org/)也可以运行在保留模式下。2018 年,我写了一篇关于 React 和 D3 之间对接的文章(https://www.cloudcity.io/blog/2018/08/07/breaking-d3s-deathgrip-on-the-dom-bringing-old-code-back-to-life-in-a-react-era/)。


与 Redux 或 Apollo-GraphQL(https://www.apollographql.com/)搭配的 React 实现了函数式响应编程(FRP)的一些理念,让它可以自动处理传播到组件的数据更改。我入门 FRP 时看的是 Elise Huard 写的一本书“Haskell 中的游戏编程”(https://leanpub.com/gameinhaskell)。时至今日这本书可能已经过时了,但在 Haskell 中特定的某个 FRP 库的背景下,它确实很好地介绍了这种理念。不幸的是,FRP 尚未在 React 之外得到广泛采用。虽说至少有一个可用于 Rust 的 FRP 库,但在撰写本文时,对于我来说采用它还为时过早。因此,凭借一些创造力和我在 React 领域的经验,我设计了一些类似于 FRP 范式的机制。


一些术语的注释:


  • 小部件(widget)是一个 GTK 对象,代表屏幕上的某些内容。它可以是一个窗口、按钮、标签或一个布局容器。GTK 小部件只能将其他 GTK 小部件作为自身的子级。

  • 组件是屏幕上一个部分的任意逻辑抽象。在简单的情况下,它会是一个从某个函数返回的 GTK 小部件。在更复杂的情况下,它可能是包含一个或多个小部件的结构。组件不一定必须传递给 GTK 函数。结构组件始终提供一个’widget’字段,其代表这个组件的根小部件。

不可变值的显示

所有组件中最简单的,就像 React 组件一样是一小组小部件,这些小部件创建后就永远不会更新。这可以简单地实现为返回一个 GTK 小部件的函数。



pub fn date_c(date: &chrono::Date<chrono_tz::Tz>) -> gtk::Label {    gtk::Label::new(Some(&format!("{}", date.format("%B %e, %Y"))))}
复制代码


当组件实际上是一个很少或甚至从不更新的可视组件时,这种模式就是可行的。在我的应用程序中,日期标签是更大一块显示内容的子组件,因此是永远不变的东西。

具有内部小部件状态的组件

具有内部小部件状态的组件肯定要复杂得多,但仍然可以实现为一个返回 GTK 小部件的函数。调用方可以直接从返回的 GTK 小部件中读取数据;在调用方提供一个回调,并且组件代码写明了何时调用回调时,这种模式可以说是最有效的。


我有一个会验证文本的输入字段。这是一个常规的 gtk::Entry(https://gtk-rs.org/docs/gtk/struct.Entry.html),但是接口抽象了’render’、'parse’和’on_update’函数背后的文本处理过程。



pub fn validated_text_entry_c<A: 'static + Clone>(    value: A,    render: Box<dyn Fn(&A) -> String>,    parse: Box<dyn Fn(&str) -> Result<A, Error>>,    on_update: Box<dyn Fn(A)>,) -> gtk::Entry {    let widget = gtk::Entry::new();    widget.set_text(&render(&value));
let w = widget.clone(); widget.connect_changed(move |v| match v.get_text() { Some(ref s) => match parse(s.as_str()) { ... }, None => (), });
widget}
复制代码


调用者必须提供一个初始值、一个’render’函数,一个’parse’函数和一个’on_update’函数。在我的实现中,验证文本的输入框将在每次更改后尝试解析框中的字符串,并且仅在解析成功时才调用’on_update’函数。这样以来,调用方负责保存数据,而不必去管解析或验证数据是否有效的机制。


我发现,将一个表单的所有值都存储在一个位置的模式特别有用。将所有数据存储在一起可以让我立即将错误通知给用户,还可以检测出由于无效数据组合而发生的错误,并能在出现错误时轻松禁用“提交”按钮。

具有内部状态的组件

2020-01-31:事实证明,我在本节的代码中犯了一些大错。我需要对其进行相当大的修改,以更有效地处理组件更新,并在一个 GTK 回调中更改组件状态。


当我使用上面提到的这种简单组件构建应用程序时,我将它们组合到一些更复杂的组件中,这些组件具有多份逻辑上互相归属,但机制上可以在各个子组件中编辑的数据。为此,我在设置内部状态时会独立于子组件的状态。


所幸我一般来说还是可以将其实现为一个函数。


拿骑自行车来举例,我把这个活动抽象为一个“时间/距离”记录。一个时间/距离事件具有一个开始时间、一个活动类型(骑自行车、步行、奔跑、皮划艇旅行…)、一段距离和一段持续时间。我的用户界面将所有这些都绑定到一个组件中,可以一次性更新全部记录。



pub time_distance_record_edit_c(    record: TimeDistanceRecord,    on_update: Box<dyn Fn(TimeDistanceRecord)>,    ) -> gtk::Box {}
复制代码


到了这里,我们就开始遇到 Rust 用来确保安全内存管理的强制规则了。


每个值都只有一个所有者。虽然你可以借用该值的引用,但是只有这些引用超出范围后,该值的所有者才超出范围。此外,如果没有其他任何类型的引用,则你只能获得一个可变的引用。Rust Book(https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html)详细讨论了这些规则,并提供了大量示例和场景。


还好所有内容已经齐备了。我需要一种在多个回调函数之间共享记录的方法,并且需要一种方法来确保对记录的安全多线程访问。我们用一个 Arc(https://doc.rust-lang.org/std/sync/struct.Arc.html)来解决共享问题。这是一个线程安全的引用计数容器。传递给 Arc 的初始化器的所有值都归 Arc 所有。克隆一个 Arc 会增加引用计数,并创建另一个指向该共享值的引用。


Arc 不允许对其包含的值进行可变访问,因此我们还需要包含一个 RwLock(https://doc.rust-lang.org/std/sync/struct.RwLock.html)。像我们期望的那样,RwLock 允许多重读取者,但仅允许一个写入者,并且当存在写入者时不允许有读取者。于是我们像这样来安全更改记录:


pub time_distance_record_edit_c(    record: TimeDistanceRecord,    ...) -> gtk::Box {
let record_ref = Arc::new(RwLock::new(record));
{ let mut rec = record_ref.write().unwrap(); ref.activity = Cycling }
复制代码


在代码子块内,'rec’成为对记录数据的一个可变引用。'RwLock’控制对数据的读/写访问,而’Arc’允许跨函数甚至线程共享数据。


综上所述,我们的代码如下所示:


pub time_distance_record_edit_c(    record: TimeDistanceRecord,    ...    on_update: Box<dyn Fn(TimeDistanceRecord)>,) -> gtk::Box {
let on_update = Arc::new(on_update); let record = Arc::new(RwLock::new(record));
let duration_entry = { let record = record.clone(); let on_update = on_update.clone(); let duration = record.read().unwrap().duration.clone(); duration_edit_c( &duration, Box::new(move |res| match res { Some(val) => { let mut r = record.write().unwrap(); r.duration = Some(val); on_update(r.clone()); } None => (), }), ) };}
复制代码


(注意:函数始终是只读的,因此仅需要'Arc'即可共享)


回顾一下,在上面的函数中,我们有一段代码来克隆包含记录的 Arc。该克隆将移至’duration_edit_c’的回调函数中(这意味着该回调函数现在拥有这个克隆)。在这个回调函数中将可变地借用记录、更新记录、克隆数据并将其传递给’on_update’,然后将在该块末尾自动删除写锁定。


一下子要学的东西真不少。如果你不熟悉 Rust,我强烈建议你阅读有关所有权和借用系统的知识(https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html),这是让内存管理无需开发人员操心,而又不会带来垃圾收集器负担的魔法。

从系统状态更改来更新

最后,第四个模式涵盖了需要响应系统更改的所有组件。用 React 术语来说,这意味着属性可能从 Redux 更改。


在较高的层级上,我们需要一个’struct’来跟踪在给定新数据时可能会更新的所有可视组件,以及一个将处理这些更新并返回根级小部件的’render’函数。


在这里我用自己的 History 组件举例。



struct HistoryComponent {    widget: gtk::Box,    history_box: gtk::Box,}
pub struct History { component: Option<HistoryComponent>, ctx: Arc<RwLock<AppContext>>,}
impl History { pub fn new(ctx: Arc<RwLock<AppContext>>) -> History { ... }
pub fn render( &mut self, range: DateRange, records: Vec<Record<TraxRecord>>, ) -> &gtk::Box { ... }
复制代码


实际上,这里的构造函数非常简单,除了创建抽象的’History’组件外什么都不做。由于它没有数据可填充到小部件中,因此它这里甚至还没有创建小部件。这样非常方便,因为在构造时组件可能会需求尚不可用的数据。


大部分工作是在’render’中完成的:


    pub fn render(        &mut self,        range: DateRange,        records: Vec<Record<TraxRecord>>,    ) -> &gtk::Box {        match self.component {            None => {                let widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);
/* create and show all of the widgets */
self.component = Some(HistoryComponent { widget, history_box, });
self.render(prefs, range, records) } Some(HistoryComponent {...}) => { .. } } }
复制代码


如果这是第一个’render’调用,则可视组件尚不存在。'Render’将创建所有组件,然后再次调用自身以使用数据填充它们。


    pub fn render(        &mut self,        range: DateRange,        records: Vec<Record<TraxRecord>>,    ) -> &gtk::Box {        match self.component {            None => {                ...            }            Some(HistoryComponent {                ref widget,                ref history_box,                ...            }) => {                history_box.foreach(|child| child.destroy());                records.iter().for_each(|record| {                    let ctx = self.ctx.clone();                    let day = Day::new(                        record.clone(),                        ctx,                    );                    day.show();                    history_box.pack_start(&day.widget, true, true, 25);                });                &widget            }        }    }
复制代码


在随后的调用中,render 将处理小部件的更新。如何填充新数据的细节因组件而异。在本例中我将销毁所有现有子组件,并根据我拥有的数据创建新的子组件。这是一个非常幼稚的策略,但有时它挺好用的。

总结

就这些了。经过数周的学习,在理解如何编写 GTK 的过程中我发现了四种高级模式。我觉得这些模式已经很完备了。


在撰写本文的整个过程中,我也对我的组件做了大量修改、重构和简化。我想这四种模式将帮助我进一步改进我的应用程序,同时我也希望在继续学习的过程中能学到更多内容。


原文链接:https://savanni.luminescent-dreams.com/2020/01/15/rust-react-gtk/


2020-03-24 10:042633

评论

发布
暂无评论
发现更多内容

合合信息加入元脑生态 携手浪潮信息推动金融AI应用创新

合合技术团队

AI 智能时代 合合信息 人工智能’ 浪潮

前端培训学完课程后如何找工作?

小谷哥

大数据培训学习软件工程师机构靠谱吗

小谷哥

终于!极狐GitLab 支持 ARM 啦!

极狐GitLab

DevOps 敏捷开发 arm 极狐GitLab 嵌入式硬件

史上最全的Java并发系列之Java并发容器和框架

自然

多线程 并发 8月月更

2022 CCF国际AIOps挑战赛决赛暨AIOps研讨会成功举办

BizSeer必示科技

人工智能 AIOPS

开源一夏 | 如何使用Java操作华为对象存储OBS删除一个目录?

wljslmz

Java 开源 对象存储 华为云 8月月更

零门槛掌握基于大模型技术的AIGC场景应用

文心大模型

CSO视角:Sigstore如何保障软件供应链安全?

SEAL安全

软件供应链安全 OpenSSF

预约直播 | 深度学习编译器技术趋势与阿里云BladeDISC的编译器实践

阿里云大数据AI技术

深度学习 阿里云 编译器

史上最全的Java并发系列之Java并发机制的底层实现原理

自然

多线程 并发 8月月更

如何利用CANN DVPP进行图片的等比例缩放?

华为云开发者联盟

人工智能 图像 昇腾AI

华贵保险连续五年获得“A级纳税信用企业”

江湖老铁

Java 异步调用原理与实战

PPPHUANG

性能优化 线程池 Async Java core java nio

易周金融分析 :支付科技着力服务产业数字化升级

易观分析

金融 产业数字化 分析 支付科技

2篇论文入选KDD 2022!腾讯广告持续探索效果提升新思路

Geek_2d6073

首批成员!博云入选信通院“可信边缘计算推进计划”

BoCloud博云

云计算 开源 云原生

Alluxio on Amazon EMR 集成实践

亚马逊云科技 (Amazon Web Services)

实践 集成 Tech 专栏

IPv5是什么意思?到底有没有IPv5?

郑州埃文科技

ipv6 互联网协议 ipv5

二分查找:一种效率较高的查找方法

华为云开发者联盟

开发 二分查找 查找 区间

开源公开课丨ChunJun数据传输模块介绍

袋鼠云数栈

科技赋能会展!3DCAT助力广东旅博会元宇宙场景的首次搭建

3DCAT实时渲染

云计算 元宇宙

RT-Thread记录(八、理解 RT-Thread 内存管理)

矜辰所致

内存管理 RT-Thread 8月月更

干货复盘 | 银行数智化转型十大趋势

易观分析

金融 银行 数智化转型

java就业培训班如何选择?

小谷哥

大数据程序员参加培训好还是自学好

小谷哥

virtio 1.2 来了!龙蜥社区携手业界打造新版虚拟化 IO 标准

OpenAnolis小助手

开源 虚拟化 龙蜥技术 virtio

SAP ABAP Netweaver 服务器的标准登录方式讲解

Jerry Wang

web开发 web服务器 abap Netweaver 8月月更

2分钟一图看懂AntDB数据库产品

亚信AntDB数据库

AntDB 国产数据库 aisware antdb

SAP ABAP 关键字语法图和 ABAP 代码自动生成工具 Code Composer

Jerry Wang

Java SAP abap commerce 8月月更

计算机专业和培训出来的前端程序员的区别

小谷哥

从React的视角谈谈Rust和GTK_大前端_Oleg Obleukhov_InfoQ精选文章