DEV Community

Cover image for What does 'porting' to Wasm mean?
Martin Nyaga for DRS Software

Posted on • Originally published at drs.software

1

What does 'porting' to Wasm mean?

Welcome to part 3 of our series, Porting Native Apps to the Web with WebAssembly: A Complete Guide, by DRS Software.

Table of Contents

Introduction

In previous articles, we explored how native applications can be compiled to WebAssembly (Wasm) and run in the browser. We also discussed the advantages of this approach compared to rewriting applications from scratch.

In nearly all cases, however, the process isn’t as simple as compiling the source code. Most applications require porting to WebAssembly before they can function properly in a browser environment. This article explains what porting means, why it’s necessary, and what it involves.

Why Applications Need Porting

A key concept to understand is that typical applications don’t operate in isolation. While source code defines an application’s logic, it relies on various system services to perform meaningful work. For example, an application may need to:

  • Allocate memory from the operating system.
  • Draw graphics to the screen or output text to a terminal.
  • Read and write files.
  • Communicate over a network.
  • Access hardware devices such as a microphone, speaker, or camera.
  • Use system-level timing, randomness, or process management features.
  • Load dynamic libraries (.dll, .so) as new executable code at runtime.

All of these tasks are fulfilled by the operating system, which provides essential services through system calls (syscalls). These are low-level functions that allow programs to request resources, interact with hardware, and communicate with other processes.

A diagram of a native application using syscalls to interact with the operating system

Typically, programming languages and libraries provide abstractions over these low level APIs that you call from your program. The OS is then responsible for executing the requested actions and returning a result that is safe to handle within the application.

However, when compiling an application to WebAssembly, there is a major missing link: Wasm runs in a strictly sandboxed environment that lacks direct access to system calls or operating system services. Instead, it can only interact with the outside world through functions explicitly imported from the host environment — in the case of the browser, this means calling JavaScript APIs.

Porting: Adapting System Interactions

Since WebAssembly does not support traditional system calls, porting an application means replacing these system interactions with equivalents that work in a browser environment. This is conceptually similar to porting software from one operating system to another — such as adapting a Linux application to run on Windows — but with additional constraints due to different "building blocks" (primitives and APIs) in the browser environment.

A diagram of a Wasm module using JavaScript APIs to access system services

So at a high level, porting an application to the browser involves adapting code that interacts with the underlying system to instead use JavaScript-based alternatives. For example:

  • Using fetch for networking instead of native sockets.
  • Using the Web Audio API instead of direct sound hardware access.
  • Using LocalStorage, IndexedDB, or the FileSystem API instead of direct file system access.
  • Using the DOM and WebGL instead of native graphics APIs.

For certain features, there is a good matching browser API. For example, playing simple audio or drawing basic 2D graphics can be implemented in a conceptually similar way on the browser as in a native application (even though the details of the APIs themselves are different).

However, some features have no conceptual parallel on the browser, and must be emulated or re-implemented. One such example is the fork syscall, which duplicates a process into two identical copies; this is a syscall available on native systems that cannot have a faithful implementation on the browser.1

Beyond Syscalls: Porting Libraries and Runtimes

While system calls are a major part of the challenge, porting is often further complicated by dependencies on libraries, frameworks, or language runtimes.

Language Runtimes

Many programming languages include a runtime or virtual machine that manages execution of your code, including memory management and system interactions. Examples include:

  • The Java Runtime Environment (JRE), which provides garbage collection, threading, and platform-independent APIs.
  • The .NET Common Language Runtime (CLR), which handles memory management, Just-In-Time (JIT) compilation, and interoperability.
  • The Python interpreter, which provides an execution environment for Python code.

These runtime components assume access to system services that are not available in a Wasm sandbox. Porting an application written in such a language may require adapting or porting the runtime itself, or replacing it with a Wasm-compatible alternative. Fortunately, a lot of tooling and wasm-compatible runtimes2 exist, and this burden is rarely borne directly by application developers.

Frameworks and Libraries

Applications often rely on frameworks and libraries that provide essential functionality but are deeply integrated with native platforms. Porting such applications requires addressing these dependencies. For example:

  • GUI frameworks like MFC, WinForms, GTK or AppKit provide abstraction layers for rendering interfaces but depend on system-specific implementations.
  • Multimedia libraries for audio/video processing often use platform-native APIs that are not directly portable.
  • Other third-party libraries (e.g., for cryptography, compression, or networking) may assume access to system-specific features.

In these cases, porting requires one or more of the following approaches:

  • Rewriting parts of the application to use browser-native alternatives (e.g., using WebGL instead of a native graphics library).
  • Finding Wasm-compatible replacements for key libraries or frameworks.
  • Compiling the required libraries to WebAssembly, if they are open-source and can be appropriately adapted.

Practical Example: Drawing a Pixel on the Screen

We'll use rendering a single pixel as an example; the code is simple, but it demonstrates the key differences between how native applications and WebAssembly interact with their respective environments.

Let's consider the simple C++ function below that uses the Win32 SetPixel function to draw to a global window:

void drawPixel(int x, int y, int color) {
    SetPixel(globalWindowHandle, x, y, color);
}
Enter fullscreen mode Exit fullscreen mode

On Windows

When this function is compiled and run natively on Windows:

  1. The SetPixel function is part of the Win32 API, which interacts with the operating system.
  2. The OS communicates with the graphics driver to modify the framebuffer.
  3. The GPU processes the change, and the display updates with the red pixel.

Porting to Wasm

If the same C++ function were compiled to WebAssembly, it would not work as-is, because WebAssembly cannot directly access the Win32 API or the OS graphics subsystem. Instead, it must be ported to the web, i.e., rewritten to call JavaScript, which interacts with the browser's rendering engine.

We can introduce a JavaScript function, in this case drawing to a HTML canvas element:

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

function SetPixel(x, y, color) {
    ctx.fillStyle = intToRgb(color); // Set the color of the pixel
    ctx.fillRect(x, y, 1, 1); // Draws it at (x, y)
}
Enter fullscreen mode Exit fullscreen mode

Note: the implementation of intToRgb is omitted here for brevity.

This function can then be imported into the Wasm module and used as the low-level API for rendering the pixel.

The modified C++ code might look as follows:

// The code within the block below is only compiled for Wasm
#ifdef __WASM__
namespace [[cheerp::genericjs]] client {
    // Declare that there’s an imported JS function for drawing a pixel
    void SetPixel(int x, int y, int color);
}
#endif

void drawPixel(int x, int y, int color) {
    #ifdef __WASM__
    // In Wasm build: Use the JavaScript function
    client::SetPixel(x, y, color);
    #else
    // In native build: Use the normal Win32 function
    SetPixel(globalWindowHandle, x, y, color);
    #endif
}
Enter fullscreen mode Exit fullscreen mode

Note: the example above uses the semantics of the Cheerp C++ to Wasm compiler, but similar code is possible using Emscripten.

In this WebAssembly-friendly approach:

  1. The generated Wasm module imports a JS function to supply the functionality that was previously supplied by a native system API (Win32)
  2. When Wasm calls the JavaScript function, it updates a HTML Canvas element using the JS API.
  3. The browser’s rendering engine processes the update and sends it to the GPU.

Key Differences

This example illustrates why porting is necessary — code that interacts with the operating system must be adapted to work within the browser’s execution model.

  • In Windows, the application makes direct system calls via Win32.
  • In WebAssembly, all interactions go through JavaScript and browser APIs.
  • The browser enforces sandboxing, preventing direct hardware or OS access.

Tooling Automated Transformations

In practice, compilers and tools can automatically transform many common patterns (e.g. memory allocation, some standard library functions) into browser-compatible equivalents. This means that porting efforts are typically required only for application specific functionality where such transformations do not apply.

For example, in standard C++ to Wasm compilers (e.g. Cheerp, Emscripten), writing to stdout is automatically redirected to print in the browser console, requiring no manual code changes. Similarly, memory allocation, and growing the size of the WebAssembly memory is handled automatically by the compiler. While these transformations happen behind the scenes, they follow the same conceptual process as illustrated above; that is, they automatically replace calls to native APIs with calls to JavaScript APIs.

Conclusion

Porting an application to WebAssembly is often more than just recompilation — it involves adapting system interactions, dependencies, and runtime expectations to function in a browser sandbox. This can mean:

  • Replacing system calls with JavaScript/browser-native equivalents.
  • Porting, adapting or replacing runtime components.
  • Porting dependencies, or finding/developing alternative libraries.

This process can be partially automated by modern tooling and compilers, which greatly simplifies the process of porting.

In future articles, we will explore the differences between the browser and native platforms in more detail, discussing common challenges and practical techniques for overcoming them.

Stay tuned for part 4, or review part 2: When should you use WebAssembly?


  1. See this issue for some discussion about why fork cannot faithfully be implemented in the browser. 

  2. Several examples of wasm-compatible language runtimes have been touched on in previous articles

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (2)

Collapse
 
fyodorio profile image
Fyodor

Wouldn’t WASI help with that actually?

Collapse
 
martinnyaga profile image
Martin Nyaga

@fyodorio depends on what you mean. Here we're focused on running the app in a browser. You can definitely run a WASI module in the browser and provide a JavaScript implementation of the system API (in the WASI 0.1 model). So I suppose it does give you a clear standard to work with.

But to achieve that, you first have to compile the app code into a (WASI compliant) Wasm module. The point in this article is that if you want to compile a given native codebase to Wasm, the scope of problems you will run into to achieve this is quite a bit wider than system calls specifically - libraries, platform level dependencies and APIs, and even entire language runtimes (e.g. the Java Runtime Environment).

Hopefully the next article will motivate a bit more of the considerations you need to make when approaching a porting effort.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

If this post resonated with you, feel free to hit ❤️ or leave a quick comment to share your thoughts!

Okay