How Ladybird Browser Handles JavaScript Execution and C++ Binding

Ladybird Browser executes JavaScript through the LibJS engine by creating a JS::VM and JS::Realm, while exposing native C++ objects via WebIDL-generated bindings that inherit from Web::Bindings::PlatformObject and register prototypes in the global object.

Ladybird Browser implements a standalone web stack including its own JavaScript engine called LibJS. The architecture separates ECMAScript execution logic from browser API bindings, allowing tight integration between script code and native C++ implementations through a sophisticated platform object system.

JavaScript Execution Flow in Ladybird

Ladybird’s JavaScript execution follows a structured pipeline from source parsing to runtime evaluation. The process centers on the LibJS::VM class which manages all execution contexts and heap allocation.

VM Initialization and Realm Creation

Execution begins with VM instantiation. In Utilities/js.cpp, the global VM is created via:

g_vm_storage.get() = JS::VM::create();

This singleton VM manages all JavaScript execution for the process. After creation, the system allocates a JS::Realm that serves as the global environment. In browser contexts, this realm uses Web::Bindings::WindowObject as its global object, while test harnesses may use simplified globals.

The realm encapsulates the global environment record and provides the intrinsics table where all built-in prototypes reside.

Parsing and Bytecode Compilation

Source code enters the system through Parser::parse in Libraries/LibJS/Parser/Parser.cpp, which constructs an Abstract Syntax Tree (AST). When bytecode debugging is enabled, the AST compiles to bytecode via Libraries/LibJS/Bytecode/Generator.cpp. Otherwise, the VM may execute directly from the AST or other intermediate representations.

Script Execution and Context Management

The VM::run method creates an ExecutionContext and pushes it onto the execution stack. This context carries the realm, lexical environment, and variable environment necessary for proper scope resolution. The VM then processes the compiled bytecode or AST nodes until completion.

Identifier resolution occurs through VM::resolve_binding, defined in Libraries/LibJS/Runtime/VM.h. This method walks the lexical environment chain and checks the global object’s declarative record to locate variables. For example, the REPL implementation in Utilities/js.cpp uses this mechanism for autocomplete functionality around lines 775-795:

auto reference_or_error = g_vm->resolve_binding(variable_name,
                                                JS::Strict::No,
                                                &global_environment);

Microtask Processing

After the main script completes, Ladybird processes queued promise jobs via VM::run_queued_promise_jobs(). This ensures that Promise resolutions and other microtasks execute before the next macrotask, maintaining ECMAScript compliance.

C++ to JavaScript Binding Architecture

Exposing browser APIs to JavaScript requires bridging the gap between C++ implementations and ECMAScript semantics. Ladybird accomplishes this through WebIDL-generated bindings built on top of a unified platform object layer.

WebIDL Code Generation

Ladybird uses a WebIDL compiler to generate C++ classes from interface definitions. Each IDL interface produces a corresponding C++ class that wraps the underlying implementation. For example, Web::HTML::HTMLDivElement in the DOM becomes a generated binding class that exposes the element’s methods and properties to scripts.

The PlatformObject Base Class

All WebIDL-generated classes inherit from Web::Bindings::PlatformObject, located in Libraries/LibWeb/Bindings/PlatformObject.h. This class extends JS::Object and provides the bridge between JavaScript’s object model and C++ implementations:

// Libraries/LibWeb/Bindings/PlatformObject.h – lines 35-42
class WEB_API PlatformObject : public JS::Object {
    JS_OBJECT(PlatformObject, JS::Object);
    // ...
};

The WEB_PLATFORM_OBJECT macro expands JS_OBJECT and adds virtual helpers for interface identification:

// Libraries/LibWeb/Bindings/PlatformObject.h – lines 21-31
#define WEB_PLATFORM_OBJECT(class_, base_class) \
    JS_OBJECT(class_, base_class)                \
    virtual Bindings::InterfaceName interface_name() const override { ... } \
    virtual bool implements_interface(String const& interface) const override { ... }

Global Object Initialization

When a realm initializes, Web::Bindings::initialize_global_object() populates the intrinsics table with prototypes for every platform object. This registration happens in Libraries/LibWeb/Bindings/WindowObject.cpp, ensuring that window.Element, window.Node, and other constructors exist before script execution begins.

Native Function Wrappers

C++ methods callable from JavaScript use the JS_DEFINE_NATIVE_FUNCTION macro. These functions extract the this value as a PlatformObject*, perform the operation, and return JS::Value instances. For example, a simplified window alert implementation:

JS_DEFINE_NATIVE_FUNCTION(WindowObject::alert)
{
    auto* window = TRY_OR_FAIL(verify_self<WindowObject>(vm, this_value));
    // C++ implementation细节...
    return JS::js_undefined();
}

The verify_self template function ensures type safety by validating that the JavaScript this value points to the expected C++ class.

Property Access Hooks

PlatformObject overrides internal object methods like internal_get_own_property and internal_set to forward property accesses to generated C++ getters and setters. This mechanism respects WebIDL’s "expose" rules and implements attribute reflection automatically.

Practical Implementation Examples

Running JavaScript from C++

The Utilities/js.cpp file demonstrates standalone script execution:

// VM already initialized: g_vm_storage.get() = JS::VM::create();
g_vm->heap().set_should_collect_on_every_allocation(gc_on_every_allocation);

// Create execution context and realm
auto execution_context = JS::create_simple_execution_context<ScriptObject>(*g_vm);
auto& realm = *execution_context->realm;

// Execute source
String source = "console.log('Hello from Ladybird');";
if (!TRY(parse_and_run(realm, source, "inline"sv, /*parse_only=*/false)))
    return 1;

// Process microtasks
g_vm->run_queued_promise_jobs();

Exposing Custom C++ Classes

To expose a C++ class to JavaScript, inherit from PlatformObject and register native methods:

// MyObject.h
#pragma once
#include <LibWeb/Bindings/PlatformObject.h>

namespace MyNamespace {

class MyObject final : public Web::Bindings::PlatformObject {
    WEB_PLATFORM_OBJECT(MyObject, Web::Bindings::PlatformObject);
public:
    explicit MyObject(JS::Realm&);
    JS_DEFINE_NATIVE_FUNCTION(say_hello);
};

}

Implementation:

// MyObject.cpp
#include "MyObject.h"

namespace MyNamespace {

MyObject::MyObject(JS::Realm& realm)
    : PlatformObject(realm)
{
    define_native_function("sayHello", say_hello, 0);
}

JS_DEFINE_NATIVE_FUNCTION(MyObject::say_hello)
{
    auto* self = TRY(verify_self<MyObject>(vm, this_value));
    dbgln("Hello from C++!");
    return JS::js_undefined();
}
}

Registration in the global object:

auto* my_proto = heap().allocate<MyObject>(realm);
realm.intrinsics().my_object_prototype = my_proto;

JavaScript can then instantiate and call methods:

let obj = new MyObject();
obj.sayHello(); // Logs to Ladybird console

Key Source Files

Understanding the Ladybird JavaScript binding system requires familiarity with these specific files:

Summary

  • Ladybird uses LibJS, a standalone C++ ECMAScript implementation, managed by the JS::VM class for all script execution.
  • Execution flows through VM creation, Realm allocation, AST parsing (via Parser::parse), and evaluation via VM::run with identifier resolution handled by VM::resolve_binding.
  • C++ binding occurs through WebIDL-generated classes inheriting from Web::Bindings::PlatformObject, registered in the global realm’s intrinsics table.
  • Native functions use the JS_DEFINE_NATIVE_FUNCTION macro and verify_self template to safely bridge C++ implementations to JavaScript calls.
  • Microtask processing completes the event loop via VM::run_queued_promise_jobs after main script execution finishes.

Frequently Asked Questions

How does Ladybird create the JavaScript VM?

Ladybird instantiates the JavaScript VM through JS::VM::create(), typically stored in thread-local storage via g_vm_storage.get() as seen in Utilities/js.cpp. This VM singleton manages the heap, execution contexts, and global realms for the entire browsing session.

What is PlatformObject in Ladybird?

PlatformObject is the abstract base class defined in Libraries/LibWeb/Bindings/PlatformObject.h that all WebIDL-generated DOM classes inherit from. It extends JS::Object and provides the necessary infrastructure for JavaScript to recognize and interact with C++ browser implementations, including interface name resolution and property access hooks.

How are C++ methods exposed to JavaScript?

C++ methods become callable JavaScript functions through the JS_DEFINE_NATIVE_FUNCTION macro, which creates a wrapper that extracts the this value using verify_self, executes the native code, and returns a JS::Value. These functions are registered on prototypes during global object initialization in WindowObject.cpp.

Where does identifier resolution happen in Ladybird?

Identifier resolution occurs in VM::resolve_binding within Libraries/LibJS/Runtime/VM.h. This method traverses the lexical environment chain and checks the global declarative record to locate variables, supporting both strict and non-strict mode resolution rules according to ECMAScript specifications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →