State of WebAssembly and Rust?

Adding a bit here:

Using Rust to compile to WebAssembly is still not completely usable in our applications in Parity.

Runtime is just huge, most of it is not used by actual code we want to run sandboxed and it is not being optimized out from binary. I can compile C source to just 120 bytes (!) of WASM with emcc, no way to do the same in Rust. Maybe using emcc SIDE_MODULES and compiling crates as side modules can somehow help.

This probably due to no dynamic linking and no library building support at all. If I just want to make sandboxed pub fn add(a: u32, b: u32) -> u32 in Rust usable from whatever can run WASM, i will still get WASM as the result which has main function, which will import various runtime symbols like STACK_TOP, DYNAMICTOP_PTR etc. (never used by actual code i care about - add function). Can be done easy with emcc and C.

We do a little tree-shaking against, say, add function (just exclude everything that can not be possible invoked from add including imports, globals, other functions and everything), but much of the runtime compiled into dynamic calls via call_indirect and cannot be statically optimized.

Despite those difficulties, we still have a lot of code that compiles to a rather performant wasm: keccak/sha256 hashing, secp256k1 cryptography, big (256/512) integer arithmetic, and we hope to make Rust suitable choice for smart contract development :slight_smile:

1 Like

@NikVolf

Is the source code for the tree-shaking or the projects you mentioned (hashes, big integers) available somewhere?

source is here

and also available as command line util

cargo install --git https://github.com/paritytech/wasm-utils wasm-prune
1 Like

Just adding another data point:

I’m compiling Rust to asmjs and have attempted to compile it to WebAssembly. My use case is a cross-platform library integrated into a larger JavaScript codebase. I haven’t had any issues installing, compiling, or linking, but I’ve run into some general problems and frustrations:

  • FFI is high-effort because it requires two explicit interfaces, one between JS and C and another between C and Rust. For instance, any function that passes a string to Rust has to convert JS string -> cstring -> &str and then free the cstring from the heap once the Rust function returns. Returning a string from Rust requires the same process in reverse. cwrap removes the JS <-> C FFI but it causes memory leaks in functions that return cstrings because it frees the cstring with free rather than Drop.
  • Both asm.js and wasm have huge starting file sizes, I guess because of libc. My use case only requires libc for heap allocations.
  • I can’t figure out how to use asmjs or wasm with WebPack. There’s at least one solution on GitHub but I couldn’t replicate it.
2 Likes

Author of stdweb and cargo-web here. AMA.

Could you also elaborate on some of the implementation details of stdweb?

As it was already mentioned, stdweb mostly uses emscripten_asm_const_int, etc. to embed JS code into Rust, and it has a fairly extensive machinery to marshal data between Rust and JS.

Glossing over some implementation details the js! macro goes through the tokens it was passed twice - in the first pass it stringifies everything, so

js!{ alert("Hello world!" + @{123}); }

becomes (minus some extra whitespace):

const CODE_STR: &'static str = "alert(\"Hello world\" + $0);\0"`;
const CODE: *const u8 = CODE_STR as *const _ as *const u8;

In the second pass it looks for any @{$expr} where $expr is a Rust expression, and automatically serializes them. Something like this is emitted (I'm simplifying so not exactly, but it's just so that you get the idea of how it works):

let a0 = $expr;
let a0 = serialize(a0, arena);
let a0 = &a0 as *const _;

Then it finally calls into emscripten:

emscripten_asm_const_int(CODE, a0);

(Not shown here is handling of a return value, and all of the other machinery necessary for serialization.)

Are there various features we need to add to rustc to get things to work? Maybe work nicer and/or less brittle than they may be right no? I’m not even sure how a js! macro would work!

@alexcrichton Some of the features off the top of my head that I would like the compiler and/or cargo to have, in no particular order:

  • Fixing cargo #2508 or any of the related issues. (Basically add an easy way to get the path to whatever cargo generated.) Yes, I know I can use --message-format=json, and no - I don't want to. I just want to run cargo, pipe all of the messages back to the user, grab the path to whatever file was generated and do something with it.
  • Stable procedural macros. Currently the js! macro is one, big, ugly hack, with ridiculously long compile times.
  • Procedural macros which do not tokenize the input and pass the input string exactly as it appears in the source code, e.g. the procedural macro is passed code: &str, filename: &str, line: u32, column: u32. Then we can use a full JavaScript parser on code, and easily report any errors since we know the first character from code is at filename:line:column.
  • The ability to use a custom test harness, or even a predefined harness which would support asynchronous code. Currently (AFAIK, please correct me if I'm wrong) it's not possible to test anything which is asynchronous. In native code this isn't really a problem since you can just block until the thing you're testing finishes, but you can't really do that here. For example, what I'd like is to be able to do something like this (to test setTimeout):
#[test]
fn test_set_timeout(done: Box<FnOnce(Result<(), Box<Error>>)>) {
    stdweb::set_timeout(|| { done(Ok()); }, 1);
}
  • Have cargo install Emscripten automatically so that I don't have to. I have some bastardized Docker build scripts to build Emscripten for Linux, but it's a pain to maintain and since I don't have the infrastructure I'm unable to provide packages for all of the platforms.
  • A way to easily mark a function to be exported to JS, and a way to process such functions at compile time so that the marshaling machinery can be automatically generated for them. (I haven't thought about this too much yet, so I'm not really sure yet how exactly it would work; just throwing it out there. This could probably be done with procedural macros, and a tiny bit of supporting code in the compiler which would export the symbol to the JS world.)
5 Likes

Inlining this comment from HN: Hi, I've been using Rust to WebAssembly very recently for a toy project [0] and ... | Hacker News

Hi, I've been using Rust to WebAssembly very recently for a toy project [0] and posted about it very recently on HN [1].

So my answers:

  1. I'm using both wasm and asm.js (installed using rustup) and it works for me extremely well, no weird bugs, very easy to set up.

    However I'm really missing a way to pass arguments to the linker, like "-s BINARYEN_METHOD='asmjs,native-wasm'" or "-s EMCC_DEBUG=1" or "-s DETERMINISTIC=1" or "-s SIMD=1" or "SIDE_MODULE=1"

    I think there is already a bug opened for this but I'm not able to find it on Google right now.

    From crate.io, I'm missing a N-dimensional array library (BLAS/LAPACK like) that compile to webassembly. There is ndarray[3], but its available BLAS dependencies are using fortran and therefore not compilable to wasm. Maybe thoses projects could help: emlapack[4], weblas[5]

    Also, I know it would be difficult to implement and would need some kind of better interaction with emscripten, but anything that makes it easier to pass and convert/cast object/arguments to and from javascript would be welcome. Right now, it requires a lot of boilerplate and unsafe code, mostly with structs [6].

  2. I'm using it and considers using it more, I love it, thank a lot to everyone working on it!

  3. Right now I'm deep into deep learning and I'm thinking about writting an implementation in Rust (that's why it would be nice to have a BLAS/LAPACK library). It could then be used on the Tensorflow playground[6] to showcase the technology.

[0] https://buddhabrot.paulg.fr

[1] Show HN: Buddhabrot explorer: A high-performance PWA using Rust and WebAssembly | Hacker News

[3] crates.io: Rust Package Registry

[4] GitHub - likr/emlapack: BLAS / LAPACK for JavaScript

[5] weblas - npm

[6] GitHub - PaulGrandperrin/rustybrot-web: Buddhabrot explorer as a Progressive Web App written in Rust compiled to WebAssembly...

[7] http://playground.tensorflow.org

It’s not at all related to Rust, but just for completeness I once implemented a radically different approach to emscripten for running compiled code in-browser. My main challenge was implementing the correct blocking/synchronous behaviour inside a browser, so I ended up writing an LLVM bitcode (*.bc) interpreter in javascript. Supports pre-emptive threading and a simple method-JIT that does something like emscripten-on-the-fly for functions that are called frequently and the VM can prove don’t block.

Here it is running ScummVM (a 20k-line highly portable C++ project), using only the existing ScummVM platform portability abstractions (implement file accesses as URL fetches, convert drawing primitives into canvas calls, etc).

I haven’t touched the code in many years, so it needs to be updated to the latest LLVM bitcode format (not something I’m looking forward to), it was written pre-wasm/asm.js, and I never really did much towards optimisation.

But my point was: I found this interpreted approach easier for porting codebases expecting a vastly different runtime model than the straight-to-javascript approach of emscripten, and with acceptable speed. Just food for thought.

9 Likes
#![feature(link_args)]
#[cfg_attr(target_arch="wasm32", link_args = "\
    -s TOTAL_MEMORY=268435456 \
    -s NO_EXIT_RUNTIME=1 \
")]
extern {}
2 Likes

This sounds similar to the Emterpreter, except yours uses LLVM bitcode directly whereas the Emterpreter goes all the way to asm.js and then translates that into a bytecode (plus generates an interpreter). It's a great option for easily porting blocking behaviour, and also supports calling into asm.js code for perf-critical parts. This makes yours an interesting trade-off - while it sacrifices performance initially, yours don't require a user to list the functions they want (or don't want) to be Emterpreted, they're just jitted if appropriate.

I'm curious - do you have any feel for how much slower your interpreter was than native? asm.js is commonly seen as 2x slower than native, and the Emterpreter is ~10x slower than that. Given that the Emterpreter translates from asm.js, it should be benefiting somewhat from the asm.js optimizer passes happening beforehand (e.g. register allocation), plus the interpreter itself being generated asm.js code.

Related, one thing we need big improvement on here is docs; see this thread on users:

(Plus if anyone from this thread has good resources, go help them out! :D)

2 Likes

@koute Re AMA: What’s the current state of stdweb and wasm? I’ve seen this unanswered issue, which says that at least one example works, though all my attempts to use stdweb with wasm failed.

A lot slower than compiled to native executable code (ie: no javascript). I was effectively emulating a 16MB RAM i486 at faster-than-realtime on a "decent laptop" circa 10y ago. I'd guess wildly that that's about 10-20x slower than running the same code compiled straight to amd64 executable. I have no idea how that compares to wasm/asmjs, since they didn't exist in more than blog-manifesto-form at the time.

Also: I did relatively little optimisation, other than using the javascript minimiser (which inlined trivial js function calls). My jit-generated javascript code was a naive translation of llvm basic blocks (each function was a loop over a switch(block_number) block - so I could do jumps between blocks), and I know emscripten moved on from that to (presumably) much faster constructs early in emscripten's history.

@koute Re AMA: What’s the current state of stdweb and wasm? I’ve seen this unanswered issue, which says that at least one example works, though all my attempts to use stdweb with wasm failed.

@konstin It should work. Install newest cargo-web (updated today), clone stdweb, go into examples/todomvc, type cargo web start --target-webasm-emscripten and visit http://localhost:8000. (With a caveat that cargo-web will take care of the Emscripten stuff for you only if you're running Linux; on any other OS you'll have to install Emscripten and Binaryen yourself.)

Just to make sure no one on this thread misses it, Alex has been working on a new wasm target:

3 Likes

Is it correct that there is no safe way to persist data between calls from JavaScript?

In a normal Rust program one can allocate some data structures in main, and pass them around as needed. But with a wasm target there is no persistent function, thus everything allocated normally will be cleaned up when returning the call from JavaScript.

It seems weird that I should have to resort to “unsafe” for such a simple and basic feature.

You can solve this by returning all the long living state to the JavaScript code that then manages it. You can do that by passing an opaque heap allocated pointer to javascript, that it can then pass back into the functions that need to access it. You can create such an opaque heap allocated pointer by using Box::into_raw(Box::new(obj)). Whenever JavaScript doesn’t need the data anymore, you also need to provide a free_my_opaque_ptr function that takes that opaque pointer and turns it back into a Box that then gets dropped Box::from_raw(ptr). Unfortunately there’s no way to tell JavaScript to automatically call that free function whenever the corresponding variable in JavaScript goes out of scope, so you are now in manual memory management mode in JS.

1 Like

As best I can tell I need unsafe to access the pointer when it gets passed back to the wasm module.

Between methods that require unsafe, declaring a static mut seems like the simplest, but then I need to litter my code with unsafe. Is there even anything unsafe about it in a single-threaded application?

Yes, that’s still unsafe. Imagine getting a shared reference to a value stored in a static mut Vec, and then calling some function and that then pushes into the Vec, causing it to reallocate. That’ll break the shared reference of the other function.

Thank you for the help, still took me a while to figure the details.

For reference, here is the cleanest static allocation I could come up with:

use std::mem;

struct Godobject{
	data:u64,
}

static mut GODPOINTER:isize=0;

fn main(){
	unsafe{
		GODPOINTER=mem::transmute(Box::into_raw(Box::new(Godobject{data:0})));
	}
}

#[no_mangle]
pub fn count()->f64{
	let rawgod:*mut Godobject=unsafe{mem::transmute(GODPOINTER)};
	let mut god=unsafe{Box::from_raw(rawgod)};
	
	(*god).data+=1;
	let res=(*god).data as f64;
	
	Box::into_raw(god);
	res
}

Having to write this kind of code is disastrous for beginners, and at least annoying to everyone else.

Why not just use RefCell and a regular static? (and/or lazy-static)