我是一个中级的 Rust 程序员(绝对不是高级的!),现在我正在用 Rust 写一个分析器,到目前为止大概有 1300 行代码。2013 年我曾用 Rust 写了一个 400 行的“操作系统”(是一个简单的键盘驱动程序)。
尽管在 Rust 方面我没有太多的经验(使用 Rust 还不到10周),但我认为 Rust 已经能够让我做很多非常棒的事情!例如——我正在用 Rust 写的这个 Ruby 分析器,只需要有访问 PID、内存映射和从进程中读取内存的能力,就可以调用任意 Ruby 程序中的堆栈进行跟踪。它已经可以工作了!虽然距离发布第一个版本还有一些工作需要做,但是在我的笔记本里它在 35 个不同的 Ruby 版本都可以运行(从1.9.1到2.5.0)!即使 Ruby 程序的符合被剥离,也没有调试信息,但它确实是有效的!
对我来说,这真是太神奇了,如果没有 Rust 的话,我真的不认为能这么快就做到这一步。
对于一个不经常使用 Rust 的用户来说,编译器的改进是一件非常 Cool 的事情!我上一次使用 Rust 是在2016年的5月份(同一个 Ruby 分析器项目)。
在2016年,从我使用 Rust 的经验来说,它的编译器是非常难用的,在 RustConf talk in 2016 中,我说到:
我在很多时候对 Rust 的编译器感到沮丧,但我仍然喜欢它,因为它让我做了一些我可能不会做的事情。
现在我不用再忍受 Rust 的编译器了。并不是因为我对 Rust 更了解了(我还没有!),而是因为 Rust 的编译器更好用了。
这当然不是魔法,而是因为大量的工作在 Rust 的贡献者。在 Rust 的 2017 路线图中,他们宣布 2017 年要将重点放到生产力上,并表示:
对生产力的关注似乎与 Rust 的其他目标不一致。毕竟,Rust 关注的是可靠性和性能,很容易想象,实现这些目标会迫使其他地方妥协——就像学习曲线,或者开发者的工作效率。“Fighting the Borrow Checker” 是 Rustaceans 首次获得成功的必经之路?是否要移除故障和复杂的需要掩饰的安全漏洞和性能障碍?
和
我们用 Rust 的处理办法一直都是权衡再权衡,正如我们在这个博客上谈论过的各种支柱中所体现的:
我喜欢这种方式(“我们将使它使用起来更方便同时不影响可靠性和性能”), 我觉得他们真的付出了努力。
但是,当我在说器编译器时,我总是很小心的说“更容易”而不是“容易”——我想 Rust 在如何变的“容易”是有一些局限性的!当然,关于 Rust 的问题(像编译时的线程安全保障一样),从根本上来说,你需要仔细考虑你的程序到底在做什么。所以我不期望或者希望 Rust 能够像“ Python 一样容易”之类的。
为了展现 Rust 的编译器是如何的好:下面是我这一两天得到的关于编译器提示的错误消息的真实例子。我找到这些例子仅仅是通过往上翻阅我的终端内容。
这是第一个例子:
error[E0507]: cannot move out of borrowed content --> src/bin/ruby-stacktrace.rs:85:16 | 85 | if let &Err(x) = &version { | ^^^^^-^ | | | | | hint: to prevent move, use `ref x` or `ref mut x` | cannot move out of borrowed content
这个错误提示是非常有用的!!我按照提示操作:我将 ref x 用 x 替代,然后我的程序编译通过!!现在这种情况是经常发生的——我只是按照编译器告诉我的方式去做,然后正常运行!
下面是另一个简单的错误消息提示的例子:我无意中忘了给 Err() 传参数。它确切指出了出现问题的代码我觉得这点非常好。
error[E0061]: this function takes 1 parameter but 0 parameters were supplied --> src/bin/ruby-stacktrace.rs:154:25 | 154 | if trace == Err() { | ^^^^^ expected 1 parameter
最后一个非常棒的例子:我忘了输入 Error 正确的类型。Rust 给出了4种我可能会在这使用的非常有用的类型!(我想用的 failure::Errror 也在列)。
error[E0412]: cannot find type `Error` in this scope --> src/lib.rs:792:84 | 792 | ) -> Result<Box<Fn(u64, pid_t) -> Result<Vec<String>, copy::MemoryCopyError>>, Error> { | ^^^^^ not found in this scope help: possible candidates are found in other modules, you can import them into scope | 739 | use failure::Error; | 739 | use std::error::Error; | 739 | use std::fmt::Error; | 739 | use std::io::Error;
当然,有些时候,Rust 并不能呈现出我想要的结果。例如: ruby_stacktrace::address_finder::AddressFinderError 这个类型,是表达 Error 的原因。所以当 Error 出现时,我应该能够返回一个 AddressFinderError ,对吧?错!
先不抱怨 Rust :
Compiling ruby-stacktrace v0.1.1 (file:///home/bork/work/ruby-stacktrace) error[E0308]: mismatched types --> src/bin/ruby-stacktrace.rs:86:20 | 86 | return version; | ^^^^^^^ expected struct `failure::Error`, found enum `ruby_stacktrace::address_finder::AddressFinderError` | = note: expected type `std::result::Result<_, failure::Error>` found type `std::result::Result<_, ruby_stacktrace::address_finder::AddressFinderError>`
我知道怎么样处理这个问题:我可以通过返回 OK 来绕过这个问题(版本?)并且我的代码可以编译通过。但是编译器不会告诉我怎么样去解决这个问题,而且不能给我任何解决这个问题的清晰的线索。
但是!!!基本上每一次我都会遇到这样一个让人恼怒的问题,我问过 Kamal 这个问题(他用 Rust 的时间比我长),他说“是的,Rust 确实有一个 RFC ,人们正在积极的解决这个问题!”
下面2个具体的让人恼怒的例子已经是公认的 RFCs (这意味着这些问题正在解决中!):
一个让人恼怒的事情是,有时你需要在你的代码中插入括号来进行编译。还有一个被接受的 RFC 称为非词汇生命周期,基本上使变量生命周期变得更加智能化!
当我使用引用时(经常使用!!)我经常遇到一种状况,编译器告诉我需要在某处添加或删除一个符号(类似于我给出的第一个编译器错误消息提示)。在引用上更好的人机工程匹配模式这个公认的 RFC 让使用引用更加便捷而且不损耗任何的可靠性和性能!这很棒!竞争工程学特性已经出现在 Rust 的 nightly 构建版本中!
Rust 继续投入大量的时间去研究像这样的人类工程学的问题,这让我感到非常高兴。这都是个别比较小的让人恼怒的问题,但是,当大量的问题被修复时,我认为这将会对使用 Rust 的体验上带来巨大的正面效果。
我喜欢 Rust 的另一个原因是它能够用一些简单的方法来避免一些复杂的操作。例如!!在我的程序里有一个 get_bss_section 的方法。这个方法是非常简单的——它只是遍历了 ELF 文件的二进制部分并返回这部分名为 .bss 的头。
pub fn get_bss_section(elf_file: &elf::File) -> Option<elf::types::SectionHeader> { for s in &elf_file.sections { match s.shdr.name.as_ref() { ".bss" => { return Some(s.shdr.clone()); } _ => {} } } None } }
在编译器中我遇到了一大堆有关权限的问题,我真的不知道要怎么解决。所以我做了一个简单的权衡!我只用了 clone()来拷贝内存,这样问题就解决了。现在我可以将注意力放在实际的程序逻辑中!
我认为在开始使用 Rush 时,用这种方法(会让程序更容易编写并牺牲一点性能)去权衡是非常成功的。我最喜欢的是这个独特的权衡是显式的。我可以在程序里查找到每一个使用了 .clone() 地方并且检查它们 —— 函数被调用的太多了吗?我是否应该担心?当我检查到代码中使用了 .clone() 的地方在程序起始处的函数中,并且只被调用了一次或两次。我可能需要以后对它们进行优化。
在我的程序中,我解析了 ELF 二进制文件。事实证明,有一个 crate 可以做到这一点:elf crate!
现在我正在使用 elf crate 来完成这类事情。但也存在 goblin crate ,它支持 Linux(ELF)、Mac(Mach-o)和 Windows(PE32)的二进制格式! 我可能会在某个时候切换到 goblin 。这些库的存在,以及他们是有据可查、易于使用,令我喜爱有加!
另一个我喜爱的 Rust crate(通常意义上的 Rust )是 —— 我觉得它们通常不会在它们所暴露的概念的基础上增加不必要的抽象。elf crate 里的结构就像 —— Symbol,Section,SectionHeader,ProgramHeader …… 等 ELF 文件中的概念!
当我发现一件我从来没有听说过的奇怪的事情,我需要使用它时(在程序标题 vaddrfield ),它就在那里! 它被称为 vaddr ,在 C 结构中也被这样叫。
Cargo 是 Rust 的包管理器和构建工具,它非常棒。我认为它是相当值得被大家知道的。这一点我感受颇深,因为我最近一直在使用 Go —— Go 有很多我喜欢的东西,但 Go 的软件包管理真的非常痛苦,而 Cargo 是很容易使用的。
我在 Cargo.toml 文件中的依赖关系看起来像这样。很简单!
[dependencies] libc = "0.2.15" clap = "2" elf = "0.0.10" read-process-memory = "0.1.0" failure = "0.1.1" ruby-bindings = { path = "ruby-bindings" } # internal crate inside my repo
在 Rust 中,我可以控制程序的每一个方面 - 我可决定究竟是选择什么系统调用,分配哪类内存,让程序休眠多少微秒等等一切东西。我觉得可以在 C 中做的任何事情,都可以在 Rust 中做到。
我真的很喜欢这样。对于大多数编程任务来说 Rust 并不是我的首选语言(如果我想编写一个 Web 服务,从个人角度我可能不会使用 Rust 。如果你对 Rust 的 Web 服务感兴趣的话,看看 are we web yet 一文)。我觉得 Rust 就像我的超级英雄语言! 如果我想做一些奇怪的系统魔法级的东西,我知道在 Rust 中将是可行的。也许并不容易,但绝对可行!
我已经写了关于这些的博客文章,但在此我想再谈谈!
我使用 bindgen 为每个我需要引用的 Ruby 结构(跨越 35 个不同的 Ruby 版本)生成 Rust 结构定义。这有点魔幻吗?就像我刚刚指向的一些内部 Ruby 头文件(从我的本地克隆的 Ruby 源代码)中那些我想提取的结构定义,告诉它我感兴趣的 8 个结构体,然后它完成任务了。
我认为 bindgen 可以让你和用 C 语言编写的代码很好地互操作,这真是不可思议。
然后我使用了宏(参见:我的第一个 rust 宏),并写了一堆代码引用这 35 个不同的结构体版本,以确保我的代码可以在所有这些版本中正常工作。
而当我引入一个新的内部 API 变化的 Ruby 版本(如2.5.0)时,编译器会说:“嘿,你的旧代码可以在 Ruby 2.4 中的结构体上工作,但现在不能编译了,你必须处理这个问题。”
评论删除后,数据将无法恢复
评论(7)
引用来自“dwing0”的评论
"Rust 适合那些想用 C/C++ 进行编程但发现这些语言太难学的人。" 😂挺形象的,最大的障碍