开始学习和使用 Rust

你如今的气质里,藏着你走过的路、读过的书、爱过的人,以及学习过的编程语言。

如果把编程类比作武侠或者修仙里面的打怪升级,那我在入行的头几年,一直在痴迷于各种各样的招式;那时候我学习和体验了各种各样的语言:C/C++/Java/Javascript/Kotlin/Python/Ruby/Scala/Clojure/Scheme/Erlang/Haskell,不过自从我学习了 Haskell 之后,这份列表就基本停止增长了;虽然后来也对 Elixer、Go 也有过简单的了解,但是也仅限于了解了。

一方面随着从业时间的增长,要解决的问题逐渐变成了系统问题或者业务问题;另一方面也是体会到,语言只是工具,招式再花里胡哨也是徒劳,修炼内功才是王道。

不过,在一次机缘巧合之下,我再一次接触到了 Rust。

那是在某大型 5V5 手游上个赛季更新的时候,我制作的游戏体验增强工具就用不了。当时我就想,与其用一些第三方工具找基址,不如直接自己整一个?如果再配合上我自制的内核 rootkit,那岂不是所向无敌?

一不做二不休,我就开始着手写一个内存修改器了。这种类型的工具,最出名的莫过于 CE,于是我就直接去看它的源码;这个项目是用古老的 Pascal 语言写的,我花了点时间了解语法就开始啃;啃着啃着就觉得浑身难受,这门语言语法其实很简单,但是它提供的抽象能力实在有限,语法又十分地啰嗦,写一行代码要吟唱半天;再加上其业务逻辑本身就比较复杂,文件动辄近万行,我估计除了作者本人,这玩意已经没人维护得动了,因此我果断放弃,开始寻找其他类似项目。在寻找的过程中,我发现了一个用 Rust 语言写的运行在 Windows 系统上的原型,其作者还详细地描述了其制作过程,于是我就又开始看起了它的 Rust 代码。

其实在几年前 Rust 发布 2018 Edition 的时候,Rust 有过一波小热潮,那时候我简单地了解过这门语言,不过没有继续深入下去。这个世界上每 30 天就会诞生一门新的编程语言,没什么好稀奇的;并且语言只是一门工具,是工具就要讲究其实用性,如果学会了一门手艺却无用武之地,那这项技能只会慢慢被遗忘。我曾花了很大的力气去学习 Haskell,但是现在我可能连一个 Hello World 也写不出来。

不过,在再一次接触 Rust 之后,我的看法有稍许改变。Rust 是一门充分吸收了现代语言优秀特性,同时提供了高阶的零开销抽象能力以及面向操作系统底层的接口,注重实用、性能,安全和编程体验的语言。它值得每一个已经拥有一定编程经验的工程师去深入学习。

实用性

对我来说,一门语言最重要的就是实用性。如果一门工具没有了实用性,那就失去了掌握它的意义。编程语言的使用者,绝大部分都是为了解决实际问题的。我们不是理论研究者,语言的完备性、设计美感甚至一致性都不是我们关注的重点。Haskell 的纯函数式,Ruby 的一切皆对象,在我看来都是非常不实用的特性;这些纯粹的东西,除了理论上的美感,带给我们的还有什么?与之相反,Java、PHP 和 Go 就是非常务实的语言,因此它们在工程师群体内大受欢迎并且广为流行。Rust 也是一门蕴含这种设计哲学的语言。Rust 语言的 Unsafe 经常被人吐槽,很多人嘲讽说「Rust 不是宣称安全性吗,那还提供 Unsafe 干什么?」实际上,现实世界本就是 Unsafe 的,一只蝴蝶扇动翅膀都可能引发一场飓风,一只臭虫都可能引发电路板故障;与 Unsafe 世界打交道使用 Unsafe 是很自然的事。有人会说,Haskell 就没有 Unsafe 呀?不,我不是 PLT 理论家,我不需要学习「自函子范畴上的幺半群」,给我一个 unsafe,我能干翻整个世界[滑稽]

性能

在很多场景下,性能并不是至关重要的因素;“过早优化是万恶之源”。但在某些场合,性能问题是 0 和 1 的问题。我们经常听到人们对 Java 和 C++ 性能的比较,很多人有一种错觉,在 Java 强大的 JIT 加持下,不说超越 C++,接近应该是没问题的;但很多场景并非如此。Android 平台上也使用 Java 系语言,其运行在专为移动设备设计的虚拟机 ART 上,在这种性能受限的场合,原生语言和托管语言之间有着不可逾越的鸿沟。太极 App 的核心现在是用 C++ 构建的(它曾经是 Java),在我从 Java 切换到 C++ 之后,其核心路径的性能最高提升了 10 倍有余!另外,epic 的早期实现使用 Java 提供的 dexmaker,切换到 C++ 之后,dex生成速度提升了将近 10 倍。这是什么概念呢?原本拦截一个函数需要 80 ~ 100ms,大型模块拦截几百个函数,你打开 App 就要黑几秒,现在只需要 6 ~ 15ms,同样的模块就只需要几百 ms,肉眼根本感知不到;这也是太极速度飞快的秘密。

Rust 也是一门注重性能的语言,咱们使用的很多命令行工具就是用 Rust 写的,它们都有超越同类工具的卓越性能,这是它性能最好的佐证;比如 fd, ripgrep, starship 等等。说到 startship,我曾经使用了 spaceship,我去那个龟速简直跟树懒一样;后来切换成了 powerlevel10k 体验好了很多,直到我切换到 starship,我的妈呀那叫一个 balzing fast!用起来简直就是一种享受。虽然我没有探究过 starship 快的原因,也可能是与语言无关的,但是我们可以知道,Rust 在性能方面,有着极高的上限,它完全可以胜任你对性能敏感的场合。

移动语义

右值和移动语义是 C++11 中最为重要的特性之一,可以说它深刻地改变了整个 C++。Rust 语言没有历史包袱,它默认就是移动语义,不需要你去考虑右值引用、引用折叠,完美转发这种问题,使用起来非常自然。

最近我接触到一个程序,它从一个 socket 读数据,然后解析处理,最后通过 websocket 异步发送到远端。程序是用 Java 写的,它遇到了频繁 GC 导致 stop the world 的性能问题。其原因实际上很简单,读取数据的时候要频繁分配数组导致 GC 繁忙;解决的办法也很简单,那就是建立一个对象池,这个数组在读到 socket 数据的时候分配,在 websocket 异步发送完毕之后就可以重复利用了。然而在没有这种默认语义的语言中,这个方案可能没有那么容易实施;比如说 websocket 你用的是第三方库,它异步处理的时候并没有给你回调,你无法知道数组何时应该 recycle。如果在 Rust 中,处理这种问题就非常容易。

所有权、生命周期和 RAII

大多数使用高级语言的程序员可能没有意识到,我们写代码本质上就是在与 CPU、内存和外设打交道。我们使用的很多语言并不直接提供对这些资源的访问方式,但是它并不意味着我们应该忽略这些资源。我们学习的任何一门技术,从上层到底层,自顶向下到最后,你一定会接触到 CPU、内存等底层资源。

很久以前我也是一个页面仔,觉得指针简直反人类,GC 就是 yyds,我曾无比期待 Kotlin 1.0 的发布,觉得它就是未来。然而,随着时间的推移,我发现解决很多问题不与底层打交道根本不够;我们使用的上层业务系统,各种框架和库,实际上是另一部份人对底层的封装;而我们对这些框架的使用,不过是遵循某些人制定的游戏规则;我们自己不能成为规则的制定者吗?你们所看到的太极,它需要通过汇编控制寄存器,需要精准地控制 GC 和 JIT,还需要小心翼翼地处理各种内存布局;而我们即将实现的内存修改器,它需要直接操作内核控制内存物理页面,这些东西哪一个不需要关心底层?

Rust 的所有权、生命周期对一些人可能比较新奇,但是,它本质上就是提供了一种对内存的控制方式,仅此而已。而这种访问方式,给了我们一种内存安全的可能,它需要我们编写程序的时候付出更多,但是这种 trade off 是有价值的。

后记

其实 Rust 还有很多东西值得一提,但是我这并不打算做一个 Rust 的全面介绍,感兴趣的话可以 Google 查阅相关资料7,8,9,10,11,12

如果你是一个有一定经验的程序员,已经能对若干门语言熟练使用,那么我强烈建议你去学习一下 Rust,它的很多优秀特性博采众长,再不济你也可以从这里了解到很多其他语言的精华。当然,如果你是一门新手,那么 Python 和 Java 可能更适合你。

传闻中 Rust 很难学,但我认为并非如此;我曾花费过很长的时间学习 Haskell,你甚至需要翻阅各种论文才能理解其设计,Rust 与其相比真的不算什么,顶多称得上是有门槛;然而,这个世界上没有门槛的事情大多没有核心竞争力。

另外,本文可能提及了一些其他的语言并且赞扬了一波 Rust,但是并没有 diss 和吹捧的意思;如果你是一个语言技术 fans,那么几乎毫无疑问你会成为一个糟糕的决策者;《失败的逻辑》忠告我们,不要为了工具而工具,忘记了工具的目的是什么。

最后,欢迎你成为 Rustaceans 的一员!