0%

练手项目小结之 Umbra

开端

五月的某个早上,一觉醒来后顺手打开手机,发现收到了 JetBrains 的通知邮件 —— CLion 免费了。

大学时我曾短暂地体验过一段时间的 CLion,不过那时几乎不怎么用 C/C++,也不懂 CMake 怎么配置,只是糊里糊涂地拿来写过一些零零碎碎的代码片段。再后来我装了双系统,日常使用切换到了 Linux,偶尔想学学 Linux 系统编程相关的东西,却发现能选择的 IDE 都不太好用。其中 VSCode 算是挺不错的了,不过我习惯了 JetBrains 系列的 IDE,总感觉 VSCode 用起来哪里有点不太舒服。

所以当时看到 CLion 免费了的邮件后我就想着用 C 或者 C++ 写点什么,不过却一直都没想到什么用来练手的好点子。

顺带一提,去年写 Flutter 的时候,为了 Debug 我去翻了翻 media-kit 的源代码,发现其在 Linux 平台下的实现用的是 C++ / GTK3 / MPV 这一套。当时花了很多时间也没弄明白,毕竟 GTK,MPV 和 OpenGL 我是一样都不会。

虽然后来发现这个 Bug 是 Flutter 自己的问题,但自己还是想着有机会了的话就去试试 GTK。这下 CLion 免费了,连 IDE 不太趁手的问题也解决了。

CMake

既然都使用 CLion 了,那当然也得把 CMake 用起来(并不是)。

CMake 虽然一直以来都被很多 C/C++ 开源项目广泛使用,但其因为配置复杂也被不少人诟病。社区内也出现了像 Meson 之类的新秀。顺带一提,目前 GTK 和开源游戏引擎 Godot 都是默认使用 Meson 来构建项目的。

虽然我也很想试试 Meson,不过考虑到 CMake 目前还是挺主流的,经常看见 C++ 项目里都会有一个 CMakeLists.txt 文件,所以感觉最好还是先了解一下 CMake 的用法。

幸运地是,CMake 官方提供了一份渐进式的入门教程。跟着示例项目一步一步了解各项概念的基本用法,意外地发现并不是很难(错觉)。因为示例项目本身很简单,在不涉及复杂的依赖关系和环境配置的情况下,CMake 要配置的东西其实并不多,很容易给人简单的错觉。好在我自己的练手项目本身也就是个极小的玩具项目,因此使用 CMake 也不是什么问题。

其实 CMake 的入门教程看完后,我惊讶地发现居然这么简单,然后心态膨胀地去翻了翻开源项目 Sunshine 的 CMake 配置文件。虽然不再像是第一次看见时那样一头雾水了,却还是发现项目复杂起来后构建配置也会随之越发复杂。也难怪 Android 的 Gradle 这么复杂了。

GTK 生态

UI 框架的选择倒是不需要犹豫,毕竟 Flutter 自己在 Linux 上都用的是 GTK 3。

在很久以前的文章里我曾吐槽过 GTK 的官方文档更像是一份详尽的 API 参考手册,而不太像是一份完善的学习教程。如果你是一个新手的话,只看官方文档可能还是会感觉摸不着头脑。

不过这次好在我发现了一份大佬写的入门教程。GObject 教程GTK 4 教程 从头为新手介绍了 GTK 中的诸多基础概念,完美解答了我之前看官方文档时产生的种种疑惑。

为什么里面混进去了一个 GObject 教程呢?因为 GTK 本身是使用 C 语言开发的,而众所周知,C 语言的语法极其简洁,缺乏很多现代编程语言所拥有的特性 —— 比如对面向对象编程的原生支持。GObject 则用 C 语言实提供了一个面向对象编程的实现,算是在某种层面上补齐了一部分 C 的短板。所以学习 GTK 是一定要先看看这份 GObject 教程 的。

GTK 自身除了依赖 GObject 之外,还依赖了多个其他库,如 GLib,GIO,Cairo,GDK 等等。在 Umbra 这个项目的开发过程中,如果用到了相关的功能,我经常会需要去查对应的文档。个人感觉其中查得最多的就是 GLib 的文档了。

如果说 GObject 是提供了一套 OOP 的实现的话,那么 GLib 就是提供了一套 C 语言的基础设施。C 语言本身的标准库提供的功能很核心、很简洁,但这也意味着只使用标准库的话,那么有很多基础功能都必须得自己实现。

GLib 帮我们减轻了这部分负担。它提供了一套常用的数据结构,如动态数组、链表、哈希表等,这几乎是每个程序都会使用到的数据结构。此外 GLib 还提供了一些内存管理工具,g_autoptrg_autofree 这些宏极大地减轻了手动释放内存的痛苦。查看 GLib 的官方文档 可以发现它还提供了种种类似的基础功能,如字符串操作工具、日志工具、跨平台支持等等。正是因为有了这些工具,我们用 C 语言开发程序才不至于特别痛苦。

GTK 开发体验

虽然前面我赞美了 GLib 减少了开发 C 程序的痛苦,但是,不得不说使用 C 语言开发 GTK 4 程序还是有点痛苦的 —— 特别是对于那些用惯了现代编程语言和现代 GUI 框架的人来说。

首先是 C 语言的问题。

在 C 语言中没有诸多现代编程语言的特性,也没有垃圾回收,你需要使用指针手动管理自己分配的内存。这一点当然不能说是 C 语言的问题,甚至可以说是 C 语言接近底层的优势所在。但是,一旦场景变成了不太那么涉及底层的 GUI 开发,这个优势就变成负担了。你需要时刻关注你所使用的指针的生命周期,需要时刻提醒自己不要忘了及时释放内存。

这一点在涉及到调用外部库函数时尤为突出。对于每个库函数你都需要去查阅文档,看清楚每个参数和返回值的指针所指向的数据的归属,以免忘记释放内存或者释放了不属于自己管理的内存。这虽然不至于造成太多的心智负担,却会不可避免地降低程序员的开发效率。—— 不过话说回来,这在某种程度上也是相关库函数的头文件中没有详细的文档注释导致的。

总之,用 C 语言我有种在雷区蹦迪的感觉。尽管你知道哪里有雷,但因为一直在雷区蹦跶,难免会不小心踩到一两颗地雷。每当这时候我就会怀念起 Rust 的好了。甚至连之前觉得会严重拖慢重构进度的生命周期语法也变得眉清目秀起来。怪不得 Gnome 社区的不少项目也在积极从 C 语言迁移到 GTK 的其他编程语言 Binding 上去。

然后是 GTK 使用 GObject 来实现面向对象编程的问题。

这虽然看起来像是 GObject 的问题,但其实本身还是 C 语言过于简洁导致的。GObject 虽然实现了一套 OOP 工具,也提供了一套非常强大且现代的功能,但由于不是语言原生支持的,这导致其在使用时显得非常啰嗦。如果你需要自定义 GTK 组件,那么你就不得不写上一大串的模板代码。如果你还需要为自定义组件添加自定义的 signal 和 property,那么恭喜你,仅仅是模板代码可能就接近百行了。或许最佳实践应该是尽量组合使用 GTK 的原生控件?不得而知。反正我看 libadwaita(一个 GTK 的组件库)的每个组件的代码实现都非常长。

最后还是 GTK 方面的问题。

不过这与其说是问题,倒不如说是 GTK 作为 2020 年发布的 GUI 框架渐渐比不上现代的 GUI 框架罢了。对于 UI 开发的范式,我记得大致经历了三个阶段:界面和逻辑写在一起,界面和逻辑分离,用代码描述界面。在第一眼看到 GTK 4 的 Template 功能时,我以为它是在第三阶段。然而用着用着我才发现它是在第二阶段。Template 主要还是用于描述 UI 结构,数据绑定还是需要在代码里才能手动进行绑定。习惯了 React 开发 UI 后,看到 Template 我一下联想到了 JSX,但实际上它却更像是 HTML。

虽然抱怨了 GTK 这么多用起来不太舒适的地方,但是不得不说我对用 C 来实现一套 GUI 框架还是充满敬意的。或许 GTK 有着很多问题,但不得不说 GTK 还是实现了它应该有的功能。

内存泄漏排查

虽然我自认为在开发过程中自己有小心管理内存了,不过为了以防万一,最好还是用相关排查工具分析一下。

在 AI 的推荐下我了解到了 Valgrind 这个工具。它拦截了程序对内存管理相关函数的调用,从而可以检测到代码里可能存在的内存泄漏。

分析结果如下,确定出现了内存泄漏的地方还不少。根据报告信息定位到了源代码,脑测了下,的确是代码写得有问题,部分分支会导致内存泄漏的情况发生。

居然能轻松发现内存泄漏的存在,只能说 Valgrind 这个工具让人感到非常惊喜。

顺带一提,Valgrind 分析内存泄漏需要用到你的程序所依赖的库的调试符号。而 Arch 系发行版仓库里的库文件是默认没有调试符号的,因此必须去 https://debuginfod.archlinux.org 下载调试符号。也正因为如此,如果你的网络不佳的话,那么你会经历一段非常漫长且痛苦的下载时光(不要问我是怎么知道的)。

C 与内存安全

前面我提到了用 C 语言有种在雷区蹦迪的感觉,的确如此,因为我非常担心遇到安全问题。

内存泄漏其实还不算太大的问题,因为只要不是泄漏得特别严重,对于 GUI 程序来说基本不会影响用户使用,大不了关闭程序重新打开得了。但是安全问题可不是这样。毕竟你也不希望只是打开一份文件,你的电脑就被莫名其妙地执行了一段恶意代码吧。

对于需要手动管理指针和内存的语言,像我这种经验不够丰富的开发者很容易就会写出存在安全漏洞的代码,毕竟缓冲区溢出和 Double Free 可是非常常见的漏洞。虽然使用 GLib 的安全函数能减少问题出现的概率,不过个人总感觉不是很能够安心下来。

又是怀念 Rust 好处的一天。

AI 使用体验

这次的项目开发中我频繁地用到了 AI。

虽然很少直接让 AI 来写代码,不过在学习 GTK 相关 API 的用法时我经常会询问 AI。这样的好处是减少了我花在查阅文档上的时间。此外,对于一些不是很懂的概念可以一直追问下去,对于官方文档上没有给出 Demo 的 API 可以直接让 AI 给出用法示例,写完代码后还可以让 AI 来 Review 代码,可以说是非常方便了。

不过目前 AI 也确实存在一些问题。可能是因为 GTK 相关的资料比较少,AI 的幻觉问题挺严重的,经常会编造不存在的 API。可能是需要手动设置 Prompt 优化吧,使用默认设置询问 AI 时,它给出的代码的代码风格有时不是很优雅。

目前我遇到的最严重的问题是,尝试让 AI 重构一段代码时,它的输出结果来悄无声息地带上了一段会导致 Double Free 的代码。

总之,AI 作为知识广度和深度都十分惊人的编程伙伴,对程序员的帮助是非常惊人的。在 AI 的帮助下学习新知识,比以往完全靠自学的模式的效率要高多了。

结语

这次絮絮叨叨地说了很多,大部分都是一些自己在学习和开发时的个人感想。

虽然说是项目的小结,但项目本身反而感觉没什么好说的。

下一篇的文章就不知道是什么时候了。