Actually Running C++ on Cloudflare WebAssembly
WebAssembly (WASM) has been available on Cloudflare (CF) since late 2018. How usable this is in practice depends on what you're trying to do.
My goal was to put online a small subset of functionality from Saus.
Given that Saus otherwise runs fully-locally, I wanted to minimize the complexity added by service hosting. This motivated the use of existing C++ code and tooling with as few changes as possible, to not fragment logic across C++ and JS.
My motivation to try and deploy a Cloudflare Worker instead of a container on AWS/GCP/etc comes from a decade of experience doing the latter, and the insane complexity these platforms now normalize.
And it was a barely-good-enough excuse to learn about WASM (hey it worked out well for this guy 🤷♂️).
Status Quo
Cloudflare has some tempting demos of deploying self-contained C/C++ to WASM, but if you want to communicate with the outside world, things quickly get trickier.
Rust enjoys 1st-party support built upon wasm-bindgen - C++ not so much. Thankfully the wonderful/crazy Emscripten plugs that gap - let's get started.
Standalone WASM
At the outset, a standalone WASM executable seemed the natural, ideal goal. This means compiling against WASI (Web Assembly System Interface) system calls, avoiding the need to manually go through JS to reach the outside world.
However while many runtimes support the WASI sockets API needed for networking, I found little mention of Cloudflare's WASI support. A public repo for it is over 3 years stale. Brief investigation indicated this to be the "current" state, with no support for sockets, so I did not dig further. If you do have WASI available, that's probably the way to try and go.
"Bridging"
With WASI networking off the menu, we must bridge through the JS host runtime in order to reach the outside from our C++. For example, if we want to make an HTTP request, we need to somehow invoke JS' fetch()
from our C++. We'll need to pass the request from C++ to JS, and the response back the other way.
And don't forget the asynchronous nature of all things JS.
Emscripten
Emscripten is a fascinating project, that has embraced and spurred on WASM despite long predating it.
That adaptability has its downsides: Its documentation, interfaces, and implementation have all the warts you would expect of project built atop over a decade of shifting sands; it's a wonder it exists at all.
Approaching it afresh in 2025 is an archaeological expedition to determine what advice remains relevant and what does not.
The Menu
Emscripten gives you many ways to bridge between C++ and JS, all of which can be mixed and matched:
Inline JS
The EM_JS
and EM_ASM
macros are the Siren call for anyone new to Emscripten. Write your JS right there in your C/C++ with a simple interface between them:
EM_JS ( void , doTheThingInJs , ( const char * str ) , {
console . log ( ' Hello ' + UTF8ToString ( str ) ) ;
} ) ;
doTheThingInJs ( " world " ) ;
Going back the other way is also simple: Use the -sEXPORTED_FUNCTIONS
linker option to expose your C/C++ to your JS. Emscripten kindly creates a JS binding based on the function signature.
What more could you want?
Besides the jankiness of maintaining that inline JS and the fun you'll have getting syntax highlighting from your editor, you'll also have to manually handle the interop between the two runtimes. This includes managing memory on the shared heap, and if you're using strings, probably going back-and-forth between UTF-8 and JS' native UTF-16. And don't forget C++ name mangling (but see this handy workaround).
This is more of a C solution than a C++ one.
Enter Embind
Embind gives you ready-made support for exchanging many common types between C++ and JS. You can even expose complete class definitions and spend much less time thinking about memory.
std :: string doTheThingInCpp ( std :: string who ) {
return std :: string ( " Hello " ) . append ( who ) ;
}
EMSCRIPTEN_BINDINGS ( module ) {
emscripten :: function ( " doTheThingInCpp " , & doTheThingInCpp ) ;
}
This improves calling into C++ from JS, but what about the reverse, so far provided by EM_JS
/EM_ASM
?
Transliteration
In my opinion, val.h
is Emscripten's most under-represented feature and should really be where you are advised to start. While EM_JS
/EM_ASM
have that appealing turn-key-ness, transliterating your JS into C++ using emscripten::val
provides a more robust and maintainable way to bridge to the outside world:
using namespace emscripten ;
val fetch = val :: global ( " fetch " ) ;
val options = val :: object ( ) ;
options . set ( " method " , " POST " ) ;
options . set ( " body " , " {...} " ) ;
val response = fetch ( " http://example.org/ " , options ) ;
But fetch()
is async, so our response
var is a JS Promise
. How do we suspend our C++ and resume it once the the promise completes?
JSPI
The JavaScript Promise Integration API has been recently introduced to simplify the interop between synchronous WASM and an asynchronous host. I didn't manage to get it working using node's --experimental-wasm-jspi
flag (you should presume user error), but the point is moot because it's not supported in CF's worker anyway. On to the next one.
Asyncify
Asyncify is Emscripten's long-standing solution for async interop. In theory it can be as easy as trading your EM_JS
for an EM_ASYNC_JS
. In practice, my goal of making as few changes as possible to existing code meant retaining the -pthread
compile flag, even if threads weren't actually going to be used within WASM.
Asyncify comes with caveats, but most weren't a concern. However enabling both pthread support and Asyncify seemed to trigger all sorts of hazards, and I dug through years of valuable tickets (thank you to everyone who ever documented their problems and solutions).
Reentrancy was also going to be both a present and future foot-gun.
If there hadn't been another way, I might have invested the time required to compile without -pthread
, or more likely just canned this idea altogether. Thankfully a better solution emerged:
Coroutines
Saus is built using coroutines from the ground up. While I love working with coroutines as an abstraction, the excellent concurrencpp library had largely shielded me from the specifics of C++'s implementation.
Unfortunately that would have to change.
Though it receives no mention in Emscripten's Asynchronous Code documentation, the co_await
operator was lurking at the bottom of my beloved val.h
.
@RReverser had implemented a lovely little shim that bound the C++ coroutine mechanics to the resolve and reject callbacks of the JS Promise
. This means we can co_await
any emscripten::val
that references a JS Promise
, and receive its value when it completes.
There were some minor niggles to get it to play nice with concurrencpp, for which I had to bite the bullet and learn how C++ coroutines actually work. But in the end, only a small change was needed (PR to come!).
Now we can await coroutines at our JS -> C++
entry point and C++ -> JS
exit point, and everything behaves as expected - no Asyncify or JSPI required!
using namespace emscripten ;
val fetchTheThing ( ) {
val fetch = val :: global ( " fetch " ) ;
val response = co_await fetch ( " https://saus.app/ " ) ;
val body = co_await response . call < val > ( " text " ) ;
co_return body ;
}
EMSCRIPTEN_BINDINGS ( module ) {
function ( " fetchTheThing " , & fetchTheThing ) ;
}
Our promise effectively propagates through, so our JS can await our exposed C++ function as if it were any other async JS function:
const htmlBodyStr = await WasmInstance . fetchTheThing ( ) ;
Lovely.
The Gotchas
After trawling through the Emscripten scriptures, I had arrived at my holy grail: Embind + Transliteration + Coroutines
Time to deploy? Not so fast. Cloudflare has a few more hurdles to throw at us:
Environments
Emscripten's output runs in a range of environments, from browsers to servers to web-workers. Unfortunately the CF worker runtime is a-little-bit-none of these, and Emscripten's auto-detect logic can mistakenly identify it as node
, leading to crashes when it turns out to be not-node
.
Explicitly setting the -sENVIRONMENT=worker
linker flag resolves this by disabling any auto-detection.
✔ No more module loading errors
Dynamic Code Evaluation
Now attempting to run your Emscripten-compiled program within the CF worker runtime still won't get you very far because:
For security reasons, the Cloudflare Workers runtime does not allow dynamic code evaluation via
eval()
ornew Function()
.
Additionally, WebAssembly.compile
, WebAssembly.compileStreaming
, WebAssembly.instantiate
(with a buffer parameter), and WebAssembly.instantiateStreaming
are also not supported.
This breaks two rather important things:
- How Emscripten binds your C++ and JS.
- How Emscripten instantiates your WASM compiled code.
Thankfully, the same restrictions have existed in previous execution environments, and Emscripten handily provides the -sDYNAMIC_EXECUTION=1
flag to avoid calls to eval
or new Function()
. This does break some Emscripten features, but none that I've used thus far. Point 1 done.
To address point 2, we change how we load our compiled code in the Cloudflare worker: We import the compiled WASM ourselves (CF's runtime provides a loader to do so) alongside Emscripten's usual output. Then we implement the instantiateWasm
hook in the options we pass to the Emscripten-generated module factory:
import WasmModule from ' ./gen/MyWasm.js ' ;
import WasmBinary from ' ./gen/MyWasm.wasm ' ;
const wasmInstance = WasmModule ( {
async instantiateWasm ( imports , onSuccess ) {
const instance = await WebAssembly . instantiate ( WasmBinary , imports ) ;
onSuccess ( instance ) ;
return instance . exports ;
} ,
async onRuntimeInitialized ( ) {
// The wasmInstance is only usable after this hook is called.
wasmInstance . doTheThing ( ) ;
} ,
} ) ;
We still need to tell Emscripten that we want to define these hooks, with the linker option -sINCOMING_MODULE_JS_API=instantiateWasm,onRuntimeInitialized
.
✔ No more dynamic code evaluation errors
ES6
The above assumes the use of ES6 modules, but this is not the Emscripten default. Setting the -sMODULARIZE
and -sEXPORT_ES6
linker flags takes care of that, allowing us to import our modules as shown above. As with so many flags, these too have their own caveats and implications.
Resource Re-use
It might be tempting at this point to hold your WASM instance as a global. Why re-instantiate it for each request, particularly if it's stateless? Well if you perform any I/O (which we are) you'll soon run afoul of CF's worker request isolation. There are any number of solutions to this depending on what you're doing, but the simplest is to perform your WASM instantiation when the request arrives.
✔ No more I/O errors
Package Size
There are various limits to how large your compiled worker can be, which may necessitate a modular codebase that lets you deploy only what you need. Thankfully that's what we have in Saus, and I only hit these limits when accidentally trying to upload a debug build 🙃.
✔ No more packaging & deployment errors
The Outcome
While it was more difficult than I had hoped to get working, being able to quickly and easily deploy modules from our existing code keeps the surface area and cognitive load of the codebase to a minimum. Combine that with the very low maintenance burden of running a worker, and this lets us focus more on product development and less on maintenance, which is exactly what I'd hoped for.
This is a high-level summary with sparse code examples - there are far too many nitty-gritty details to cover. You can explore the full boilerplate in a repo here for more details, particularly with regards to tooling. It should 🤞 give you everything you need to get a complex C++ program running on a Cloudflare worker.
Thoughts & Notes
1. Prerequisites
None of this would have been viable if the C++ code didn't already have the modularity and encapsulation to deploy it piecemeal, or the appropriate abstractions in place to allow easy reimplementation of dependencies (e.g. the HTTP client). Bear in mind WASM is generally 32bit (Memory64 has just arrived).
2. Debugging
Emscripten goes to its own lengths to ensure that its output is debuggable. Source maps and embedded DWARF symbols are generated. A combination of node
+ clangd
+ codeLLDB
+ WASM DWARF Debugging
on VSCode
gave a passable debugging experience. But to say I understand how all those parts work would be a lie. As such I debugged as Emscripten intended - using node
- which meant creating a basic harness to emulate some of what CF's worker offers within their JS runtime.
I'm sure you could achieve a better developer experience running the WASM always in CF's own local worker runtime, if you knew what you were doing with its debugger.
3. Performance
This endeavor was entirely for practical - not performance - reasons. This is probably at odds with most motives for using WASM. The deployed C++ module is not in a hot path, and performance hasn't been evaluated. I've no reason to believe there are any performance issues; it just hasn't been evaluated.
4. The Emscripten Combinatorial Explosion
As a newcomer to both Emscripten and WASM, the hardest thing was figuring out the interplay or interdependency between different features, APIs, specs or proposals; and the true state and availability of any of them.
There are many many ways to achieve the same thing. Different approaches may or may not affect each other in unexpected ways. Enabling or disabling various compile flags requires a careful scientific approach if you want to draw any meaningful conclusions. Sometimes I cut corners on this to try and save time, which usually just wasted time instead.
A lot of the literature is out of date or incomplete. It's not that anyone has done anything wrong: It's just a by-product of Emscripten being relatively old and WASM/WASI being relatively new, with many huge evolutions (such as ES6) in between. Hats off to all the contributors - it really is a wonder it works at all.
P.S. Yes, containers on Cloudflare just became a thing.
P.P.S. No, I have zero affiliation with Cloudflare. I would just be very happy if I never had to touch another IAM policy.