DEV Community

loading...

From Javascript to Rust to WASM

starsoccer
・4 min read

For a while Ive been interested in not only Rust but WASM but having limited familiarity with many of the languages that can be compiled to WASM I never really had a chance to try it out until recently. Over the last few months though I got the opportunity to learn Rust.

Rust in my opinion is very much like typescript on steroids. While Typescript may enforce types in your code if you happen to pass a string to a type expecting a number things will still compile and may even work as expected still. With Rust this is not the case. If you provide an unexpected type either things will not compile in the first place or your software will crash.

A personal project of mine (Cryptotithe) which is an open source tax software for cryptocurrencies was something I always though would benefit from WASM since it has some computation heavy parts. While I would not say it is extremely resource or computation heavy calculating gains does need a little bit of basic math. There is also a need to do some searches with in arrays depending on the users selection of alternative types of accounting such as LIFO, HCFO(Highest Cost First Out), etc.. which can increase the amount of calculations being done.

So, a few weeks back I decided to try converting the heaviest parts into rust and then using wasm-bindgen convert it wasm for use in my typescript project. While creating a basic Rust project was easy moving building the WASM package and linking things proved to be the first challenge.

My project has a few different functions but overall has a straightforward path of functions that more or less all rely on one another which is broken down below. The end goal being to convert all of them to Rust.


                                           ┌─────────────────┐
                                           │                 │
                                      ┌────┤ calculate_gains │
                                      │    │                 │
       ┌──────────────────────────┐   │    └────────┬────────┘
       │                          │   │             │
       │ add_to_currency_holdings │◄──┤             │
       │                          │   │             │
       └──────────────────────────┘   │     ┌───────▼───────┐
                                      │     │               │
                                      └─────┤ process_trade │
                                            │               │
                                            └───────┬───────┘
                                                    │
                                                    │
                                                    │
   ┌───────────────────────────────┐      ┌─────────▼─────────┐
   │                               │      │                   │
   │ check_currency_holding_amount │◄─────┤ holding_selection │
   │                               │      │                   │
   └───────────────────────────────┘      └─────────┬─────────┘
                                                    │
                                                    │
                                                    │
                                         ┌──────────▼───────────┐
                                         │                      │
                                         │ get_currency_holding │
                                         │                      │
                                         └──────────────────────┘

Enter fullscreen mode Exit fullscreen mode

Gotchas

While wasm-bindgen has support for automatically generating typescript types, in general there are some common gotchas.

One of the biggest gotchas is that u32 are converted to regular typescript numbers but u32 are actually smaller.

// this is not valid
let num: u32 = 1621867244484;
Enter fullscreen mode Exit fullscreen mode

This might not seem like a big deal but if your dealing with numbers on the higher end of this specturm it quickly becomes an issue. This means a u64 has to be used, but sadly this means the typescript interface wasm-bindgen generates will have this as a BigInt instead of a number. This simply pushes the complexity to the javascript side though.

After trying a few different ways I could not find a great solution that didn't involve a lot of extra boilerplate code. In the end I personally found it easier to simply give up on having correct typescript types and instead accepted that Any were going to be there.

While not specifically a wasm-bindgen issue debugging wasm can be quite a challenge. Perhaps this is due to the way I was converting types or maybe there are tools Im not aware of that make this easier. The majority of time there was an issue I basically got a standard unreachable code error which would link to some wasm that was not at all helpful.

Solving issues like this basically became a guessing game to see where exactly it stopped working and then try to backtrack to understand the why.

One helpful way of debugging is by logging right in your wasm code which wasm-bindgen natively supports

use web_sys::console;
console::log_2(&"Logging arbitrary values looks like".into(), &some_variable_here);
Enter fullscreen mode Exit fullscreen mode

The best part about using console log in rust is you can also log javascript objects passed directly into rust relatively easily by simply first converting them to a JSValue as seen below:

use web_sys::console;
console::log_2(&"Logging arbitrary values looks like".into(), &JsValue::from_serde(&some_variable_here).unwrap());
Enter fullscreen mode Exit fullscreen mode

Slow Data Transfer

While not a gotcha, one thing to be aware of is that transferring complex types between Javascript and WASM can be slow. This means its often not worth it to simply pass an object to WASM for one or two small computations. If you can simply pass a number instead it may be significantly faster, but in scenarios where thats not an option, WASM may actually be slower. This means when planning to convert some area of your code to WASM you should first investigate what data would be passed around and how much you may need to rewrite in order to reap the benefits.

I originally started working by simply converting the bottom most function in my project, get_currency_holding and exposing it as a proof of concept. As a proof of concept this was great, but it was significantly slower.

The slowness made sense since holding_selection, the function that calls get_currency_holding does so repeatably possibly multiple times per trade. This made it clear to me that I needed to rewrite this function as well which began a snowball effect. First holding_selection but that requires calling check_currency_holding_amount; But still to slow since holding_selection is simply called repeatably per trade by process_trade. process_trade requires add_to_currency_holdings. process_trade though is repeatably called by calculate_gains.

Its only at this final function calculate_gains where the speed benefits became clear and the whole conversion ended up being worth it since this function is called one and only has a one time transfer cost typically.

Results

Overall I would consider the work a success as it took the time to execute on a personal data file of mine from ~130ms to less then 10ms. A 10x improvement. I have yet to push this new WASM powered version live quite yet as I need to do some clean things up a bit but you can take a look at the rust version here, CryptoTithe-RS

Discussion (0)