FreeCAD in the Browser: A 1.5M-Line C++ CAD App Ported to WebAssembly in 4 Days
FreeCAD, the open-source parametric 3D CAD application, now runs entirely in a browser tab. The port, completed in roughly four days, compiles the full application—including its OpenCASCADE geometry kernel, Coin3D scene graph, embedded CPython 3.14, PySide6 bindings, and 17 workbenches—into a single 196 MB WebAssembly module. First load is ~96 MB compressed (Brotli), cached afterward. Requires Chrome/Edge 137+ (WebAssembly JSPI support).
The port was executed almost entirely by an AI agent named Fable, with all 48 prompts and session transcripts published. The developer's goal was to stress-test Fable's capabilities on a large, complex project.
The Build: Kernel-First, Then GUI, Then Python
The strategy was to prove the headless kernel worked before touching the GUI. FreeCAD's C++ modules are Python extension modules normally dlopened at runtime. Since WebAssembly has no dlopen, every module was statically linked and registered on the interpreter's inittab via PyImport_AppendInittab before Py_Initialize. With a NODERAWFS build for node-side testing, FreeCADCmd ran headlessly—opening .FCStd files, recomputing, and round-tripping STEP—first in Node, then in the browser.
Linking the GUI: OpenGL Emulation
The GUI link required a workaround for OpenGL. FreeCAD renders through Coin3D, which uses legacy fixed-function OpenGL (glBegin, glVertex, matrix stacks). WebGL2 lacks these, so a hand-written fixed-function-on-GLES2 shim (~900 lines, WasmGLFixedFunc) was included. The shim implements CPU-side matrix stacks, a GLSL ES program with two-light per-pixel shading, and immediate-mode batching. Compositing required an offscreen framebuffer (WasmGLWidget) because WebGL contexts cannot share textures with Qt's raster compositing.
Exception Handling: JSPI and Binaryen Pass
The port initially used Emscripten's ASYNCIFY and JavaScript exceptions. Migration to JSPI (WebAssembly Promise Integration) plus native wasm exceptions hit a wrinkle: V8 rejects modules mixing legacy try/catch and new try_table/exnref instructions. The fix was a post-link Binaryen pass (wasm-opt --translate-to-exnref) to normalize all exceptions, plus a small JS post-process (jspi_postprocess.py) wrapping timer and event-dispatch callbacks in WebAssembly.promising. Result: 46% smaller module and markedly faster than the asyncify build.
PySide6 on WebAssembly: First Time
Getting PySide6 (Qt bindings) to work under wasm was a subproject. It required building shiboken6 and PySide6 statically against the wasm CPython, no-dlopen. The result: Gui.getMainWindow() returns a live PySide6 QMainWindow that round-trips to the real C++ window. This unlocked Python workbenches.
Workbenches and Threading
Seventeen workbenches were enabled. Each flushed out assumptions about threading: Sketcher's constraint solver ran QR decompositions on a thread (deferred to inline), and modules using QtConcurrent or thread-affinity guards had to be serialized. Persistence uses IndexedDB, with a gate to prevent hydration timing issues.
Technical Details
- Toolchain: Qt 6.11.1 compiled for wasm with JSPI and native wasm exceptions, Emscripten 4.0.12.
- Static dependencies: OCCT, CPython, ICU, Boost, Xerces, {fmt}, yaml-cpp, VTK 9.3, Salome SMESH.
- OpenGL shim: ~900 lines, handles ~70 glBegin sites across 20 files.
- Modal dialogs: 185 C++ exec() sites and 156 in Python, all handled by JSPI suspension.
Why It Matters
This port demonstrates that large, complex desktop applications with legacy OpenGL and Python bindings can be brought to the browser. It provides a scriptable CAD kernel in a tab, enabling collaborative design, cloud-based workflows, and easier access for education. The techniques used—static linking, JSPI, OpenGL emulation—are applicable to other Qt/OpenGL applications.
Editor's Take
I've spent years fighting with FreeCAD's build system on Linux, so seeing it run in a browser is surreal. The fact that an AI agent did most of the work in four days makes me question how much of my own porting effort was just boilerplate. The reliance on Chrome/Edge 137+ is a real limitation—Firefox and Safari users are left out. But for teams building collaborative CAD tools, this is a huge leap. I'd love to see the approach applied to other large Qt apps like KiCad or even Blender's viewport.

