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:
Utilities/js.cpp– Entry point for the standalone JS REPL; creates the VM, parses scripts, and implementsparse_and_run.Libraries/LibJS/Runtime/VM.h– DefinesVM::resolve_binding, execution context management, and promise job queues.Libraries/LibWeb/Bindings/PlatformObject.h– Base class for all WebIDL-generated objects; implements the JavaScript object interface andWEB_PLATFORM_OBJECTmacro.Libraries/LibWeb/Bindings/WindowObject.cpp– Sets up the globalwindowobject and registers all DOM prototypes during realm initialization.Libraries/LibJS/Parser/Parser.cpp– Transforms source text into an AST for subsequent execution.Libraries/LibJS/Bytecode/Generator.cpp– Compiles AST nodes to bytecode when debugging or optimization requires intermediate representation.
Summary
- Ladybird uses LibJS, a standalone C++ ECMAScript implementation, managed by the
JS::VMclass for all script execution. - Execution flows through VM creation, Realm allocation, AST parsing (via
Parser::parse), and evaluation viaVM::runwith identifier resolution handled byVM::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_FUNCTIONmacro andverify_selftemplate to safely bridge C++ implementations to JavaScript calls. - Microtask processing completes the event loop via
VM::run_queued_promise_jobsafter 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →