It's not 100% better, but it cuts out a few tokens which helps readability and moves the significant asterix further left where I think it's easier to spot.
Frankly, "sizeof(T*)" should generate a warning if T is anything other than void, or a function type.
Yes, I know that C technically allows rather heterogenous representations for pointers to different types, but in practice there is difference only between object pointers and function pointers.
You don't need 4GB and it wastes memory to make pointers twice as big? Even Linux supports running 64-bit code in a 32-bit address space ("x32 ABI") for this reason.
TL;DR: wasm64 has slower memory load/store operation because it requires 'software bounds checking', so unless you absolutely need more than 4 GB RAM, wasm32 is the better choice.
TL;DR: wasm64 requires explicit heap bounds checks, while in wasm32 the memory mapping hardware does it for free.
E.g. quote:
"The only reason to use Memory64 is if you actually need more than 4GB of memory.
Memory64 won’t make your code faster or more “modern”. 64-bit pointers in WebAssembly simply allow you to address more memory, at the cost of slower loads and stores."
I love how WASM is the thing that finally blurred the line between Web and Native programming, formely two realms isolated from each other for a long time. This both develops better awareness of how the code is executed by the hardware, which JavaScript devs often lack, and also brings skilled folks from the Native platforms who seem to be not so against WASM as they were against JavaScript (and all other parts of the Web, really). Maybe this will bear fruit in that people will make more Native user interfaces again.
Wasm still doesn't let you make native user interfaces, the UI is in the web browser. You can put native UI components into a React Native or Electron app though.
The pointer size difference is the symptom, not the disease. Storing a raw pointer in a pak file was never valid even on native since the address changes every load, so the real fix is exactly what the author landed on, store an index into a side table instead of an embedded pointer. That offset based blob approach is what flatbuffers and console asset bakers use precisely so the same baked file loads identically across architectures and can be mmapped with zero fixups.
With regards to 1), do not write/read structs directly to/from files. Instead write a proper serializer/deserializer. Without it, you may encounter another breakage soon when a different compiler/compiler options insert different struct padding bytes, which will then once again make your data non-portable, and a maliciously crafted save file with no length/size field validation on the deserializer level can lead to a variety of memory bugs.
struct layout is well specified, it should be possible to avoid any padding issues by just aligning and by padding (with dummy members) correctly. The problem in practice is mostly integer representation (big-endian vs little-endian).
FTA: I was serializing asset structs directly to disk (pak file) that had raw pointers in them
I’m surprised that that works in WASM. Wouldn’t a tiny change in your memory usage (say if you toggle your “log startup progress” flag) load data at a different address?
I've been porting Micropolis (SimCity Classic) to WASM / WebGPU / Svelte 5. Emscripten + Embind compile the C++ engine and glue it to TypeScript/Svelte/Runes/Reactivity; TypeScript owns UI, rendering, and callback handlers.
I agree with the article's main lessons: wasm32 pointer size, don't serialize structs with pointers, debug native 32-bit when you can, WebGL/WebGPU is stricter than desktop GL, Emscripten export flags still bite. I hit some of the same categories; the parts that were actually tricky for Micropolis are below.
Svelte 5 runes ($state, $derived, etc.) work in plain .ts modules, not just .svelte templates. That matters because the WASM bridge is a reactive module the HUD, command bus, and Vitest all import -- not a component-only trick. The file has to be MicropolisReactive.svelte.ts so runes compile under the same Vite/SvelteKit pipeline as the app; plain .ts breaks in Node with "$state is not defined".
Embind API surface -- what to expose and what to leave out:
// This file uses emscripten's embind to bind C++ classes,
// C structures, functions, enums, and contents into JavaScript,
// so you can even subclass C++ classes in JavaScript,
// for implementing plugins and user interfaces.
//
// Wrapping the entire Micropolis class from the Micropolis (open-source
// version of SimCity) code into Emscripten for JavaScript access is a
// large and complex task, mainly due to the size and complexity of the
// class. The class encompasses almost every aspect of the simulation,
// including map generation, simulation logic, user interface
// interactions, and more.
The comments in that file go on to describe the strategy for wrapping: Core Simulation Logic, Memory and Performance Considerations, Direct Memory Access, User Interface and Rendering, Callbacks and Interactivity, and Optimizations.
The engine callback virtual interface bridged C++ to JS via JSCallback:
In the old NeWS/Hyperlook, TCL/Tk/X11, SWIG/Python/PyGTK, and SWIG/Python/TurboGears/AMF/Flash versions, this callback interface used to be a stringly typed general purpose event callback interface, which I tightened up into a strict C++ interface and corresponding typescript interface, so embind could help me integrate it safely and cleanly with TypeScript and Svelte Runes.
TypeScript handlers that update rune-backed state (sendMessage, didTool, budget hooks, etc.):
The pattern: C++ fires callbacks with enough context for the UI; TS updates $state; components read micropolisReactive (peek / poke / memory / getSnapshot) instead of calling Embind...
I just finished a similar project for fun and education.
It was a 20-year-old codebase from my old game in win32 and DirectX 9.
I first ported it to native and also switched to bgfx for rendering. This was the bulk of the work - converting all of the old DirectX fixed function pipeline code to shaders. Luckily all modern shaders can simulate all of the old fixed-function DX pipeline features with little effort. Including the coordinate system. Loading DDS textures didn't present a major challenge either.
Had similar native asset loading as yours - no deserializer. It loaded an entire asset file into a preallocated memory block, used packed structures and converted file offsets to pointers after loading. I had to convert it to 64bit for native first.
The most surprising thing: I had no idea WASM is 32bit until I read your article! Once I ported to 64bit, I then ported to WASM and I didn't even encounter any arch related bugs. In hindsight I guess it's because most of the original code was 32bit and the asset file format is still 32bit format. When I ported to 64bit I used a deserializer, so I guess that's why it all worked out in the end.
For native audio I ended up using SoLoud library, but for emscripten I #ifdef'd it out to use inline JS instead. I figured there is no point in having all that extra audio library code compiling to WASM when modern browsers natively support playing audio, oggvorbis, etc. It worked out ok, but there's still a minor bug where the music doesn't loop perfectly. You can hear a split second gap between end/start. I haven't looked deeply into it yet.
Originally when we wrote the game we had banned ourselves from using C++ Exception handling and RTTI. The decision likely paid off as it makes the generated binary smaller and faster. Although I haven't had time to measure. Supposedly C++ exceptions introduce a much heavier overhead in Emscripten.
I did the same with one of small games I have developed. It wasn't that hard. I only needed to tweak the build script and to fix some minor issues, like changing how main function works and swapping color components in the result picture. I did use SDL2 for it, but without OpenGL, so, I had no problems with shaders or something similar.
30 comments
[ 2.9 ms ] story [ 51.6 ms ] threadSince this is one of the bugs, I always recommemd writing
Like this instead: It's not 100% better, but it cuts out a few tokens which helps readability and moves the significant asterix further left where I think it's easier to spot.But ACSHUALLY, how you write allocation is like this
The kernel people seem to finally have figured out this one in 2026.Yes, I know that C technically allows rather heterogenous representations for pointers to different types, but in practice there is difference only between object pointers and function pointers.
https://spidermonkey.dev/blog/2025/01/15/is-memory64-actuall...
TL;DR: wasm64 has slower memory load/store operation because it requires 'software bounds checking', so unless you absolutely need more than 4 GB RAM, wasm32 is the better choice.
> Web is 32-bit. Your 64-bit structs will break. This was the root cause of most of my bugs. WASM is 32-bit address space, pointers are 4 bytes not 8.
The real mistake is requiring pointer to be 64 bit when most programs don’t use it.
https://spidermonkey.dev/blog/2025/01/15/is-memory64-actuall...
TL;DR: wasm64 requires explicit heap bounds checks, while in wasm32 the memory mapping hardware does it for free.
E.g. quote:
"The only reason to use Memory64 is if you actually need more than 4GB of memory.
Memory64 won’t make your code faster or more “modern”. 64-bit pointers in WebAssembly simply allow you to address more memory, at the cost of slower loads and stores."
[0] https://soft.vub.ac.be/Publications/2022/vub-tr-soft-22-02.p...
[1] https://www.usenix.org/system/files/sec20-lehmann.pdf
I’m surprised that that works in WASM. Wouldn’t a tiny change in your memory usage (say if you toggle your “log startup progress” flag) load data at a different address?
I agree with the article's main lessons: wasm32 pointer size, don't serialize structs with pointers, debug native 32-bit when you can, WebGL/WebGPU is stricter than desktop GL, Emscripten export flags still bite. I hit some of the same categories; the parts that were actually tricky for Micropolis are below.
Svelte 5 runes ($state, $derived, etc.) work in plain .ts modules, not just .svelte templates. That matters because the WASM bridge is a reactive module the HUD, command bus, and Vitest all import -- not a component-only trick. The file has to be MicropolisReactive.svelte.ts so runes compile under the same Vite/SvelteKit pipeline as the app; plain .ts breaks in Node with "$state is not defined".
Embind API surface -- what to expose and what to leave out:
https://github.com/SimHacker/MicropolisCore/blob/main/packag...
The comments in that file go on to describe the strategy for wrapping: Core Simulation Logic, Memory and Performance Considerations, Direct Memory Access, User Interface and Rendering, Callbacks and Interactivity, and Optimizations.The engine callback virtual interface bridged C++ to JS via JSCallback:
https://github.com/SimHacker/MicropolisCore/blob/main/packag...
In the old NeWS/Hyperlook, TCL/Tk/X11, SWIG/Python/PyGTK, and SWIG/Python/TurboGears/AMF/Flash versions, this callback interface used to be a stringly typed general purpose event callback interface, which I tightened up into a strict C++ interface and corresponding typescript interface, so embind could help me integrate it safely and cleanly with TypeScript and Svelte Runes.
TypeScript handlers that update rune-backed state (sendMessage, didTool, budget hooks, etc.):
https://github.com/SimHacker/MicropolisCore/blob/main/apps/m...
Simulator attach/detach, singleton engine load, wiring JSCallback into Micropolis:
https://github.com/SimHacker/MicropolisCore/blob/main/apps/m...
The pattern: C++ fires callbacks with enough context for the UI; TS updates $state; components read micropolisReactive (peek / poke / memory / getSnapshot) instead of calling Embind...
It was a 20-year-old codebase from my old game in win32 and DirectX 9.
I first ported it to native and also switched to bgfx for rendering. This was the bulk of the work - converting all of the old DirectX fixed function pipeline code to shaders. Luckily all modern shaders can simulate all of the old fixed-function DX pipeline features with little effort. Including the coordinate system. Loading DDS textures didn't present a major challenge either.
Had similar native asset loading as yours - no deserializer. It loaded an entire asset file into a preallocated memory block, used packed structures and converted file offsets to pointers after loading. I had to convert it to 64bit for native first.
The most surprising thing: I had no idea WASM is 32bit until I read your article! Once I ported to 64bit, I then ported to WASM and I didn't even encounter any arch related bugs. In hindsight I guess it's because most of the original code was 32bit and the asset file format is still 32bit format. When I ported to 64bit I used a deserializer, so I guess that's why it all worked out in the end.
For native audio I ended up using SoLoud library, but for emscripten I #ifdef'd it out to use inline JS instead. I figured there is no point in having all that extra audio library code compiling to WASM when modern browsers natively support playing audio, oggvorbis, etc. It worked out ok, but there's still a minor bug where the music doesn't loop perfectly. You can hear a split second gap between end/start. I haven't looked deeply into it yet.
Originally when we wrote the game we had banned ourselves from using C++ Exception handling and RTTI. The decision likely paid off as it makes the generated binary smaller and faster. Although I haven't had time to measure. Supposedly C++ exceptions introduce a much heavier overhead in Emscripten.
You can see the port in action at https://scorchedplanets.com