Building a Tiny Custom JavaScript Runtime with Duktape and WebAssembly

This article explains how to create a lightweight, embeddable JavaScript runtime using Duktape, compile it to WebAssembly, expose custom APIs, and integrate it into a web-based login greeter, highlighting implementation steps, code examples, and potential use cases.

ELab Team
ELab Team
ELab Team
Building a Tiny Custom JavaScript Runtime with Duktape and WebAssembly

Background

This short experiment started after noticing the custom JS Runtime feature in the lightdm-webkit2-greeter plugin, which allows using web technologies to design login screens and interact with the OS via a set of JS APIs for actions like login, shutdown, and sleep.

The approach renders the system UI with WebKit and extends the JS runtime to bind native OS functions, similar to how Electron works but without a separate process, resulting in a smaller, more customizable solution.

Thoughts

Modifying a full‑featured engine like V8 or JavaScriptCore is extremely difficult, so a lightweight, portable engine is needed. Instead of directly binding OS functions, the author proposes first creating a Web‑based binding and running the runtime inside WebAssembly.

Demo

Start a JS Engine

Easy to port and modify.

Simple native‑JS interaction (type conversion, communication, event loop).

Small binary size and low memory footprint when compiled to WebAssembly.

The chosen engine is Duktape , an embeddable JavaScript engine focused on portability and low memory usage.

Duktape can be integrated by adding duktape.c, duktape.h, and duk_config.h to the build and using its API for two‑way calls between C and ECMAScript. The compiled size is under 64 KB and runs in 192 KB of memory.

Simple function to execute JavaScript:

extern "C" char* runScript(char* script){
    duk_context *ctx = duk_create_heap_default();
    duk_eval_string(ctx, script);
    duk_pop(ctx);  /* pop eval result */
    duk_destroy_heap(ctx);
    return "ok";
}

Extend Some Runtime API

Implement I/O functionality.

static duk_ret_t native_print(duk_context *ctx) {
    duk_push_string(ctx, " ");
    duk_insert(ctx, 0);
    duk_join(ctx, duk_get_top(ctx) - 1);
    printf("%s
", duk_safe_to_string(ctx, -1));
    return 0;
}

Bind to the runtime.

duk_push_c_function(ctx, native_print, DUK_VARARGS);
duk_put_global_string(ctx, "print");

The following diagram shows Duktape’s stack model:

Compile to WASM

Emscripten is used to compile the engine into a WebAssembly module and generate the JavaScript glue code.

Expose the JS execution function to the host environment.

int main(){
    EM_ASM("console.log('wasm js runtime is ready!')");
    EM_ASM("window.runScript = Module.cwrap('runScript','string',['string'])");
    return 0;
}

Specify exported functions during compilation.

CCOPTS += -s EXPORTED_FUNCTIONS=['_runScript','_main']

Complete Makefile:

DUKTAPE_SOURCES = ./engine/duktape.c
CC = emcc
CCOPTS = -s DISABLE_EXCEPTION_CATCHING=0 -s ALLOW_MEMORY_GROWTH=1 -O3 --bind
CCOPTS += -s EXPORTED_RUNTIME_METHODS=["cwrap"]
CCOPTS += -s EXPORTED_FUNCTIONS=['_runScript','_main']
CCOPTS += -I./engine
BUILD = wasm/index.html
all: $(DUKTAPE_SOURCES) main.cpp
	${CC} $(CFLAGS) $(CPPFLAGS) ${LDFLAGS} -o ${BUILD} ${DEFINES} ${CCOPTS} ${DUKTAPE_SOURCES} main.cpp ${CCLIBS}
run:
	cd wasm && python3 -m http.server 8080

Simple Test

make
make run

The resulting WASM bundle (glue code + binary) is just over 600 KB, which is acceptable for a single‑image deployment.

The custom JS runtime is now functional, offering:

Small size and on‑demand usage.

Complete isolation from the host JS runtime, enhancing security.

Good performance on the web because the WASM implementation runs outside the main JS thread.

Application Scenarios

JavaScript sandboxing.

Building plugin systems.

Porting the WASM artifact to WASI for true OS bindings.

References

Duktape – https://duktape.org/index.html

Emscripten documentation – https://emscripten.org/

How to build a plugin system on the web – https://www.figma.com/blog/how-we-built-the-figma-plugin-system/

frontendJavaScriptWebAssemblyRuntimeEmbeddingDuktape
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.