So, you want to get a native Rust application running on the web with input and rendering? This post will go through the steps needed to achieve that by using Emscripten to build for the wasm32-unknown-emscripten target. For the purpose of this guide, I will assume that you already have an application running on desktop that uses SDL2 with OpenGL and I will not go through the code needed to get to that point. If you’re starting from scratch, check out the example project here.

As a bonus, your code will still work for desktop and choosing your build target will be just a matter of switching the --target= flag. Alternatively cargo web can be used to invoke the web builds.

Up to date

As of writing this, there is a lack of up to date information on this topic online. The articles/tutorials I’ve found are often out of date to the point where you run into obscure errors that require in-depth knowledge to even know where things went wrong. My goal is to turn this post into a resource that is kept up to date as the ecosystem matures to always work as a step-by-step guide for the experienced and newcomers alike. There is also an example project accompanied with this article which will serve as a fully working code example.

This github repository contains the example project and also serves as a place to raise issues if this article goes out of date, to suggest improvements or simply just to ask questions.

As a token of how up to date this article is, I will leave a time stamp below on when it was last known to work using the 3 major platforms. This should probably be automated in the future with CI.

Last tested

  Plain Cargo Cargo Web
Windows 8/10/2020 8/10/2020
MacOS Untested Untested
Linux 8/10/2020 Broken: #2

I aim to have these dates be no older than 6 months. If they are, feel free to raise an issue in the github repo.

Step by step guide

Step 0 - Knowledge briefing

Since this post is primarily about getting things up and running on the web, I won’t go into what SDL2 nor OpenGL are. I assume you already know what role they play and how to set up an application that uses them. This post will help you take that application to the web platform. The target triplet we will use is wasm32-unknown-emscripten so let’s go through wasm32 and Emscripten.

Wasm32

Wasm32 is the WebAssembly architecture. WebAssembly is a binary-code format that can be portably executed, designed for browsers. It’s an alternative to browsers executing other scripts, such as JavaScript and offers a more compact and performant way to run code in the browser. Comparable to how we need to compile our Rust application into x86 for desktop so that the CPU can execute it, we need to compile it into Wasm32 for the web so that the browser can execute it.

Why Emscripten?

There is also the more barebones target wasm32-unknown-unknown. Why can’t we use that one, and what does Emscripten bring to the table?

Well, we can run Rust on the web without Emcsripten by using that target, and in fact, some people prefer that route. Myself, I prefer to use Emscripten because it comes with a lot of functionality that helps bridge the gap between desktop and the web platform. If we go by the analogy that our Wasm32 compiled program is the machine code, and the browser environment is the hardware to execute it, then Emscripten could kind of be viewed as the operating system that provides the system features we are used to having out of the box. If you would go by plain wasm32-unknown-unknown then a lot of things we take for granted, like being able to use std::fs to load files would be missing. I’ll go into these features in more depth later in the guide but for now let’s list some of the functionality we get by using Emscripten.

  • SDL2 (this is the killer feature!)
  • OpenGL
  • File IO
  • Sockets
  • Multithreading

Most of these are an integral part to visual desktop applications which is why I prefer to use Emscripten over the barebones approach - I would have to re-invent a lot of those things anyway! There are however other helpful tools if you do want to go the barebones approach but that’s for another article.

Cargo Web

cargo-web is another helpful tool that I will show you how to use. It’s a helper for cargo that makes the development process a bit easier. It comes with handy commands for both producing web builds as well as starting a local webserver for testing without having to upload the files to a real webserver. It also provides a neat config file where we can pass flags to Emscripten. I’m including cargo web in this article since it’s a nice thing to have, but I’ll also show you how to go without it since it sadly hasn’t been maintained for over a year which makes it a risky tool to rely on.

If you want to try it out, you can install it globally with cargo install cargo-web. This gives you the ability to directly build for the web targets by using the command cargo web build (optionally with --release like usual), and you can also use cargo web start to launch your application for testing using a localhost web server which is really handy. It will even rebuild on the fly as you change your code files! There’s also the cargo web deploy command which will bundle your project inside target/deploy all ready to be uploaded to a web server. Cool!

Step 1 - Setting up Emscripten

The first thing you need to do is to download and setup the Emscripten SDK which is pretty straightforward. The official docs have great instructions on how to do this and should be your main resource. I won’t go through any platform specific issues you might run into (again, refer to the official docs) but in short, the process will be something like the following.

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install 1.39.20
./emsdk activate 1.39.20
source ./emsdk_env.sh

Note that we prefer to use version 1.39.20 of Emscripten since the LLVM version it uses most closely matches the LLVM version used by Rust (as of 1.47.0). Other versions might work as well though.

That last command will set up your terminal environment so that it is all ready to use with Emscripten, and this step is the only one you’ll need to repeat for your next coding session. I personally do this in a terminal instance in VS Code since that is my editor of choice, but any terminal will do.

Rustup

Secondly, you need to install the build target for your local Rust installation. Simply running rustup target add wasm32-unknown-emscripten should be enough.

You are now ready to use cargo with --target=wasm32-unknown-emscripten to compile your code for the web! In fact it’d be a good idea to try that now to make sure that your Emscripten install is functional. You can create a new hello world project with cargo new emscripten_test and run cargo build --target=wasm32-unknown-emscripten inside that directory. You should end up with a pair of .js and .wasm files inside of target/wasm32-unknown-emscripten/debug. The .wasm file will contain the binary code of your application while the .js file contains a lot of scaffolding and setup code provided by Emscripten.

By comparison you can try the wasm32-unknown-unknown target and you’ll see that you only get the .wasm part so it’s a lot more barebones indeed!

index.html

The above compilation does not provide us with an index.html so we don’t actually have a way to load and run the application as a web page. This can be sorted in two ways - either let a tool generate an index.html for you, or provide one yourself. I like to provide one myself but I often put it together by looking at the ones that have been autogenerated. For a usable one for our purposes, have a look at the one in the example project. Remember to change the <script src="rust_sdl2_opengl_emscripten.js"></script> to whatever your project is called. You also need to make sure that both the compiled .js and .wasm files and also the index.html file are accessible next to each other on whatever web server you use.

Building with rustc without Cargo makes it possible to autogenerate a html file by using the -o flag such as rustc --target=wasm32-unknown-emscripten emscripten_test.rs -o index.html and you can also let cargo web generate one for you (although I have had bad luck with this).

You can use cargo web to deploy your application by running cargo web deploy --release. It will then deploy all files needed to run your application into target/deploy/, including an index.html file. By default, it will auto-generate one for you but if you want to provide your own you can put it in static/index.html and it will instead use that one. At the time of writing this, for me, the autogenerated index.html file did not work. I have not investigated why, but it might be a bug in cargo web. This is one of the reasons why I provide my own index.html in the example project.

Step 2 - SDL2

This is the killer feature of Emscripten for me: it comes with a port of the SDL2 API with the browser as the backend, which is very awesome since it means that we can use SDL2 in our code normally and then just slap on the -s USE_SDL=2 flag and boom, Emscripten makes it work. This means that when you create an SDL2 window, Emscripten will create an HTML canvas on the website that represents the SDL2 window as well as grab any user input like mouse/keyboard and route those to your application in the form of normal SDL2 events. Also, by using SDL2 to create a window with an OpenGL context, we will also get a corresponding WebGL context. As you can imagine, this is incredibly helpful when we want to be able to easily build for both desktop and web since we basically don’t have to do anything at all to get windowing and input to work.

If you would now build your application with Emscripten, you would get linker errors for SDL2. The reason why is because we haven’t actually told Emscripten to use SDL2 so those calls will be left unlinked. Fortunately, this is easy to fix by supplying Emscripten with the linker flag -s USE_SDL=2. But wait, how do we pass this flag to Emscripten, and how do we avoid passing that flag when building for desktop? We can do this either through cargo web or just standard cargo. As both are done through local config files, I usually do it for both of them so that the person building the project can choose to use either cargo web or just plain cargo however they prefer.

Using cargo web

Create the cargo web config file in <your-project>/Web.toml and fill it with the following.

default-target = "wasm32-unknown-emscripten"

[cargo-web]
minimum-version = "0.6.0"

[target.emscripten]
link-args = [
  "-sUSE_SDL=2",
]

This should be mostly self explanatory. The first two sections specifies the default target to use when invoking builds from cargo web, as well as deciding what the minimum version requirement is. The last part is the relevant one as it lets us list the linker flags to pass on when Emscripten is targeted. That’s where we put the -s USE_SDL=2 flag and we will add more things here later on in this guide.

Using standard cargo

The process of supplying the flags is similar without cargo web, we just put it in a different config file. This time, it’s a config file for cargo itself that we have to create in .cargo/config. Put the following into it.

[target.wasm32-unknown-emscripten]
rustflags = [
    "-C", "link-arg=-sUSE_SDL=2",
]

As you can see it looks very similar to the cargo web approach, except a bit more verbose. The principle however is the same, we are specifying linker flags for only the emscripten build target.

SDL2 should now be all set and ready to go.

Step 3 - OpenGL

This is another browser intricacy. On desktop we can quite freely choose which OpenGL version to target (for example OpenGL 4.3, or 3.3 etc), but the browsers actually don’t support any of those! The only two OpenGL versions we have to choose from are WebGL 1.0 and WebGL 2.0, and these are a lot more trimmed down than what we might be used to. Fortunately Emscripten is pretty helpful here as well, as it lets us write code against OpenGL as if we are targetting the equivalent desktop/embedded OpenGL versions while the browser is actually receiving WebGL calls. Again very helpful since it minimises the need to change our code and maintain a separate desktop VS web code structure, instead we can just code away as if we are only targetting desktop. There’s a caveat though.

Version compatibility

Even though Emscripten lets you use WebGL without directly creating a WebGL context, there’s only so much it can do. You will still need to use an OpenGL version that is very close to the WebGL equivalent, otherwise Emscripten will not manage to bridge the gap. The full documentation is available in the emscripten docs but I will go through the approach that I use.

I like to target WebGL 2.0, which is equivalent to OpenGL ES 3.0 and OpenGL 4.3 Core. In practice this means that as long as I limit my OpenGL usage to what is available in OpenGL 4.3 Core, I can just tell Emscripten to create a WebGL 2.0 context and it will work.

In the case that you are using a higher OpenGL version than OpenGL 4.3, then you will need to either rewrite the code to target the lower version, or write alternative fallback code that is used when you run on the web.

GL Flags

Like when enabling SDL2 we need to supply flags to Emscripten to tell it how we want to use OpenGL. Which flags we use depends on which version we decided on. Like SDL2 flags, these are also linker flags and are supplied in the same way.

WebGL 2.0

If we want to do my recommended setup of using OpenGL directly compatible with WebGL 2.0 then we can supply the flags -s MIN_WEBGL_VERSION=2 and -s MAX_WEBGL_VERSION=2. That way we just tell Emscripten to create a WebGL 2.0 context, and we can adhere to what is available in that GL version (i.e. compatible with 4.3 Core and OpenGL ES 3) and it will just work.

OpenGL ES 2 and 3 Emulation

Emscripten has modes to emulate some of the features that are lacking in WebGL 1.0 but exist in ES 2 and 3. This is mainly regarding the client-side array rendering methods (e.g. glDrawArrays from client data and the like). These are enabled through the flags -s FULL_ES2=1 and -s FULL_ES3=1 respectively. Usually you won’t need to do this unless your application uses those particular OpenGL features and you don’t want to use WebGL 2.0.

Further Options

There are some further capabilities like limited emulation of old-style OpenGL 1.x code and other things but that is out of scope for this article. Consult the docs if you are interested.

OpenGL Library

Aside from the Emcsripten/building side we also need a way to call OpenGL functions through code. I like to use the gl_generator crate for this but you can use whatever option you want. As long as it uses OpenGL underneath and it targets the right OpenGL version, it doesn’t matter. If you want to see how my OpenGL setup looks like, head over to the example project and check out the source code.

Step 4 - File Handling

Many applications need to load files from disk, like sprite sheets, tilemaps, models and so on. On desktop you can do that by just dumping the files in a folder next to the executable, but this proves difficult when running through the browser. Where would you put the files? Next to the .js files on the server? That will not work to load from the user’s client-side browser!

Luckily, Emscripten provides a virtual file system that lets you embed local files during compile time that you can then load using the standard std::fs facilities. To clarify, this means that the asset files will be embedded in the produced .js file which Emscripten will then present to your code as if it was a local filesystem. In practical terms, the only thing you need to do to be able to load your files is to use yet another Emscripten flag! This time the flag is --embed-file <file-or-folder>, which can be given several times if needed. There is also the similar --preload-file <file-or-folder> flag which instead of embedding the files in the .js file will create a separate .data file with the data that can be loaded more efficiently. As always, check the official docs for more in-depth information.

Step 5 - Main Loop

At this point you might be able to successfully compile and run your program on the web without errors in the console but if you’re making a game or similar you’ll probably be faced with a frozen tab that is eventually killed by the browser.

The reason for this is that your game probably has some form of main loop that is executed over and over as long as the game is running. This is appropriate for desktop apps but when it’s running in a tab it is going to hog the browser’s execution engine and the browser is going to treat the tab as having gotten stuck in an endless loop and kill it.

emscripten_set_main_loop

The Emscripten API is equipped to fix this. The relevant function is called emscripten_set_main_loop and the idea behind it is that you pass a function that you want to be called repeatedly. Emscripten will then schedule this function to be run by the browser at a regular time interval (often synced with the browser’s render frequency). By passing in a function that runs one iteration of your main loop, the scheduling will effectively result in your looping logic being executed over and over again. This is the closest you can get to a main loop in the browser environment.

There are several considerations here though! Firsly, this means that you cannot have your code arranged in a loop statement anymore. Furthermore, invoking this function might lead to unintuitive behaviour that depends on the last parameter passed. If you pass false, then execution will continue straight away past the function call in a non-blocking manner, and potentially run to the end of the scope and drop local variables and so on. If you instead pass true, the Emscripten will abruptly terminate the current code-path execution without dropping any local variables. This means that even when the looping stops, none of the remaining code past the invocation of emscripten_set_main_loop will be run.

Abstracted main loop

Given that looping looks quite different in our web build compared to desktop and that we still want it to work on both transparently, we need an abstraction that takes care of the differences in implementation. Since this is an isolated but common thing for Emscripten builds, I turned it into a crate called emscripten_main_loop that you can use. For the sake of completion I’ll show you how to use it below, but if you prefer to see it in action you can just head over to the example code repository.

pub enum MainLoopEvent {
    Continue,
    Quit,
}

pub trait MainLoop {
    fn main_loop(&mut self) -> MainLoopEvent;
}

This trait defines a data object capable of running a main loop. For example, in my projects it is usually some kind of Application or Game struct that holds the entire game state and systems. The main_loop function is implemented to run a single iteration of your main loop. Usually if you previously had your code in a while(!quit){...} or similar, you can just chuck all the contents of that loop block into this function. Make sure to return either Continue or Quit as appropriate.

To start the looping the crate provides the function run. In other words, if you implemented the MainLoop trait for a Game struct, you would then call this run function and pass that game struct into it, which would start the looping and run it over and over until Quit is returned from the main_loop function.

Internally, this is implemented as a standard while(!quit){...} on desktop and for web builds it will capture the struct and store it in global memory while scheduling Emcsripten to run the loop. Easy. If you are wondering how this is done behind the scenes, head over to the source and have a look and let me know if you see ways that it can be improved.

Phew

That was a lot wasn’t it? Hopefully those steps will help you get past the most common hurdles and give you something up and running. There is however a lot more that can go wrong than what this post could ever cover and debugging these things can be difficult. Luckily, the Emscripten documentation is really good and the folks involved are also very friendly and helpful.

I want this post to be as useful as possible so if you feel that something important is missing, or have ideas on how things could be improved I’m more than happy for you to either leave a comment here or head over to the repository to submit an issue.

Extra Considerations

As a bonus, I’ll include a few considerations for other topics that might be relevant to some of your projects. Note that I’ll only touch briefly on these and if you want more in-depth information you should check out the official Emscripten docs. The information below only aims to give you an overview.

Networking

Networking from web code is tricky since browsers are quite restrictive in what you can do - you cannot just send plain TCP/UDP data from an application running in the browser. Emscripten can however emulate TCP sockets using WebSockets, but this still doesn’t mean you can connect to any TCP server since WebSockets utilise a different protocol. It does mean that you can connect two instances of your application in the web with each other, but you cannot automatically connect to say, a backend server that expects TCP connections. To be able to do that, the server needs to support the WebSocket protocol.

Networking is complex and also quite project specific so I won’t go into it any further than that.

Multithreading

Another thing that is tricky due to browser restrictions is multithreading. To get the kind of multithreading we are used to in desktop applications, we need shared memory between the threads. The kind of memory that is usually protected with atomics/mutexes and similar control structures. Modern browsers do support this, but due to security issues it is not enabled by default in all browsers (currently only in Firefox and Chrome). This means that your multithreaded application won’t work unless the end user explicitly enables it in their browsers.

Multithreading with non-shared memory is supported using web workers and Emscripten does have an API to interact with them but you can’t expect them to be a drop-in replacement to standard threading code since you cannot share any memory between the web workers or the main thread.

All in all, unless you specifically really need your code multithreaded, you’re probably better off making your web version single threaded until all browsers support shared memory out of the box.