Bincode Parser
feature
, extern crate
, and use
statements. This isn't too much different from our hello world example, just more stuff needs to be imported.
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate bincode;
extern crate serde;
extern crate serde_json;
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
extern crate wasm_tutorial_shared;
use wasm_tutorial_shared::models::{Message, ToDo};
console.log
function from js
. We do that by defining an extern
block and annotating that with some wasm_bindgen
macros.
#[wasm_bindgen]
extern {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
First the extern
block will tell the compiler that we are defining some non-rust code, inside that we are going to name the function we are importing which is log
taking a single string. Notice that we used (js_namespace = console)
when we defined log
, that is because we need to provide a hint to the compiler that the JS
side of things will need the console
constant in front of it.
Up next we are going to define our function for converting bincode
encoded bytes to JSON
strings and vice versa.
#[wasm_bindgen]
pub fn bincode_to_json(buffer: Vec<u8>) -> String {
log("bincode_to_json");
if let Ok(msg) = Message::from_bytes(buffer) {
match msg {
Message::All(todos) => {
if let Ok(ser) = serde_json::to_string(&todos) {
return ser
}
},
Message::Error(msg) => {
log(&format!("Error getting todos from msg {}", msg));
},
_ => log("Incorrect message type")
}
}
"[]".into()
}
Since bincode
is just a list of raw 8 bit unsigned integers, we are going to expect that is what comes from the JS
code. Once we have that, we can try and convert it from raw bytes into a Message
using the from_bytes
constructor. In this case, we don't have much to do if this fails so we are just going to let the function return an empty JSON array if it does, that allows us to use if let Ok(msg)
, this will only enter the block if deserialization was successful. Once we know that we have successfully created a Message
we are going to use match
to check if it is either an All
or Error
, if it is anything else we would end up in the _
branch, there we just want to log that it happened for debugging. Also for the error case we really just want to print the error to the browser's console, however if the message is the All
case, then we want to assign the associated value to a variable named todos
. Once we have our list of ToDo
s, we want to try and serialize that into JSON, technically this might fail but that is actually a pretty slim possibility so we are going to use the if let
to just let our empty JSON array be the fallback if it does. If serde_json::to_string
is successful, that means we can return the string to our JS caller.
...but I thought Rust didn't use the return
keyword, how come you are here?
Very astute of you, Rust does allow you to use this keyword, you can actually use it all the time if that is what floats your boat. The Rust style guide encourages you to only use return
in the case of an early return, like what we are doing here.
That should handle all of the messages we will need to receive from the server, now lets go the other way.
#[wasm_bindgen]
pub fn get_add_message(action: String) -> Vec<u8> {
let todo = ToDo::new(action);
let msg = Message::Add(todo);
msg.to_bytes()
}
#[wasm_bindgen]
pub fn get_update_message(id: i32, complete: bool, action: String) -> Vec<u8> {
let todo = ToDo {
id,
complete,
action
};
let msg = Message::Update(todo);
msg.to_bytes()
}
#[wasm_bindgen]
pub fn get_remove_message(id: i32) -> Vec<u8> {
let msg = Message::Remove(id);
msg.to_bytes()
}
#[wasm_bindgen]
pub fn get_all_message() -> Vec<u8> {
Message::GetAll.to_bytes()
}
These functions are a little more straight forward than the last one, they take in each of the important properties of a ToDo
item and constructs them on the wasm
side of things. Once that is done we just construct the message and use the to_bytes()
method that will return the bincode representation.
Since Rust doesn't have any special constructor function, you can see here how we build our model instances. Building a struct is very similar to building javascript objects, just put the type in front, followed by some curly braces and set each of the values, you can even use the same "object short notation" from ES6 if your variable is the same as your property. For enums, you are going to use the type name and then two colons (::
) and the case, if that case has an associated value you would need to provide an instance of that value in the parentheses. You can also see that we use that same double colon to call static methods on our types like ToDo::new(action)
. Now that we have all this setup, lets try and build it.