24 days from Node.js to Rust

preface

WebAssembly is an exciting technology right now, it’s a real development platform, and a lot of people think that JavaScript is going to become a universal compiled language, that you write once and run everywhere, We even have “Univeral JavaScript” and “Isomorphic JavaScript.

We’re close to that goal, but we’re a little off, mainly because JavaScript isn’t very CPU intensive. WebAssembly was created for the Web and can run anywhere as bytecode, and the cool thing is that browsers support running WebAssembly

The body of the

Create the WebAssembly module

It’s not difficult to compile high-level language code into WebAssembly, it’s difficult to choose what useful functionality to use it for. Since WebAssembly can only accept and return numbers, it may seem like WebAssembly is only good at math, but you can say the same about computers. A computer is essentially an automatic arithmetic machine, but it can do a lot of things

waPC

WaPC defines the WebAssembly Procedure Calls protocol, which is similar to a WebAssembly plug-in framework. In the waPC world, an implementer is the host and a WebAssembly module is the client. In Rust you can create a WebAssembly module

Here we have three articles on waPC, the second on building WebAssembly modules in Rust, and the third on how to run them in Node.js

  • Building WebAssembly platforms with waPC
  • Getting Started with waPC & WebAssembly
  • Building a waPC Host in Node.js

Test module

We pre-set up a test module that has an exposed interface hello that takes a string and returns a string

Set up the waPC host

We created the project in my_lib, we tested the Module::from_file function, and now we need to refine it.

Read the file

You can implement basic reads with STD ::fs::read and STD ::fs::read_to_string. Since we are loading binary data, we need to get a list of bytes of type Vec< U8 > with fs::read

pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, std::io::Error> { debug! ("Loading wasm file from {:? }", path.as_ref());
    letbytes = fs::read(path.as_ref())? ;// ...
}
Copy the code

Now that we have the bytes, what do we do with them? Our Module should have a constructor and a new method that accepts bytes. Let’s go ahead and change that

Crates/my – lib/SRC/lib. Rs:

pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, std::io::Error> { debug! ("Loading wasm file from {:? }", path.as_ref());
    letbytes = fs::read(path.as_ref())? ; Self::new(&bytes) }pub fn new(bytes: &[u8]) -> Result<SelfAnd????? > {// ...
}
Copy the code

Now we have the same problem as in tutorial 14. We know that new() returns a Result, but it can fail in a variety of ways, and we need a generic error to catch each type of case. It is time to create a custom error, we add thisError to the dependencies in the Cargo. Toml file of our my-lib project

[dependencies]
log = "0.4"
thiserror = "1.0"
Copy the code

Create an error. Rs file and create an error enumeration

Crates/my – lib/SRC/error. The rs:

use std::path::PathBuf;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Could not read file {0}: {1}")]
    FileNotReadable(PathBuf, String),}Copy the code

Instead of encapsulating IO ::Error, we added a custom message and multiple parameters to customize the Error message. To use this Error, first declare the Error module and then import Error:

pub mod error;
use error::Error;
Copy the code

Now we can change the Result type to return an Error and use.map_err() to map IO ::Error to Error

pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> { debug! ("Loading wasm file from {:? }", path.as_ref());
    letbytes = fs::read(path.as_ref()) .map_err(|e| Error::FileNotReadable(path.as_ref().to_path_buf(), e.to_string()))? ; Self::new(&bytes) }Copy the code

.map_err() is a common way to convert error types, especially and? Operators are combined. We didn’t add the implementation of From< IO ::Error> because we wanted more control over Error messages and needed to do the mapping manually

Read wapc, wasmtime – the provider

Wapc package provides WapcHost data structure and WebAssemblyEngineProvider trait, WebAssemblyEngineProvider allows us to easily exchange of multiple WebAssembly engines, We will use next wasmtime engine, but you can simply use wasm3 or for any new engines to achieve a new WebAssemblyEngineProvider

Add wAPC and WASmTime-Provider dependencies to Cargo. Toml:

crates/my-lib/Cargo.toml

[dependencies]
log = "0.4"
thiserror = "1.0"
wapc = "0.10.1"
wasmtime-provider = "0.0.7"
Copy the code

Before we can create WapcHost, we need to initialize the engine:

let engine = wasmtime_provider::WasmtimeEngineProvider::new(bytes, None);
Copy the code

The second parameter is our WASI configuration, which is ignored for now

The WapcHost constructor takes two parameters: the engine and the function to run when the WebAssembly client calls it. For now, we have no special needs but to return an error:

let host = WapcHost::new(Box::new(engine), |_id, binding, ns, operation, payload| { trace! ("Guest called: binding={}, namespace={}, operation={}, payload={:? }",
        binding,
        ns,
        operation,
        payload
    );
    Err("Not implemented".into())
})
Copy the code

The constructor returns a Result with a new error type, so we add a new error enumeration:

crates/my-lib/src/error.rs

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error(transparent)]
    WapcError(#[from] wapc::errors::Error),
    #[error("Could not read file {0}: {1}")]
    FileNotReadable(PathBuf, String),}Copy the code

We also need to store host in the Module structure:

pub struct Module {
    host: WapcHost,
}
Copy the code

The final code looks like this:

impl Module {
  pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> { debug! ("Loading wasm file from {:? }", path.as_ref());
      letbytes = fs::read(path.as_ref()) .map_err(|e| Error::FileNotReadable(path.as_ref().to_path_buf(), e.to_string()))? ; Self::new(&bytes) }pub fn new(bytes: &[u8]) -> Result<Self, Error> {
    let engine = wasmtime_provider::WasmtimeEngineProvider::new(bytes, None);

    let host = WapcHost::new(Box::new(engine), |_id, binding, ns, operation, payload| { trace! ("Guest called: binding={}, namespace={}, operation={}, payload={:? }",
            binding,
            ns,
            operation,
            payload
        );
        Err("Not implemented".into())
    })?;
    Ok(Module { host })
  }
}
Copy the code

Now that we have implemented from_file to read the file and instantiate a new Module, if we run cargo test we can see that the test passes:

$ cargo test
[snipped]
     Running unittests (target/debug/deps/my_lib-afb9e0792e0763e4)

running 1 test
test tests::loads_wasm_file ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in0.84 s [snipped]Copy the code

The next step is to run WASM

Call the WebAssembly method

Moving data in and out of WebAssembly is serialization and deserialization. WaPC doesn’t require a specific serialization format, and its default code generator uses MessagePack, which we’ll use later, but you’re free to change it

Add the RMP-serde dependency to Cargo. Toml:

[dev-dependencies]
rmp-serde = "0.15"
Copy the code
Design test

Our test module contains an exported function hello, which returns a string. If “World” is passed in, it returns “Hello, World.”

crates/my-lib/src/lib.rs

#[cfg(test)]
mod tests {
  / /... snipped
  #[test]
  fn runs_operation() - >Result<(), Error> {
      let module = Module::from_file("./tests/test.wasm")? ;let bytes = rmp_serde::to_vec("World").unwrap();
      let payload = module.run("hello", &bytes)? ;let unpacked: String = rmp_serde::decode::from_read_ref(&payload).unwrap();
      assert_eq!(unpacked, "Hello, World.");
      Ok(())}}Copy the code

Line by line analysis:

  • Line 6: Load our test module
  • Line 8: will"World"Encoded inMessagePackByte format
  • Line 9: Call.run()Method that takes two arguments
  • Line 10: Result returned by decoding
  • Line 11: Determine if the result is returned"Hello, World."

When you test, the above code will not compile because we did not implement.run(). To implement.run(), use the WapcHost created to call WebAssembly and return the result, using the.call() function:

pub fn run(&self, operation: &str, payload: &[u8]) -> Result<Vec<u8>, Error> { debug! ("Invoking {}", operation);
    let result = self.host.call(operation, payload)? ;Ok(result)
}
Copy the code

Now run the test again, and you should now pass

$ cargo test
[snipped]
     Running unittests (target/debug/deps/my_lib-afb9e0792e0763e4)

running 2 tests
test tests::runs_operation ... ok
test tests::loads_wasm_file ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.84s
[snipped]
Copy the code

Upgrade command line

Now that our library can load and run WebAssembly, and we need to export this capability to the command line, we need to add two additional new parameters to the CliOptions, one is the name of the action, and the other is the number of data transferred:

crates/cli/src/main.rs

struct CliOptions {
    /// The WebAssembly file to load.
    #[structopt(parse(from_os_str))]
    pub(crate) file_path: PathBuf,

    /// The operation to invoke in the WASM file.
    #[structopt()]
    pub(crate) operation: String./// The data to pass to the operation
    #[structopt()]
    pub(crate) data: String,}Copy the code

We didn’t handle the error well enough at first, so if the main() function failed, the error message we received was confusing and unprofessional. Let’s extract the business logic into a run() function that produces a Result, which we can test in main(). If we get an error result, we print it and exit with a non-zero value to indicate that the program failed

crates/cli/src/main.rs

fn main() { env_logger::init(); debug! ("Initialized logger");

    let options = CliOptions::from_args();

    match run(options) {
        Ok(output) => {
            println!("{}", output); info! ("Done");
        }
        Err(e) => { error! ("Module failed to load: {}", e);
            std::process::exit(1); }}; }fn run(options: CliOptions) -> anyhow::Result<String> {
  / /...
}
Copy the code

Note that we used anyhow. If you read Tutorial 14 carefully, you should remember the anyhow package. In addition to anyhow, we also need to add rMP-Serde to serialize the data before sending it to the WebAssembly

[dependencies]
my-lib = { path = ".. /my-lib" }
log = "0.4"
env_logger = "0.9"
structopt = "0.3"
rmp-serde = "0.15"
anyhow = "1.0"
Copy the code

The.run() function is like a test, except that the data comes from CliOptions instead of being hard-coded:

fn run(options: CliOptions) -> anyhow::Result<String> {
    letmodule = Module::from_file(&options.file_path)? ; info! ("Module loaded");

    letbytes = rmp_serde::to_vec(&options.data)? ;let result = module.run(&options.operation, &bytes)?;
    let unpacked: String= rmp_serde::from_read_ref(&result)? ;Ok(unpacked)
}
Copy the code

Now we can run our program from the command line

Cargo run -p cli -- crates/my-lib/tests/test.wasm hello"Potter"
[snipped]
Hello, Potter.
Copy the code

Now that we’ve implemented a nice program, but it can only pass and return strings, we can do better

reading

  • wapc.io
  • wapc crate
  • wasmtime
  • anyhow

conclusion

This tutorial is going to be a little long, so it’s not going to be easy to follow. WaPC is just one of a wide range of application scenarios for developers to take advantage of WebAssembly, but WASM-Bindgen is also well known. Whichever route you choose, you will inevitably need to carve your way through treacherous terrain, and WebAssembly is stable, but not yet mainstream, and best practices are far from being established

In the next tutorial, we will extend our command line program’s ability to receive JSON data so that it can handle any WASM module