Introduction
In the previous article “Getting Fancy with Rust and WebAssembly (Part 2) - DOM Manipulation and Type Conversion”, we described how to use Rust to manipulate the DOM and implement various methods for type conversion between Rust and JS.
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 advantages of both languages to achieve better web applications.
Based on the various methods of type conversion between Rust and JS in the previous article, this article continues to delve into the interaction between Rust and JS.
First, the type conversion between Rust and JS can achieve variable passing, so where are these variables to be used? It must be in functions!
Therefore, this article will discuss the mutual function calls between Rust and JS. Based on this, a large number of daily functional developments can be implemented.
Additionally, we will also discuss how to export Rust structs for JS to use.
Yes, that’s right, calling Rust structs in JS! When I first saw this feature, my mind was a bit blown……đŗ
In this article, we will continue development based on the project created in the previous article.
Environment
- Rust 1.70.0
- wasm-bindgen 0.2.87
Mutual Function Calls
1. JS Calling Rust Functions
Actually, in the first article of this series, we used JS calling Rust functions as an example for demonstration. Here we’ll still use this as an example, mainly to discuss the key points.
First, declare a Rust function:
|
|
The key points to note here are as follows:
- Import
wasm_bindgen
- Declare a function, use
pub
to declare - Use the
#[wasm_bindgen]
macro on the function to export the Rust function as a function of the WebAssembly module
Then, compile it into a wasm file:
|
|
Next, call this function in JS:
|
|
Finally, start the http server, and you can see the result from rust is: 3
in the browser’s console, indicating a successful call!
2. Rust Calling JS Functions
2.1 Specifying JS Objects
When calling JS functions in Rust, you need to specify JS objects, which means you need to explicitly tell Rust where this JS function is taken from JS to use.
It mainly involves the following two ways:
- js_namespace: This is an optional attribute used to specify a JavaScript namespace that contains functions to be exported in the wasm module. If
js_namespace
is not specified, all exported functions will be placed in the global namespace. - js_name: This is another optional attribute used to specify the function name in JavaScript. If
js_name
is not specified, the exported function name will be the same as the function name in Rust.
2.2 JS Native Functions
For some JS native functions, in Rust, you need to look for alternative solutions. For example, the console.log()
function we talked about in the previous article, isn’t it a bit troublesome!
So, do you want to directly call JS native functions in Rust?!
Here, let’s take the console.log()
function as an example, directly import and call it in Rust, avoiding the trouble of alternative solutions.
First, here’s the Rust code:
|
|
In the above code, the call_js_func
function, as the name suggests, calls the js function and passes the parameter hello, javascript!
.
So, let’s analyze the code above the call_js_func
function step by step:
- The first line of code
#[wasm_bindgen]
is a Rust attribute that tells the compiler to export the function as a WebAssembly module extern "C"
is the C language calling convention, which tells the Rust compiler to export the function as a C language function#[wasm_bindgen(js_namespace = console)]
tells the compiler to bind the function to the console object in JavaScriptfn log(message: &str)
is a Rust function that takes a string parameter and prints it to the console object in JavaScript
The key to interacting with JS here is js_namespace
. In Rust, js_namespace
is an attribute used to specify the JavaScript namespace. In WebAssembly, we can use it to bind functions to objects in JavaScript.
In the above code, #[wasm_bindgen(js_namespace = console)]
tells the compiler to bind the function to the console
object in JavaScript. This means that the log()
function in Rust is called using the console.log()
function in JS.
Therefore, similar native functions can all be called using this method.
Finally, let’s call it in JS:
|
|
You can see hello, javascript!
in the browser’s console. Wonderful!
Actually, for console.log()
, there’s another way to call it, which is to use js_namespace
and js_name
together.
You might ask, is there any difference? Yes, there is some difference.
I don’t know if you’ve noticed, in the current example, js_namespace
is specified as console
, but the actual executed function is log()
. So the specification of this log
function is actually reflected in the same function name log
in Rust. In other words, the log()
in this example is the log()
in console.log()
.
Let’s try changing the name, change the original log()
to log2()
:
|
|
Then after compiling, look at the console, you’ll see an error:
|
|
Therefore, when we use the combination of js_namespace
and js_name
, we can customize the function name here.
Let’s look at the Rust code:
|
|
Here, a new function log_str
is defined, but it specifies js_namespace = console
and js_name = log
, so here, a custom function name can be used.
After directly compiling, look at the console, you can see the normal output: hello, javascript!
.
To summarize, if js_name
is not specified, the Rust function name will be used as the JS function name.
2.3 Custom JS Functions
In certain scenarios, you need to use Rust to call JS functions, such as scenarios that are more advantageous for JS - use JS to manipulate DOM, use Rust to calculate.
First, create a file index.js
, write a function:
|
|
The current file structure relationship is as follows:
|
|
Among them, index.js
and lib.rs
, as well as hello_wasm_bg.wasm
are not at the same level, index.js
is at the upper level of the other two files. Remember this structural relationship!
Then, in lib.rs
, specify the function:
|
|
Where raw_module = "../index.js"
means to specify the corresponding index.js
file, you should be clear that this specifies the index.js
we just created. The role of raw_module
is to specify the js file.
This code in the frontend can be equivalent to:
|
|
This way, you don’t need to import it in the frontend, it’s directly imported in Rust, which feels a bit magical.
Next, call this function in Rust:
|
|
Finally, call it in the frontend, after compiling, you can see the output result in the browser’s console!
To summarize, there are several points to note here:
- The JS function must be exported, otherwise it cannot be called;
raw_module
can only be used to specify relative paths, and, as you can notice in the browser’s console, the../
relative path here is actually the relative path in terms of the wasm file, this must be noted!
IV. JS Calling Rust’s struct
Now, for something mind-blowing, JS calling Rust’s struct?!
JS doesn’t even have structs, what will this exported thing look like, and how will it be called in JS?!
First, define a struct, and declare several methods:
|
|
Here, a struct named User
is declared, containing two fields name
and age
, and methods new
, print_user
, and set_age
are declared.
There’s also an unseen #[wasm_bindgen(constructor)]
, constructor
is used to indicate that the bound function should actually be converted to calling the new operator in JavaScript. You might not be very clear yet, but as you continue reading, you’ll understand.
Next, call this struct and its methods in JS:
|
|
As you can see, the usage here is very familiar!
Think about how to call it in Rust? That is, directly new one - User::new('HunterJi', 20)
.
Here in JS, it’s the same, first new one!
Then naturally call the struct’s methods.
After compiling, open the browser, you can see the output in the console: name is : HunterJi, age is : 21
.
Actually, you might be very curious, at least I was very curious, what exactly is Rust’s struct in JS?
Here, directly add a console.log(user)
, and you can see in the output. So what exactly is it in JS? Please print it out and see for yourself! :P
V. Conclusion
In this article, we mainly discussed the interaction between Rust and JS, reflected in the mutual calls between Rust and JS, which is based on the type conversion in the previous article.
The learning cost of mutual function calls between Rust and JS is still relatively high, and compared to writing wasm with Go, Rust’s granularity is very fine, almost can be said to be at will.
The most mind-blowing thing is exporting Rust’s struct for JS to use, which is a very great experience for the interaction between Rust and JS.