I. Introduction
In the previous article "Getting Fancy with Rust and WebAssembly (Part 1) - Quick Start", we described how to create a project and quickly generate wasm for use in the frontend, taking the first step towards creating cool stuff.
When developing web applications, Wasm modules written in Rust can provide higher performance and better security. However, to integrate with existing JavaScript code, interaction between Rust and JS must be implemented. The main purpose of Rust-JS interaction is to combine the strengths of both languages to achieve better web applications.
In this article, we will continue development based on the project created in the previous article.
II. Environment
- Rust 1.70.0
- wasm-bindgen 0.2.87
- web-sys 0.3.64
III. DOM
1. Configuring Dependencies
To manipulate the DOM, we need to introduce a new dependency web-sys
. Therefore, you can configure the dependencies in Cargo.toml
as follows:
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = [] }
You might be curious about what this features
is. To be honest, I was very curious at first and didn't see any special explanation. I found out through trial and error that you need to manually import feature dependencies... For example, if you need to use JS's console
in Rust, you need to add console
to features
.
2. Getting the Document
To use Document
in Rust, we need to add features
as explained in the previous step. There's a dependency relationship here: first, we need to get the window
in Rust, and then get the document
.
Therefore, after adding features
, it looks like this:
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document"] }
Then create a function in lib.rs
to call document
:
#[wasm_bindgen]
pub fn update_message() {
let window = web_sys::window().expect("Failed to load window");
let document = window.document().expect("Failed to load document");
}
Now we have unlocked document
in Rust, so we can do whatever we want in the frontend!
3. Manipulating Elements
Let's start manipulating. First, we need to get the Element
......
Yes, you guessed it right, let's continue to add features
. Here we need to add an Element
:
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document", "Element"] }
OK, let's continue. Here we set the function to take two parameters selector
and message
, then get the element through selector
, and update the value to the value of the message
parameter. The complete function is as follows:
#[wasm_bindgen]
pub fn update_message(selector: &str, message: &str) {
let window = web_sys::window().expect("Failed to load window");
let document = window.document().expect("Failed to load document");
let element = document.query_selector(selector).expect("Failed to load element");
if let Some(element) = element {
element.set_inner_html(message);
} else {
panic!("Failed to set inner html")
}
}
4. Compilation
Compile the completed Rust project into wasm:
wasm-pack build --target web
5. Calling in HTML
Based on the HTML from the project in the previous article, add a div
here with id message
, and add a call to the wasm update_message
function. The code is as follows:
<body>
<div id="message"></div>
</body>
<script type="module">
import init, { update_message } from './pkg/hello_wasm.js'; // 引入update_message函数
const run = async () => {
await init();
update_message('#message', '<h1>Hello, Rust!</h1>'); // 调用update_message函数
}
run();
</script>
6. Verification in Browser
Start an HTTP server, then view it in the browser. You can see an h1
tag with Hello, Rust!
appearing on the page.
7. Discovering More Methods
As you write according to the article, you might notice a problem - why aren't these methods auto-completing?!
Yes, that's right, (at least I found that) currently web-sys
doesn't have auto-completion, so we can only develop by combining developers' excellent frontend skills and the rich official documentation.
IV. Type Conversion Between Rust and JS
For wasm, performance is certainly improved, but type conversion has always been an issue. When a large amount of data needs to be type-converted in wasm/js, it's really a disaster for performance. I encountered this problem when developing wasm with Go before, where I needed to use official methods for manual type conversion, but wasm was dealing with a very large amount of data......
Fortunately, Rust's type support is really rich!
1. Basic Types
Basic types are quite simple, and Rust's generics also support many types well. Here's a mapping table of basic types:
- i8: number
- i16: number
- i32: number
- i64: BigInt
- u8: number
- u16: number
- u32: number
- u64: BigInt
- f32: number
- f64: number
- bool: boolean
- char: string
- &str: string
- String: string
- &[T] (e.g.: &[u8]): [T] (e.g.: Uint8Array)
- Vec<T>: Array
2. Basic Type Conversion Example
In the lib.rs
file, create a function that takes a few types as parameters, then reads and prints them:
#[wasm_bindgen]
pub fn print_values(js_number: i32, js_boolean: bool, js_uint8_array: &[u8], js_number_array: Vec<i32>) {
println!("js number: {}", js_number);
println!("js boolean: {}", js_boolean);
for item in js_uint8_array {
println!("js Uint8Array item: {}", item);
}
for item in js_number_array {
println!("js number array item: {}", item);
}
}
You can see that this function takes four types of parameters from JS: number
, boolean
, Uint8Array
, and Array
.
Then compile:
wasm-pack build --target web
Next, import the function and call it in the frontend:
<script type="module">
import init, { print_values } from './pkg/hello_wasm.js';
const run = async () => {
await init();
const jsNumber = 10;
const jsBoolean = true;
const jsUint8Array = new Uint8Array(3);
jsUint8Array[0] = 1;
jsUint8Array[1] = 2;
jsUint8Array[2] = 3;
const jsNumberArray = [30, 40, 50];
print_values(jsNumber, jsBoolean, jsUint8Array, jsNumberArray);
}
run();
</script>
Finally, start the HTTP server and open the browser, and in the console you can see... you can't see?!
Yes, that's right, Rust's println!
will only send the printed content to Rust's standard output stream, not the frontend's console. If you want to print in the console, you need to call JS's console
.
To use the new functionality, the first step is to add features
. Add console
to Cargo.toml
as follows:
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document", "Element", "console"] }
Calling console.log()
in Rust is as follows:
web_sys::console::log_1(&"Hello, Rust!".into());
Here we encapsulate it into a function:
fn console_log(message: String) {
web_sys::console::log_1(&message.into());
}
Then, change the println
in the example function to console_log()
and format!
. The function code is as follows:
#[wasm_bindgen]
pub fn print_values(js_number: i32, js_boolean: bool, js_uint8_array: &[u8], js_number_array: Vec<i32>) {
console_log(format!("js number: {}", js_number));
console_log(format!("js boolean: {}", js_boolean));
for item in js_uint8_array {
console_log(format!("js Uint8Array item: {}", item));
}
for item in js_number_array {
console_log(format!("js number array item: {}", item));
}
}
Finally, after compiling, open the browser, and you can see the output in the console:
js number: 10
js boolean: true
js Uint8Array item: 1
js Uint8Array item: 2
js Uint8Array item: 3
js number array item: 30
js number array item: 40
js number array item: 50
3. Generic Type
Rust provides a generic type - JsValue, which can be used as any JS type.
Here's a simple example, setting up a function that takes JsValue
as a parameter and prints it.
Create the function:
#[wasm_bindgen]
pub fn print_js_value(val: JsValue) {
console_log(format!("{:?}", val));
}
Then compile it into a wasm file.
Call it in HTML:
<script type="module">
import init, { print_js_value } from './pkg/hello_wasm.js';
const run = async () => {
await init();
const jsNumber = 10;
const jsBoolean = true;
const jsUint8Array = new Uint8Array(3);
jsUint8Array[0] = 1;
jsUint8Array[1] = 2;
jsUint8Array[2] = 3;
const jsNumberArray = [30, 40, 50];
print_js_value(jsNumber);
print_js_value(jsBoolean);
print_js_value(jsUint8Array);
print_js_value(jsNumberArray);
}
run();
</script>
In the HTML, different types of parameters are passed in, but in the browser's console, you can see that all different types of parameters are printed:
JsValue(10)
JsValue(true)
JsValue(Uint8Array)
JsValue([30, 40, 50])
4. Result
Result
is a very important existence in Rust. If you often write Rust, you don't want to change your development habits when writing WebAssembly.
For JS, Result
can be directly caught in catch
, but here we need to define the parameter types well.
4.1 Using Result to Return Errors
First, let's look at a scenario that only returns errors:
#[wasm_bindgen]
pub fn only_return_error_when_result(count: i32) -> Result<(), JsError> {
if count > 10 {
Ok(())
} else {
Err(JsError::new("count < 10"))
}
}
Here the return type is Result
, but it only returns an error. It's worth noting that the error type used here is JsError
, of course, JsValue
can also be used here.
Then call it in HTML:
<script type="module">
import init, { only_return_error_when_result } from './pkg/hello_wasm.js';
const run = async () => {
await init();
try {
only_return_error_when_result(1);
console.log('1 is ok');
} catch(error) {
console.log('An error is reported when the input parameter is 1: ', error);
}
try {
only_return_error_when_result(100);
console.log('100 is ok');
} catch(error) {
console.log('An error is reported when the input parameter is 100: ', error);
}
}
run();
</script>
Here it's called twice, the first time should be an error, the second time should be correct, and both use catch
to catch errors.
So, in the browser's console, you can see the output:
An error is reported when the input parameter is 1: Error: count < 10
100 is ok
4.2 Using Result to Return Normal Values and Errors
So, what if you want to return both normal values and errors? Rust returning a Result
is no problem, but how does JS parse it?
Let's go straight to the Rust code:
#[wasm_bindgen]
pub fn return_all_when_result(count: i32) -> Result<i32, JsError> {
if count > 10 {
Ok(count + 10)
} else {
Err(JsError::new("count < 10"))
}
}
This function, after getting the parameter, if it meets the condition, adds 10 and returns, otherwise it reports an error.
Let's see how to call it in HTML:
<script type="module">
import init, { return_all_when_result } from './pkg/hello_wasm.js';
const run = async () => {
await init();
try {
const res = return_all_when_result(1);
console.log(`get ${res}`);
} catch(error) {
console.log('An error is reported when the input parameter is 1: ', error);
}
try {
const res = return_all_when_result(100);
console.log(`get ${res}`);
} catch(error) {
console.log('An error is reported when the input parameter is 100: ', error);
}
}
run();
</script>
Yes, that's right, just get it normally..... /facepalm
The call here is still, the first one is wrong, the second one correctly returns a value, and both use catch
to catch errors.
Finally, in the browser's console, you can see:
An error is reported when the input parameter is 1: Error: count < 10
get 110
5. Directly Importing JS Types
If you want to be more direct, you can directly import JS types! This mainly uses the js-sys
dependency, you can see many JS types and functions on the official documentation, just import them directly to use. Of course, in certain scenarios, directly imported types need to be manually converted.
5.1 Configuring Dependencies
Add the js-sys
dependency in Cargo.toml
:
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["Window", "Document", "Element", "console"] }
js-sys = "0.3.61"
5.2 Uint8Array
First, take Uint8Array
as an example, import the type at the top of lib.rs
:
use js_sys::Uint8Array;
Then create a function with parameters and return both of type Uint8Array
:
#[wasm_bindgen]
pub fn print_uint8_array(js_arr: Uint8Array) -> Uint8Array {
// new Uint8Array
let mut arr = Uint8Array::new_with_length(3);
// Uint8Array -> vec
for (index, item) in js_arr.to_vec().iter().enumerate() {
console_log(format!("{} - the item in js_arr: {}", index, item));
}
// Avoid type conversion
// Use the method of the type itself
for index in 0..js_arr.length() {
console_log(format!("{} - the item in js_arr: {}", index, js_arr.get_index(index)));
}
// vec -> Uint8Array
let vec = vec![1, 2, 3];
let arr2 = Uint8Array::from(vec.as_slice());
arr = arr2.clone();
// Use the method of the type itself
arr.set_index(0, 100);
arr
}
Ignore the meaningless logic in this function and the warning of the
arr
variable, it's just for demonstration purposes.
You can see in the code that the directly imported Uint8Array
has its own methods. In certain scenarios, type conversion is needed, but it's best to avoid type conversion and directly use its own methods.
We can briefly summarize here that it's best to use directly imported JS types entirely within certain scenarios, or use Rust types to replace JS types entirely. When both exist, manually converting types is a terrible thing.
5.3 Date
The Date
type wasn't mentioned in the above sections. Here we can directly import and use the JS Date
type.
First, import the type:
use js_sys::Date;
Then, create a function that returns a timestamp:
#[wasm_bindgen]
pub fn return_time() -> f64 {
let date = Date::new_0();
date.get_time()
}
Then, call it in HTML:
<script type="module">
import init, { return_time } from './pkg/hello_wasm.js';
const run = async () => {
await init();
console.log('current time: ', return_time());
}
run();
</script>
Finally, in the browser's console, you can see:
current time: 1686979932833
V. Conclusion
In this article, we mainly discussed how to use Rust to implement DOM operations. Readers can find suitable methods to implement their own scenarios based on the methods. Secondly, we also discussed the type conversion between Rust and JS, from the mapping of basic types of each, to Rust's unique Result
, to directly importing JS types. Of course, it should be noted that directly importing JS types and mapping JS types to Rust's basic types should not be mixed as much as possible. Mixing will lead to the need for manual type conversion, causing performance loss.
At this point, we've taken another step towards creating cool stuff with Rust and WebAssembly~😼