Rust是一种编程语言,2010年起源于Mozilla研究院。今天,所有的大公司都在使用它。
亚马逊和微软都认可它是他们系统中C/C++的最佳替代品。但Rust并没有止步于此。像Figma和Discord这样的公司现在也在他们的客户端应用程序中使用Rust,从而引领了潮流。
本篇Rust教程旨在简要介绍Rust,如何在浏览器中使用它,以及何时应该考虑使用它。我将首先比较Rust和JavaScript,然后引导你完成在浏览器中启动和运行Rust的步骤。最后,我将介绍我的COVID模拟器网络应用的快速性能评估,该应用使用了Rust和JavaScript。
Rust的简述
Rust在概念上与JavaScript非常不同。但也有一些相似之处需要指出。让我们来看看这枚硬币的两面。
相同点
两种语言都有一个现代的包管理系统。JavaScript有npm,Rust有Cargo。Rust有package.json ,而不是Cargo.toml ,用于依赖性管理。要创建一个新项目,使用cargo init ,要运行它,使用cargo run 。不算太陌生,是吗?
Rust中有许多很酷的功能,你已经从JavaScript中知道了,只是语法略有不同。以这个常见的JavaScript模式为例,对一个数组中的每个元素都应用一个闭包。
let staff = [
{name: "George", money: 0},
{name: "Lea", money: 500000},
];
let salary = 1000;
staff.forEach( (employee) => { employee.money += salary; } );
在Rust中,我们会这样写。
let salary = 1000;
staff.iter_mut().for_each(
|employee| { employee.money += salary; }
);
诚然,我们需要时间来适应这种语法,用管道(|)代替小括号。
但在克服了最初的尴尬之后,我发现它比另一组小括号更清晰易读。
作为另一个例子,这里有一个JavaScript中的对象析构。
let point = { x: 5, y: 10 };
let {x,y} = point;
在Rust中也是如此。
let point = Point { x: 5, y: 10 };
let Point { x, y } = point;
主要区别在于,在Rust中我们必须指定类型(Point)。更广泛地说,Rust需要在编译时知道所有的类型。但与其他大多数编译语言不同的是,编译器在可能的情况下会自己推断类型。
为了进一步解释这一点,下面是在C++和许多其他语言中有效的代码。每个变量都需要一个明确的类型声明。
int a = 5;
float b = 0.5;
float c = 1.5 * a;
在JavaScript中,以及在Rust中,这段代码是有效的。
let a = 5;
let b = 0.5;
let c = 1.5 * a;
共享功能的清单不胜枚举。
- Rust有
async+await语法。 - 创建数组可以像
let array = [1,2,3]那样简单。 - 代码被组织在具有明确导入和导出的模块中。
- 字符串是用Unicode编码的,处理特殊字符没有问题。
我可以继续列举,但我想我的观点现在已经很清楚了。Rust有一系列丰富的功能,这些功能在现代JavaScript中也有使用。
差异
Rust是一种编译语言,也就是说,没有执行Rust代码的运行时。一个应用程序只有在编译器(rustc)完成了它的魔法之后才能运行。这种方法的好处通常是性能更好。
幸运的是,Cargo为我们处理了调用编译器的问题。而且有了webpack,我们还能把cargo 隐藏在npm run build 。有了这个指南,一旦为项目设置了Rust,就可以保留网络开发者的正常工作流程。
Rust是一种强类型语言,这意味着所有类型在编译时必须匹配。例如,你不能用错误类型的参数或错误数量的参数来调用一个函数。编译器会在你运行时遇到错误之前为你捕捉到这个错误。明显的比较是TypeScript。如果你喜欢TypeScript,那么你很可能会喜欢Rust。
但不要担心:如果你不喜欢TypeScript,Rust可能仍然适合你。Rust是近年来从头开始构建的,考虑到了人类在过去几十年中对编程语言设计的所有认识。其结果是一种令人耳目一新的简洁语言。
Rust中的模式匹配是我最喜欢的一个功能。其他语言有switch 和case ,以避免像这样的长链。
if ( x == 1) {
// ...
} else if ( x == 2 ) {
// ...
}
else if ( x == 3 || x == 4 ) {
// ...
} // ...
Rust使用更优雅的match ,像这样工作。
match x {
1 => { /* Do something if x == 1 */},
2 => { /* Do something if x == 2 */},
3 | 4 => { /* Do something if x == 3 || x == 4 */},
5...10 => { /* Do something if x >= 5 && x <= 10 */},
_ => { /* Catch all other cases */ }
}
我认为这很好,我希望JavaScript开发者也能欣赏这种语法扩展。
不幸的是,我们也不得不谈一谈Rust的黑暗面。直截了当地说,使用严格的类型系统有时会感觉非常麻烦。如果你认为C++或Java的类型系统很严格,那么请准备好接受Rust的艰难旅程。
就个人而言,我喜欢Rust的这一部分。我依赖于类型系统的严格性,因此可以关闭我大脑的一部分--这一部分在我每次发现自己在写JavaScript时都会剧烈地刺痛。但我明白,对于初学者来说,一直和编译器作斗争是非常烦人的。在这个Rust教程的后面,我们会看到一些这样的情况。
你好,Rust
现在,让我们用Rust在浏览器中运行一个hello world 。我们首先要确保所有必要的工具都已安装。
工具
- 使用rustup安装Cargo + rustc。Rustup是安装Rust的推荐方式。它将为最新的稳定版本的Rust安装编译器(rustc)和软件包管理器(Cargo)。它也可以管理测试版和夜间版,但对这个例子来说没有必要。
- 在终端键入
cargo --version,检查安装情况。你应该看到类似cargo 1.48.0 (65cbdd2dc 2020-10-14)的东西。 - 同时检查Rustup:
rustup --version应该产生rustup 1.23.0 (00924c9ba 2020-11-27)。
- 在终端键入
- 安装wasm-pack。这是为了将编译器与npm集成。
- 通过输入
wasm-pack --version来检查安装情况,你应该看到类似wasm-pack 0.9.1的东西。
- 通过输入
- 我们还需要Node和npm。我们有一篇完整的文章,解释了安装这两者的最佳方法。
编写Rust代码
现在一切都安装好了,让我们来创建项目。最终的代码也可以在这个GitHub仓库里找到。我们以一个可以编译成npm包的Rust项目开始。导入该包的JavaScript代码将在之后出现。
要创建一个名为hello-world 的Rust项目,使用cargo init --lib hello-world 。这将创建一个新的目录并生成Rust库所需的所有文件。
├──hello-world
├── Cargo.toml
├── src
├── lib.rs
Rust代码将被放在lib.rs 。在这之前,我们必须调整Cargo.toml 。它使用TOML定义了依赖关系和其他软件包信息。对于浏览器中的hello world,在你的Cargo.toml 中的某个地方添加以下几行(例如,在文件的末尾)。
[lib]
crate-type = ["cdylib"]
这告诉编译器以C兼容模式创建一个库。在我们的例子中,我们显然没有使用C语言。C-兼容只是意味着不是Rust专用的,这就是我们需要使用JavaScript的库的原因。
我们还需要两个外部库。将它们作为单独的行添加到依赖项部分。
[dependencies]
wasm-bindgen = "0.2.68"
web-sys = {version = "0.3.45", features = ["console"]}
这些是来自crates.io的依赖,crates.io是Cargo使用的默认包库。
wasm-bindgen是创建一个入口点的必要条件,我们以后可以从JavaScript中调用。(你可以在这里找到完整的文档。)值"0.2.68" ,指定版本。
web-sys包含Rust与所有Web API的绑定。它将使我们能够访问浏览器的控制台。注意,我们必须明确选择控制台功能。我们最终的二进制文件将只包含像这样选择的Web API绑定。
接下来是实际的代码,在lib.rs 。自动生成的单元测试可以被删除。只要用这段代码替换该文件的内容即可。
use wasm_bindgen::prelude::*;
use web_sys::console;
#[wasm_bindgen]
pub fn hello_world() {
console::log_1("Hello world");
}
顶部的use 语句是用来从其他模块导入项目的。(这类似于JavaScript中的import )。
pub fn hello_world() { ... } 声明了一个函数。 修饰符是 "public "的缩写,作用类似于JavaScript中的 。注释 是Rust编译到pub export #[wasm_bindgen] WebAssembly(Wasm)时特有的。我们在这里需要它来确保编译器向JavaScript暴露一个包装函数。
在函数的主体中,"Hello world "被打印到控制台。在Rust中,console::log_1() 是一个调用console.log() 的包装器。(在这里阅读更多内容。)
你是否注意到函数调用处的后缀_1 ?这是因为JavaScript允许可变数量的参数,而Rust不允许。为了解决这个问题,wasm_bindgen ,为每个参数的数量生成一个函数。是的,这可能很快就会变得很难看!但它是有效的。但它是有效的。在Rust中可以在控制台调用的函数的完整列表可以在web-sys文档中找到。
我们现在应该已经准备好了一切,。试着用下面的命令来编译它。这将下载所有的依赖项并编译该项目。第一次可能要花点时间。
cd hello-world
wasm-pack build
Huh!Rust编译器对我们并不满意。
error[E0308]: mismatched types
--> src\lib.rs:6:20
|
6 | console::log_1("Hello world");
| ^^^^^^^^^^^^^ expected struct `JsValue`, found `str`
|
= note: expected reference `&JsValue`
found reference `&'static str
注意:如果你看到一个不同的错误(error: linking with cc failed: exit code: 1),而你是在Linux上,你缺乏交叉编译的依赖。sudo apt install gcc-multilib 应该可以解决这个问题。
正如我前面提到的,编译器很严格。当它期望一个对JsValue 的引用作为一个函数的参数时,它不会接受一个静态字符串。为了满足编译器的要求,有必要进行明确的转换。
console::log_1(&"Hello world".into());
into()方法将把一个值转换为另一个值。Rust编译器很聪明,它可以推迟转换中涉及的类型,因为函数签名只留下一种可能性。在这种情况下,它将转换为JsValue ,这是一个由JavaScript管理的值的包装类型。然后,我们还必须添加& ,以通过引用而不是通过值传递,否则编译器又会抱怨。
再试着运行wasm-pack build 。如果一切顺利的话,最后打印出来的一行应该是这样的。
[INFO]: :-) Your wasm pkg is ready to publish at /home/username/intro-to-rust/hello-world/pkg.
如果你设法走到这一步,你现在就能够手动编译Rust了。接下来,我们将把它与npm和webpack整合起来,它们将自动为我们完成这项工作。