A language that empowers everyone to build reliable and efficient software. – Rust

Why learn Rust

Because we need the right tools to solve the right problems

Rust currently has the best support for WebAssembly. For front-end development, it is possible to rewrite CPU-intensive JavaScript logic in Rust and run it with WebAssembly. The combination of JavaScript and Rust gives you the power to drive everything.

But Rust is known to be a difficult language to learn, with a steep learning curve. I can’t learn anymore

For the front end, there are more mental shifts to go through than for any other language. Switching from imperative to functional programming languages, from mutability to immutability, from weakly typed to strongly typed languages, And from manual or automatic memory management to memory management through the life cycle, the difficulty increases.

But as we move past these mental shifts, Rust does have some advantages:

  • At the core, it reshapes our understanding of some fundamental concepts. For example, Rust clearly defines the lifetime of variables in a scope, allowing developers to eliminate manual memory management without having to worry about garbage collection (GC), allowing for both memory security and high performance.

  • From the outside, it feels like a high-level language like Python/TypeScript, with superb presentation power but no less performance than C/C++, allowing for both presentation power and high performance.

  • With friendly compiler and clear clear error prompts and complete documentation, basic can be done as long as the compilation passes, can be online.

With that in mind, let’s start with a few simple Rust demos

PS: This article does not give you a direct grasp of Rust or the introduction to it. It does not cover much API explanation. If you need it, you can refer to the references at the end of this article.

At the beginning of Rust experience

Use Rust Playground for a quick experience

Hello World

// The main() function is indispensable in a standalone executable and is the entry point to the program
fn main() {
    // Create String literals of type String. Default values created using let are immutable
    let target = String::from("rust");
    // println! () is a macro that prints arguments to STDOUT
    println!("Hello World: {}", target);
}
Copy the code

Example of function abstraction

fn apply(value: i32, f: fn(i32) - >i32) - >i32 {
    f(value)
}

// Input parameter and return type i32 (signed numeric type in the range [-2^31, 2^ 31-1])
fn square(value: i32) - >i32 {
    // Do not write; Return value * value;
    value * value
}

fn cube(value: i32) - >i32 {
    value * value * value
}

fn main() {
    Log (' apply square: ${apply(2, square)} ')
    println!("apply square: {}", apply(2, square));
    println!("apply cube: {}", apply(2, cube));
}
Copy the code

Control flow and enumeration

// All four coins are of type Coin
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
// Use match for type matching
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,}}Copy the code

Let’s talk about stacks and heaps

When we write JS, it seems that we do not need to pay special attention to heap and stack and memory allocation, JS will help us “automatic” everything. But this “automatic” is the root of all this confusion, giving us the false sense that we can afford not to care about memory management.

Let’s revisit the basics and how Rust handles memory management.

Stack space

The stack is characterized by “LIFO, last in, first out”. Data can only be stored one by one from the top and taken out one by one from the top. For example, in a table tennis box, the table tennis can only be put in (on the stack) after (out of the stack).

Each time a function is called, a stack frame is created at the top of the stack to hold context data for that function. For example, local variables declared inside the function are usually stored in the stack frame. When the function returns, the value returned by the function also remains in the stack frame. When the function caller retrieves the return value from the stack frame, the stack frame is released.

Heap space

Instead of stack space being tracked by the operating system, the heap is characterized by unordered key-value pair storage.

The heap claims a certain size of memory while the program is running, not when it is compiled. That is, memory is allocated dynamically, and access to it is no different from access to ordinary memory. For the heap, we can add and remove variables as much as we want, regardless of the order.

It can be summed up like this:

  1. The stack is suitable for storing data with a short lifetime.
  2. To store data on a stack, the size of the data type to which the data belongs is known.
  3. Using the stack is more efficient than using the heap.

For a value to be put on the stack, its size needs to be determined at compile time. Variables stored on the stack have a life cycle that is within the scope of the current call stack and cannot be referenced across the call stack.

The heap can store data types of unknown size or dynamic scaling. Variables stored on the heap have a life cycle that starts when they are allocated and ends when they are released, so variables on the heap can be referenced between multiple call stacks.

Stack can be understood as putting items into cartons of appropriate size and putting cartons into storage rooms according to rules, while stack can be understood as finding an empty place to place items in storage rooms. Obviously, the efficiency of accessing items in cartons is much higher, while the efficiency of directly putting items into messy storage is much lower, and the more randomly stacked things in storage, the more fragmented the free position, the lower the efficiency of accessing items, and the lower the space utilization rate.

How does Rust use the heap and stack

So let’s start by looking at how JavaScript uses stacks and heaps.

Memory in JavaScript is also divided into stack memory and heap memory. In general:

  • The stack memory holds the address of the storage object;
  • The heap memory holds the concrete contents of the storage object.
  • For primitive values, the address and contents are stored in stack memory.
  • Based on the reference type, the address of the value is stored in stack memory, and its specific content is stored in heap memory.

The various types of values in Rust are stored on the stack by default, unless they are explicitly placed on the heap using Box::new(). For dynamically sized types (such as Vec, String), the data portion is distributed in the heap, leaving a fat pointer in the stack to point to the actual data, and that fat pointer structure in the stack is statically sized.

In the use of heap and stack, the languages look similar, with the main difference being GC.

In JavaScript’s GC, without the garbage collector that some high-level languages have, it is difficult to determine whether JS automatically finds some memory that is “no longer needed.” Therefore, JS garbage collection implementation can only be limited to solve the general problem.

For example, the mark-sweep algorithm used for reference garbage collection still has the problem that objects that cannot be queried from the root object will be cleared.

Unlike other high-level languages, Rust does not provide GC and does not require manual allocation and release of heap memory, but Rust is memory-safe. One reason for this is that Rust uses its own set of memory-management mechanisms: all braces in Rust are a separate scope, and variables in the scope are invalidated when they leave the scope, while data bound to the variables (whether in the heap or on the stack) is automatically released.

fn main() {
    // Each curly bracket is an independent scope
    {
        let n = 33;
        println!("{}", n);
    }
    // The variable n is invalid at this time
    // println! ("{}", n); // Error compiling
}
Copy the code

So what if this happens:

fn main() {
    let v = vec![1.2.3];
    println!("{}", v[0]);
}
Copy the code

The v variable itself is allocated on the stack, with a fat pointer pointing to the three elements of V in the heap. When the function exits, the scope of v ends and the elements in the heap referenced by it are automatically reclaimed, which sounds good.

But the question is, what happens if you want to bind the value of v to another variable, v2?

For systems with GC, this is not a problem; both V and V2 refer to references in the same heap, which are eventually collected by GC.

For Rust, which does not have a GC, there is a natural solution, which is the move semantics of the ownership property, which we will cover later.

Features of Rust

The existence of ownership and lifecycle makes Rust a memory-safe, gC-free and efficient language.

Ownership: Control of the value of life and death

A computer’s memory resources are very valuable, and all programs need some way to make use of the memory resources of the computer.

language Memory Usage Scheme
Java, Go Garbage collection mechanism, constantly check whether some memory is not in use, if no longer needed to release it, occupying more memory and CPU resources
C, C + + Programmers manually apply for and release memory, easy to make mistakes and difficult to troubleshoot
JavaScript Memory is allocated automatically when variables (objects, strings, etc.) are created and “automatically” freed when they are not used. This “automation” is a source of confusion and gives JavaScript (and other high-level languages) developers the false sense that they can afford not to care about memory management.
Rust Ownership mechanism. Memory is managed by the ownership system according to a set of rules that are checked only during program compilation

Rust is popular and popular because it can keep memory safe without using garbage collection. Other languages, such as JavaScript and Go, use garbage collection for memory management. The garbage collector is convenient for developers at the cost of resources and performance, but it is difficult to detect problems once encountered. In the rust world, memory security can be achieved without garbage collection when you strictly follow the rules.

Let’s explore the implications of Rust design ownership and lifecycle by starting with the behavior of a variable using a stack

What happens to a variable when the function is called

Let’s start with a piece of code:

fn main() {
  // Define a dynamic array
  let data = vec![10.42.9.8];
  let v = 42;
  // Use if let for pattern matching.
  // The difference between this and the direct if judgment is that if matches booleans and if let matches patterns
  if let Some(pos) = find_pos(data, v) {
      println!("Found {} at {}", v, pos); }}// If v exists in data, returns the subscript of v in data, and returns None if it does not
fn find_pos(data: Vec<u32>, v: u32) - >Option<usize> {
  for (pos, item) in data.iter().enumerate() {
      // Unreference item to access the specific value of item
      if *item == v {
          return Some(pos); }}None
}
Copy the code

Option, Rust’s system type, is an enumeration containing Some and None to indicate the possibility that a value does not exist. This is a good programming practice that forces Rust to detect and handle cases where the value does not exist.

This code is not hard to understand, but it should be emphasized again that the dynamic array is placed on the heap because its size is not determined at compile time, and there is a fat pointer on the stack that points to memory on the heap.

When find_pos() is called, the local variables data and v from main() are passed to find_pos() as arguments, so they are placed in the arguments area of find_pos().

As most programming languages do, memory on the heap now has two references. Not only that, but every time we pass data as a parameter, memory on the heap gets referenced one more time.

But what these references actually do is unknown, and there is no limit; And exactly when memory on the heap can be freed, especially in the case of multiple stack references, is difficult to know, depending on when the last reference ends. So, such a seemingly simple function call causes a lot of trouble for memory management.

Ownership and Move semantics

Under Rust’s ownership rules, these issues are no longer an issue, and the ownership rules can be summarized as follows:

  • A value can only be owned by one variable, which is called the owner.
  • A value can have only one owner at a time.
  • When the owner leaves scope, its own values are discarded and memory is freed.

Under the rules of ownership, let’s look at how the above reference problem is resolved:

Data in main() is invalidated when moved to find_pos(), and the compiler ensures that subsequent code in main() cannot access the variable, thus ensuring that the memory on the heap still has only unique references.

But why doesn’t v get moved and it gets copied anyway? You’ll see in a minute.

So under the ownership rule, it solves the life-or-death problem of who really owns the value, eliminating multiple references to the data on the heap, which is its biggest advantage.

The biggest one is the complexity of the code, especially for simple data stored on the stack. If you want to avoid being unable to access the data after the ownership transfer, you need to call Clone (), which is not very efficient.

Rust takes this into account and provides Copy semantics in addition to Move semantics. If a data structure implements the Copy trait, it uses the Copy semantics. This way, when you assign a value or pass a parameter, the value is automatically copied bitwise (shallow copy).

#[derive(Debug)]
struct Foo;

let x = Foo;
let y = x; // unused variable: `y`

// `x` has moved into `y`, and so cannot be used
println!("{:? }", x); // error: use of moved value
Copy the code
#[derive(Debug, Copy, Clone)]
struct Foo;

let x = Foo;
// A variable name preceded by _ indicates that the variable is in the toDO state, and the compiler ignores the unused check
let _y = x;

// `y` is a copy of `x`
println!("{:? }", x); // A-OK!
Copy the code

Struct: You can think of a class in ES6, but it is recommended to think of structs as pure data.

Traits: Similar to interfaces, features and interfaces have in common in that they are a behavior specification that identifies which classes have which methods.

#[derive] : Derive. The compiler can add some basic implementations to traits from derive, such as

  • Copy: Makes types have “Copy semantics” instead of “move semantics”.
  • Clone: You can explicitly create a deep Copy of a value. When using a derivative of Copy, you usually add Clone because Clone is a superset of Copy.
  • DebugUse:{:? }The current value can be printed completely.

Going back to the v argument, since V is of type U32 and implements the Copy trait and is allocated on the stack, the call to find_pos automatically copies a Copy of v’.

But if we don’t want to use copy semantics to avoid too much memory being copied, we can use “borrow” data.

The value of the use

Let’s look at a new example:

fn main() {
    let data = vec![1.2.3.4];
    let data1 = data;
    println!("sum of data1: {}", sum(data1));
    println!("data1: {:? }", data1); // error1
    println!("sum of data: {}", sum(data)); // error2
}

fn sum(data: Vec<u32- > >)u32 {
    // Create an iterator. The fold method is used similarly to reduce
    data.iter().fold(0, |acc, x| acc + x)
}
Copy the code

It is clear that the code will not compile. Data and data1 are moved away from the assignment statement and sum, so calling them later will result in an error.

The compiler was smart enough to tell us where the error was:

error[E0382]: borrow of moved value: `data1`
 --> src/main.rs:5:29
  |
3 |     let data1 = data;
  |         ----- move occurs because `data1` has type `Vec<u32>`, which does not implement the `Copy` trait
4 |     println! ("sum of data1: {}", sum(data1));
  |                                      ----- value moved here
5 |     println!("data1: {:? }", data1); // error1
  |                             ^^^^^ value borrowed here after move

error[E0382]: use of moved value: `data`
 --> src/main.rs:6:37
  |
2 |     let data = vec![1.2.3.4];
  |         ---- move occurs because `data` has type `Vec<u32>`, which does not implement the `Copy` trait
3 |     let data1 = data;
  |                 ---- value moved here
...
6 |     println!("sum of data: {}", sum(data)); // error2
  |                                     ^^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` due to 2 previous errors
Copy the code

But we just need to change it so that it can compile without using copy:

fn main() {
    let data = vec![1.2.3.4];
    let data1 = &data;
    println!("sum of data1: {}", sum(&data1));
    println!("data1: {:? }", data1);
    println!("sum of data: {}", sum(&data));
}

fn sum(data: &Vec<u32- > >)u32 {
    data.iter().fold(0, |acc, x| acc + x)
}
Copy the code

The Borrow semantics can be implemented using &. As the name implies, the Borrow semantics allow the ownership of a value to be used by other contexts without transfer.

In Rust, “borrowing” and “referencing” are one and the same concept, and in Rust, all references are merely borrowing a “temporary use right” that does not break the single-ownership constraint on a value.

So Rust’s “borrowings” are read-only by default.

Of course, we have to borrow values with a limit: we can’t borrow beyond the lifetime of the value.

When you write React or Vue, each component has a life cycle from creation to destruction. So what’s the life cycle of a value in Rust? What’s the life cycle of borrowing a value?

Life cycle: How long the value we created can live

In any language, values on the stack have their own lifetime, which is consistent with the lifetime of frames.

In Rust, memory on the heap also introduces the concept of a lifetime: unless you explicitly do something like Box::leak(), the lifetime of heap memory is bound by default to the lifetime of its stack memory.

Box: Using Box

allows you to place a value on the heap instead of the stack, leaving a pointer to the heap data on the stack. Aside from the fact that data is stored on the heap rather than on the stack, boxes have no performance penalty and are used in the following scenarios:

  • When you have a type whose size is unknown at compile time and you want to use the type value in a context where the exact size is required (such as a recursive type)
  • When there is a lot of data and you want to transfer ownership without making sure the data is not copied

Let’s start with an example:

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}
Copy the code

This code does not compile because the values referenced by r are released before use.

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 | 
9 |     println!("r: {}", r);
  |                       - borrow later used here
Copy the code

The Rust compiler has a borrow checker that compares scopes to ensure that all borrows are valid, as in the example above, which we annotated with the lifecycle:

fn main() {
    let r;                // ---------+-- 'a
                          // |
    {                     // |
        let x = 5;        // -+-- 'b |
        r = &x;           // | |
    }                     // -+ |
                          // |
    println!("r: {}", r); // |
}                         // ---------+
Copy the code

As you can see, the inner ‘B’ block is much smaller than the outer life cycle ‘A’. At compile time, Rust compares the size of the two life cycles and finds that R has life cycle ‘A, but it refers to an object that has life cycle ‘b. The program is rejected for compilation because life cycle ‘b ‘is smaller than life cycle ‘a: the referenced object exists for a shorter time than its referrer.

Thus, we also explain Rust’s rule in value borrowing that borrowing cannot exceed the lifetime of a value.

In Rust, the life cycle of a value can be divided into:

  • Static life cycle: If the life cycle of a value lasts through the life cycle of the process, it is called static life cycle.
  • Dynamic life cycle: If a value is defined in a scope, that is, it is created on the stack or heap, then its life cycle is dynamic.

  • Memory allocated on the heap and stack has its own scope and its life cycle is dynamic.
  • Global variables, static variables, string literals, code, etc., are compiled into an executable file and loaded into memory at compile time. The lifecycle is consistent with the process lifecycle; the lifecycle is static.
  • The life cycle of a function pointer is also static, because the function is in the Text segment and its memory remains as long as the process is alive.

With these concepts in mind, let’s look at another example:

fn main() {
    let s1 = String::from("Lindsey");
    let s2 = String::from("Rosie");

    let result = max(&s1, &s2);

    println!("bigger one: {}", result);
}

fn max(s1: &str, s2: &str) - > &str {
    if s1 > s2 {
        s1
    } else {
        s2
    }
}
Copy the code

This code also failed to compile, and the compiler reported the following error:

error[E0106]: missing lifetime specifier
  --> src/main.rs:10:31
   |
10 | fn max(s1: &str, s2: &str) - > &str {
   |            ----      ----     ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `s1` or `s2`
help: consider introducing a named lifetime parameter
   |
10 | fn max<'a>(s1: &'a str, s2: &'a str) - > &'a str {
   |       ++++      ++           ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to previous error
Copy the code

Rust’s compiler has always been a mentor, rigorous and excellent, guiding me to write more reliable and efficient code.

Missing Lifetime specifier means that the compiler cannot determine the lifetime of s1, s2, and return values when compiling the Max () function.

The compiler also gave us a workaround: manually add lifecycle annotations to tell the compiler the life cycles of S1 and S2.

fn max<'a>(s1: &'a str, s2: &'a str) - > &'a str {
    if s1 > s2 {
        s1
    } else {
        s2
    }
}
Copy the code

This example might seem confusing to you. Why is it that the compiler can’t tell the life cycles of S1 and S2 when they are identical?

It’s quite simple. As we mentioned earlier, string literals have a static life cycle, whereas S1 is dynamic, and their life cycles are inconsistent.

When multiple parameters are present, their lifecycles are not consistent and the life cycle of the returned value is not determined, so we need to annotate the life cycle to tell the compiler the constraints of the reference life cycle.

Rust and Webassembly

WebAssembly (WASM) runs in modern Web browsers — it’s a low-level assembler like language with a compact binary format that runs close to native performance.

In short, For Web platforms, WebAssembly provides a way for code written in a variety of languages to run on the Web at near-native speeds.

On the front end, WASM ** technology helps solve performance bottlenecks in scenarios such as VR, graphics and video editing, 3D games, and more.

Next, let’s start with an image processing example and see how to build a Web-WASM application from scratch.

Environment to prepare

Rust

curl --proto '=https'- tlsv1.2 - sSf https://sh.rustup.rs | shCopy the code

When you install Rustup, you also install the latest stable version of the Rust build tool and package manager, Cargo.

Cargo can do a lot of things like:

  • cargo buildYou can build projects
  • cargo runCan run projects
  • cargo testCan test items
  • cargo docYou can build documentation for your project
  • cargo publishLibraries can be published tocrates.io.

Cargo clearly plays the role of Npm in Rust in Node.

Wasm-pack

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
Copy the code

Wasm-pack is used to build and use Rust-generated WebAssemblies that we want to interoperate with JavaScript, browsers, or Node.js.

Vite

I’m using Vite for the front-end build tool here, and I need to add a plug-in to Vite:

Vite-plugin-rsw: CLI integrated with WASM-pack

  • Support hot update of RUST package files and monitorsrcDirectory andCargo.tomlFile changes, automatic build
  • Vite starts optimization and starts again if it was built beforenpm run dev, will be skippedwasm-packbuild

Quick start

Create a Vite project

yarn create vite vite-webassembly
Copy the code

Add the viet-plugin-rsW plug-in

yarn add vite-plugin-rsw -D
Copy the code

And add the corresponding configuration:

// vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import ViteRsw from 'vite-plugin-rsw'

export default defineConfig({
  plugins: [
    react(),
    / / read more: https://github.com/lencx/vite-plugin-rsw
    ViteRsw({
      If the package is configured in both 'unLinks' and' crates'
      NPM unlink (NPM unlink);
      // For example
      // `npm unlink picture-wasm`
      unLinks: ['picture-wasm'].// The rust project in the project root path
      // '@' starts with NPM organization
      // For example:
      // `npm link picture-wasm`
      // unLinks unloads' picture-wasm 'for execution order reasons
      // But the installation will be redone
      crates: ["picture-wasm"],}),],})Copy the code

Initialize a Rust project using cargo

Execute in current directory:

cargo new picture-wasm
Copy the code

Let’s take a look at the current directory structure

[my - wasm - app] # | project root directory - [picture - wasm] # NPM package ` wasm - hey ` | | - (PKG) # generate wasm package directory | | | - picture - wasm_bg. | wasm # wasm file | | - picture - wasm. Js # entry document | | | - picture - wasm_bg. Wasm. Which statement s # ts file | | | - picture - wasm. Which s # ts declaration file | | | - package.json | | - ... | | - (SRC) rust source | | # learn more: # https://doc.rust-lang.org/cargo/reference/cargo-targets.html | | - (target) project, similar to the NPM ` node_modules ` | | # learn more: # https://doc.rust-lang.org/cargo/reference/manifest.html | | - Cargo. Toml rust package management listing | -... | - [node_modules] # front-end project packages depend on | - (SRC) # front-end source code (can be a vue, react, or other) | # learn more: # https://nodejs.dev/learn/the-package-json-guide | - package. Json ` yarn ` package management listing | # learn more: # https://vitejs.dev/config | - vite. Config. Ts vite configuration file | # learn more: # https://www.typescriptlang.org/docs/handbook/tsconfig-json.html | - tsconfig. Json typescript configuration fileCopy the code

As you can see, in the generated picture-Wasm project, there is a Cargo. Toml file, similar to our package.json, that is used as a package management manifest for Rust.

toCargo.tomlAdd configuration to

[package] name = picture-wasm version = 0.1.0 edition = 2021 [lib] crate-type = [cdylib, Rlib] [dependencies] wasM-bindgen = 0.2.70 base64 = 0.12.1 image = {version = 0.23.4, default-features = false, Features = [JPEG, PNG]} console_error_panic_hook = {version = 0.1.1, Optional = true} wee_alloc = {version = 0.4.2, Optional = true} [dependencies. Web-sys] version = 0.3.4 features = ['Document', 'Element', 'HtmlElement', 'Node', 'Window', ]Copy the code
  • dependencies: Dependency list
  • package: Defines the package
  • lib: We are currently a library project (lib.rs under SRC) rather than an executable project (main.rs under SRC) and need to set it up.
    • rlib: Rust Library-specific static intermediate Library format If it’s just dependencies and calls between pure Rust code projects, then rlib is perfectly adequate (the default).
    • cdylib: c specification dynamic library, which can expose some FFI functions, and can be called by other languages.

Adding Rust code

// picture-wasm/src/lib.rs

// Link to the 'image' and 'base64' libraries and import the items
extern crate image;
extern crate base64;
// Use 'use' to import the corresponding method from the image namespace
use image::DynamicImage;
use image::ImageFormat;
// Import the corresponding method from the STD (base library) namespace, which can be destructed
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::panic;
use base64::{encode};

// Introduce all prelude modules under WASM_bindgen to communicate between Rust and JavaScript
use wasm_bindgen::prelude::*;

// When 'wee_alloc' is enabled, 'WEE_alloc' is used as the global allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

The #[wasm_bindgen] property indicates that the function below it is accessible in JavaScript and Rust.
#[wasm_bindgen]
extern "C" {
    // This extern block imports the external JavaScript function console.log into Rust.
    // By declaring it this way, wASM-bindgen creates the JavaScript stub console
    // Allows us to pass strings back and forth between Rust and JavaScript.
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

fn load_image_from_array(_array: &[u8]) -> DynamicImage {
    // Use match to match the bottom of the loop
    let img = match image::load_from_memory_with_format(_array, ImageFormat::Png) {
        Ok(img) => img,
        Err(error) => {
            panic!("There was a problem opening the file: {:? }", error)
        }
    };
    img
}

fn get_image_as_base64(_img: DynamicImage) -> String {
    // Use mut to declare mutable variables, like let in js, immutable without mut
    // Create a memory cache containing the dynamic array types
    let mut c = Cursor::new(Vec::new());
    // Write the image
    match _img.write_to(&mut c, ImageFormat::Png) {
        Ok(c) => c,
        Err(error) => {
            panic!(
                "There was a problem writing the resulting buffer: {:? }",
                error
            )
        }
    };
    // Find the offset in bytes, use unwrap to implicitly handle the Option type, return the value directly or report an error
    c.seek(SeekFrom::Start(0)).unwrap();

    // Declare a mutable dynamic array for output
    let mut out = Vec::new();
    c.read_to_end(&mut out).unwrap();
    // Use the encode transform
    let stt = encode(&mut out);
    let together = format!("{} {}"."data:image/png; base64,", stt);
    together
}

#[wasm_bindgen]
pub fn grayscale(_array: &[u8]) -> Result<(), JsValue> {
    let mut img = load_image_from_array(_array);
    img = img.grayscale();
    let base64_str = get_image_as_base64(img);
    append_img(base64_str)
}

pub fn append_img(image_src: String) - >Result<(), JsValue> {
    // Use 'web_sys' to get the window object
    let window = web_sys::window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
    let body = document.body().expect("document should have a body");

    // Create the img element
    / / use `? 'returns Err directly when an error occurs
    let val = document.create_element("img")? ;// val.set_inner_html("Hello from Rust!" );
    val.set_attribute("src", &image_src)? ; val.set_attribute("style"."height: 200px")? ; body.append_child(&val)? ; log("success!");

    Ok(())}Copy the code

The React project calls the Wasm method

// src/App.tsx

import React, { useEffect } from "react";
import init, { grayscale } from "picture-wasm";

import logo from "./logo.svg";
import "./App.css";

function App() {
  useEffect(() = > {
    // WASM initialization when the 'picture-wasm' package method is called
    // Ensure that the initialization has been performed, otherwise an error will be reported
    // If there are multiple WASM packages, each wASM package must be initializedinit(); } []);const fileImport = (e: any) = > {
    const selectedFile = e.target.files[0];
    // Get the File object that reads my File
    // var selectedFile = document.getElementById("files").files[0];
    var reader = new FileReader(); // This is the core that does the reading.
    reader.readAsArrayBuffer(selectedFile); // Read the contents of the file, or read the URL of the file
    reader.onload = (res: any) = > {
      var uint8Array = new Uint8Array(res.target.result as ArrayBuffer);
      grayscale(uint8Array);
    };
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello WebAssembly!</p>
        <p>Vite + Rust + React</p>
        <input type="file" id="files" onChange={fileImport} />
      </header>
    </div>
  );
}

export default App;
Copy the code

Execute!

When yarn Dev is executed in the root directory, the RSW plug-in packages the rust project and soft links it over, and a web-WASM application that converts native color images to black and white images is complete.

The resources

  • First lesson of Rust Programming
  • Rust Industry Research Report 2021 -InfoQ
  • 24 days from node.js to Rust
  • The client perspective recognizes and feels the reds and blacks of Rust
  • Implement a simple image processing application based on WebAssembly
  • Rust and WebAssembly