Elixir + [rust -> wasm] sample code?

I stand corrected then. I checked wasmex’s Cargo.toml file and it is using wasmer, which means it is indeed doing JIT compilation of the wasm instructions into native. Since that’s the case, you’re correct in that it’s just a couple of memory operations before and after the wasm call.

The claim of multiple of the number of instructions applies only to an “Elixir native” interpreter, where the Elixir code is reading each instruction out of the wasm bytes and doing the execution on demand.

1 Like

Thanks for doing the research. I think we’re converging towards ‘truth’ of:

rust-wasm cons:

  • memcopy overhead (might be trivial outside of HFT)
  • JIT overhead
  • 4GB per limit

rust-wasm pros:

  • easier hot swapping (as you stated)
  • crashes only wasm runtime

Given the inevitable memcopy overhead, I wander if it makes sense for something like:

  • rust/wasm on wasmer in a separate process
  • rust/wasm implements the Erlang “port” (or whatever distributed erlang uses to send terms)
  • elixir talks to rust/wasm just like any other distributed node

As an aside, I think the 4GB limit might be a much bigger problem. I don’t know if the typical server with 512 GB RAM also has 128 threads – because if not, there might be quite a bit of OS context switching of the wasm runtimes (unless a single process can host multiple wasm runtimes).

JIT overhead is essentially your “cold start” penalty. In my experience using wasmer, it’s typically less than a second for anything but the fattest of wasm files.

I’m not sure what you mean by the 4GB limit… but a single wasm module shouldn’t be maintaining much state at all, let alone 4GB (this is where I’d recommend state be managed outside the wasm module).

I would recommend that you put the wasmex executing code inside something like a Genserver so that it can queue up requests to it in single-threaded fashion (since the wasm module internally is single-threaded), and so that it can die without hurting your system. You can have literally millions of OTP processes without exceeding your OS thread limit.

Put another way, you have a single Elixir OTP application that can host millions of OTP processes, some of which can be wrappers around a wasmex instance (the module is basically the “state token” for that process). Your module-wrapping GenServer would then handle an incoming message by extracting parameters, invoking an exported function on the module, and replying accordingly.

I don’t know the right answer to the following problem because I have not solved it yet and I have not found any good articles on it yet either (but now I am starting to see why you brought up the ‘memcopy’ bottleneck earlier).

Suppose you are building a distributed sharded game server, the server side of something like Minecraft / Fortnite / Quake / …

Would you:

  1. store ‘truth’ in Elixir, then, on every tick, have rust/wasm code grab the current world state, run one step of simulation, and write world data back out to elixir OR

  2. store ‘truth’ in Rust, and Elixir merely serves as a ‘router’ routing user input to the rust/wasm code and routing state (user location, health, …) back to the client ?

In model 1, we’re going to have memcpy’s everywhere (far more than what I originally anticipated, but I now also understand your concern).

In model 2, it’s not hard for a single shard to hit 4GB very quickly.

  1. Storing the truth, as some data structure, and then invoking the tick of a game loop in wasm means Elixir holds the memory, which means you control the sharing of it and have a higher limit.
  2. If the truth is stateful inside Rust then it becomes stateful inside wasm as part of the compilation process, and now your wasm module could easily run afoul of the page limit of the host runtime.

When you go with option 1, you basically run into the following rule: you must be able to serialize your input, invoke your function, and de-serialize the output in less than your maximum frame elapsed time budget (the inverse of your frame rate). The good news is that a server-side frame rate can be less than a client-side because theoretically you’re not modeling ultra-low-latency things like particle fountains and projectiles. The bad news is you still have that per-frame budget you can’t exceed without causing lag.

One more thought on option 2: this is where people typically decide they need to employ sharding techniques. If the Rust/wasm module doesn’t take a copy of the entire world/universe, and is treated like a pure function and only acts on the subset of the world it needs, then you can run thousands of instances of that wasm module and allow OTP to do load balancing/distribution for you.


If you’re actually describing running a game loop at n FPS on the server-side in the cloud where the game logic is managed inside a WebAssembly module, then I feel like I should let you know that I’m working on a distributed ECS that uses my wasmCloud WebAssembly actor framework, where the actors you write are systems and then the host runtime takes care of components and entities. This is still mostly on paper, but I’ve built a prototype of it once before. Fun retrospective on my earlier prototype here.

2 Likes

This is really interesting. Obvious in retrospect, but I never considered that it is perfectly fine to have server side ‘frame rate’ != client side frame rate.

It is interesting that for your ‘radar problem’, you solved it via an algorithmic change rather than an O(1) ‘faster language’ change.

I need to spend more time looking at wasmCloud. This looks very interesting, but is also so different from my current mindset that I don’t grok it yet. Is wasmCloud tackling the same domain as GitHub - lunatic-solutions/lunatic: Lunatic is an Erlang inspired runtime for WebAssembly ? If not, how do they differ ?

Hi, author of wasmex (the wasmer wrapper you discussed) here :wave:

I just stumbled over your discussion. I am not sure how I can best help, but if you have any specific questions, feel free to ask :slight_smile: And, of course, if you find there are missing features please open a ticket on the repo.

I did not personally benchmark the speed of wasmex yet, but that is one of the things I really wish to do (or see someone else do).

For your data-passing-bottleneck problem: You could also store your world state in the wasm instances memory (readable from both, elixir and rust). This is still faster for rust to access, but I imagine that elixir-updates to an existing world view in rust-memory are smaller than passing the whole state in for every call.

This really depends on the actual data you want to pass around. Tooling for memory manipulation is far from being as good as with wasm-bindgen. But I’m very open to PRs in that direction.

In short, wasmex is what you describe (in an earlier post) as “elixir + rust/x86_64 via NIF / Rustler”.

2 Likes

Thanks for all your hard work! :slight_smile:

1 Like

Both of the frameworks are using WebAssembly to try and solve the problem of making it easy to build distributed applications. wasmCloud does this with an aim toward stripping away boilerplate, loosely coupling capability providers (non-functional requirements), and providing cryptographically secure modules so that you can control what the actors can and cannot access. From what I can tell of lunatic, it is more “OTP-like”, with explicit use of channel senders and receivers. wasmCloud is designed for extensibility and polyglot, supporting actors in TinyGo, Rust, and AssemblyScript and it looks like lunatic’s SDK is specific to Rust.

Would it be correct to say: stated another way, if you had full control over (1) choice of language and (2) choice of libraries your applications, then wasmCloud serves as a "docker replacement’ or sorts – instead of packaging an entire Linux image to throw on AWS EKS/ECS/Fargate, you can just build wasm binary (because you choose language + libraries that can target wasm), and you just provide a tiny wasm instead of a hunderds-of-MBs docker image ?

PS: For anyone reading this thread and unaware (like myself until a few minutes ago), @autodidaddict , as stated in his public elixir forum profile, is the author of the Rust/wasm book “Programming WebAssembly in Rust”.

No wonder you are pushing the boundaries of wasm. :slight_smile:

Really appreciate all the time you took to help me work through this design space.

1 Like

I don’t want to drag you into a flamewar or have you offend either party – but could you briefly summarize you choice of wasmer over wasmtime? I have played with both APIs slightly (as a Rust dependency), read a few github / reddit comments, understand that wasmtime seems very well integrated with Mozilla / cranelift team. But I really can’t figure out how they differ / when to use which.

For you, was this a flip of a coin, or were there technical differences that caused you to pick wasmer for wasmex ?

I prefer to think of wasm as the next evolution beyond docker, but you’re correct on all counts.

I haven’t benchmarked the two of them, so I can’t say one is faster than the other. My experience with wasmer was that the API seemed slightly higher level, and a little bit easier to use than the wasmtime API. Other than that I didn’t see much difference.

I feel that our communities (rust, elixir) aren’t too much into flamewars :crossed_fingers:It’s a super legitimate and good question.

It was really close between the two. I was mainly looking for the following traits:

  • maintainability (stable API, backed by a stable team)
  • speed of wasm execution
  • community adoption and other libraries/code to learn from

maintainability / team

  • wasmer and wasmtime both are pretty stable api-wise but still follow recent wasm developments
  • both are backed by a company that does open source, wasmtime seemed to be closer to the rust core team – seen from a 5000km distance – due to both being connected to mozilla
  • both projects seem to be in for the long run

speed of execution

There were many benchmarks flying around and it was a close call. At the day of decision wasmer was faster. But both were close enough of each other to make them practically indistinguishable for my use case.

community adoption

I was mainly looking for other programming language integrations. Wasmer had a ton of existing integrations to other host languages easy to find.

In the end, the existing language integrations of wasmer (in the hope to borrow some ideas) was the point that convinced me. To be honest, there was a timeframe some month ago were I was worried wasmer lost steam (compared to wasmtime) according to the number of commits/versions relreased. I was short before migrating to wasmtime.

Turned out that wasmer was secretly working on a rewrite which reduced the number of publicly visible commits. I wish that was communicated more clear and that their plan for their implementation was in the open. But things improved: they invited their community (we organize in a slack channel) to an early preview of their rewrite. It was relatively easy to upgrade to wasmer 1.0 (the rewrite) even before the official release. They listened a lot to feedback and were super helpful in answering questions or implementing fixes/helpers once one of us got stuck. We get frequent updates again and I’m happy.

Now, that I’m a little more connected to wasmer and know the devs behind it, their friendliness and support counts as another plus point for me :slight_smile:

1 Like

Thank you for your detailed reply. I’m new to wasmex, please bear with these trivial questions.

Can you verify if the following statements are correct:

wasmex allows wasm32 code to call arbitrary Elixir functions

imports = %{
  env: %{
    sum3: {:fn, [:i32, :i32, :i32], [:i32], fn (_context, a, b, c) -> a + b + c end},
  }
}
instance = start_supervised!({Wasmex, %{bytes: @import_test_bytes, imports: imports}})

{:ok, [6]} = Wasmex.call_function(instance, "use_the_imported_sum_fn", [1, 2, 3])

What is happening above is:

(1) we define a function in elixir (a + b + c)
(2) we can pass this fuhction to the wasm32 env
(3) when wasm32 invokes this function, the corresponding elixir code gets called

wasmex allows Elixir to read/write to wasm32 linear memory directly

{:ok, memory} = Wasmex.memory(instance, :uint8, 0)
index = 42
string = "hello, world"
Wasmex.Memory.write_binary(memory, index, string)

Here we are writing directly into the wasm32 memory, at index 42.

can we do dynamic code generation using wasmex ?

Ignoring Rust for a moment, and thinking in pure Elixir.

If I have an Elixir module that generates *.wast (the s-exp text format, not the binary format) code, can I load it in wasmex and execute it? The opens another route for doing dynamic code gen in Elixir, where for specialized routines, we can generate *.wast, copy the data into the linear memory, execute, and copy back. Is this possible?

No worries, really. I am happy to help :slight_smile:

The short answer is: Yes, this is what happens.
The long answer is, that actually a little more happens (but you may not be interested in these details):

(1) we define a function in elixir (a + b + c)
(2) we can pass this function to the wasm32 env
(3) wasmex wraps the elixir function in a rust-wrapper function and passes it to wasm/wasmer
(4) we start a new wasmex GenServer (line 6 in your example code)
(5) the wasm part wants to call the sum3 function, our rust-wrapper function is called
(6) the rust-wrapper sends a message to the wasmex GenServer asking it to call the elixir function and waits for results
(7) the GenServer finds the elixir function in the imports-map, calls it in elixir land, and returns the result back to rust-land
(8) the wrapper function takes the function return values and converts them to wasm values and gives them back to WASM

If you want even more details, you can have a look at the PR implementing this feature Support imported functions by tessi · Pull Request #9 · tessi/wasmex · GitHub

yes.
If you want you can directly manipulate bytes or pass arbitrary data (see this other person asking about passing data to wasm)

I have tested (and support) compiled wasm modules for wasmex only at the moment. But after looking at the wasmer docs, it seems they support WAT files. I found references that they use WAST files for internal testing, which gives me hope that WAST may just work.

So, it seems that wasmex may just be missing some tooling/config to support wat/wast. I’m very open for PRs or issues tracking this :slight_smile:

Very minor nitpick: credit for the code goes to the wasmex documentation I copied/pasted from. :slight_smile:

Thanks for your detailed & insightful response. I think I get the basics of wasmex now. Thanks again for your hard work in making this possible.

1 Like

Sorry to bother you, I’m currently learning the interaction between wasm and elixir. I am writing wasm in rust language, but it can only return the starting position of the address of the string, which is *const u8. I can’t get the length. What is the solution? Thanks again.

Hey @echojoys - apologies for my late answer. But I have good news! Back when you asked this question the Rust/WASM ecosystem wasn’t able to support multi-value returns yet. And we’d need multi-value returns to return the string address and length at the same tine.

This changed, however. Have a look at Return string form rust in Wasm - #11 by tessi where I tell you in another thread about the good news that Wasmex will ship with multi-value returns soon.