写给前端看的Rust教程(19)WebAssembly实战一

2,454 阅读4分钟

原文:24 days from node.js to Rust

前言

通过前面18篇教程,我们已经获取了设置开发环境的知识,我们学会了rustupVS Coderust-analyzer,我们已经走过了作为新手的迷茫期,现在要开始学习如何建立一个真正的项目

正文

建立workspace

建议你使用cargoworkspaceRustJavaScript都是逻辑功能模块化后会更好管理,我们现在就会开始一个可执行库,我们以一个library开始,以一个命令行程序来做封装,这种结构会更容易测试和拓展

首先,在一个空文件夹中创建一个workspace,在Cargo.toml中添加如下内容:

[workspace]
members = ["crates/*"]

members列出了workspace中所有的crate,这里配置的意思是包含了crates文件夹下的所有内容

建立library

我们用cargo new创建一个新的library

$ cargo new --lib crates/my-lib

binary cratelibrary crate之间的差异很小,默认情况下binary crate有一个main.rslibrary cratelib.rscargo new --lib命令还会将Cargo.lock添加到.gitignore

Cargo book 建议将Cargo.lock在产品端保留(二进制、服务器、微服务等),但是在library中省略

lib.rs的默认内容比较简单:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

在这里会看到关于单元测试的内容,Rust有内置的单元测试,不需要额外的配置单元测试框架,也不需要配置如何运行测试

这意味着单元测试就在你的源码里,Rust也有集成测试,就放在和src文件夹同级的tests文件夹里,但这只能测试公共接口,如果希望是私有代码,你只能在源码里写测试

单元测试

library模板引出了两个新的属性:#[cfg()]#[test]

[#cfg()]用于条件编译,在一个模块的前面指定#[cfg(test)]就意味着Rust会忽略这个模块的编译,除非有test标记

#[cfg(test)]
mod tests {
}

[#test]会将一个函数编辑成单元测试,当你运行cargo test命令的时候,Rust会将其一一执行

$ cargo test
running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests my-lib

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Rust提供的断言很基础,你可以通过assert!()assert_eq!()assert_ne!()来做判断

编写测试用例是确定API功能的很好开始,虽然严格的TDD有点极端,但在编写API之前设计好测试会强迫你思考API的实现。

WebAssembly现在比较火,让我们建立一个wasm runner,我们将lib.rs改成下面这个样子:

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn loads_wasm_file() {
        let result = Module::from_path("./tests/test.wasm");
        assert!(result.is_ok());
    }
}

添加use super::*让我们可以不需要前缀就能使用父模块的所有内容

现在模块结构还不存在,但是我们预估需要一个从本地加载文件的方法,由于加载可能失败,所以返回值是Result,尽管不知道不知道结果如何,但预期的结果肯定希望是Ok

我们可以运行cargo test,但是会遇到编译错误,这是因为有些功能还未实现

error[E0433]: failed to resolve: use of undeclared type `Module`
 --> crates/wasm-runner/src/lib.rs:6:22
  |
6 |         let result = Module::from_file("./tests/test.wasm");
  |                      ^^^^^^ use of undeclared type `Module`
For more information about this error, try `rustc --explain E0433`.

我们需要添加Module结构体以及from_file函数,我们在测试中给该函数传递了一个&str,但实现的时候我们希望参数可以是任何代表路径的东西

use std::path::Path;
struct Module {}

impl Module {
    fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, ???> {
      Ok(Self{})
    }
}

现在我们需要指出返回的错误类型,因为我们是从文件系统加载的,而这些方法会返回io::Error类型的错误,所以我们现在也可以这么设置

现在我们的代码已经可以运行了,虽然还没做任何有用的事情

use std::path::Path;
struct Module {}

impl Module {
    fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, std::io::Error> {
        Ok(Self {})
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn executes_wasm_file() {
        let result = Module::from_file("./tests/test.wasm");
        assert!(result.is_ok());
    }
}

创建命令行程序

使用cargo new crates/[your cli name]命令创建一个命令行程序,这里我们用到的是$ cargo new crates/cli,然后在Cargo.toml中添加依赖:

[dependencies]
my-lib = { path = "../my-lib" }

现在我们可以从my_lib命名空间导入代码了

Rust有一个规则,允许在crate名字中使用连字符,但不允许在Rust标识符中使用,所以如果你的crate名字用到了连字符,那么使用的时候以下划线代替

use my_lib::Module;

当输入上述代码的时候,VS Code会给出错误提示

image.png

这是因为Module没有明确指明是public的,所以我们不能引入它。现在我们给struct Modulefn from_file i添加pub

pub struct Module {}

impl Module {
    pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, std::io::Error> {
        Ok(Self {})
    }
}

现在我们可以在命令行程序中引入Module并且调用Module::from_file方法了

use my_lib::Module;

fn main() {
  match Module::from_file("./module.wasm") {
    Ok(_) => {
      println!("Module loaded");
    }
    Err(e) => {
      println!("Module failed to load: {}", e);
    }
  }
}

运行命令行程序

我们可以在./crates/cli文件夹里通过cargo run命令运行我们的命令行程序,不过cargo也可以在任何的子crate中庸-p标记运行命令,在我们的项目根路径上,我们通过cargo run -p cli命令去运行cli crate中默认的二进制文件

$ cargo run -p cli
Module loaded

虽然还有不少事情要做,但截止到目前我们已经打好了一个坚实的基础

相关阅读

总结

打下坚实的基础很重要,初学的时候你经常会不知所措,不晓得最佳实践是什么,这会动摇你的信心。但当你跨越了这些时刻,你会感到一切都是那么自然,甚至会忘记初学时的苦恼

更多