LibWeb: Add Rust HTML parser host plumbing

Add the C++ and Rust scaffolding that lets the tree builder live in
Rust while the DOM remains owned by LibWeb. Keep the exported surface
small: Rust stores parser state, and C++ provides node creation,
insertion, script, template, and GC hooks.

Route dump-html-tree through the selectable parser backend so the new
implementation can be exercised beside the existing parser while it is
being brought up.
This commit is contained in:
Andreas Kling 2026-05-15 20:29:23 +02:00 committed by Andreas Kling
parent 5b63cb5f37
commit 09296315c2
7 changed files with 321 additions and 19 deletions

View file

@ -1255,6 +1255,7 @@ import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libweb_rust FFI_HEADE
target_link_libraries(LibWeb PRIVATE libweb_rust)
get_target_property(_libweb_rust_lib libweb_rust IMPORTED_LOCATION)
set_property(SOURCE HTML/Parser/HTMLTokenizer.cpp APPEND PROPERTY OBJECT_DEPENDS ${_libweb_rust_lib})
set_property(SOURCE HTML/Parser/HTMLParser.cpp APPEND PROPERTY OBJECT_DEPENDS ${_libweb_rust_lib})
if ((LINUX OR BSD) AND NOT BUILD_SHARED_LIBS)
target_link_options(LibWeb INTERFACE LINKER:--allow-multiple-definition)
endif()

View file

@ -46,6 +46,7 @@
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/SimilarOriginWindowAgent.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTMLTokenizerRustFFI.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
@ -54,6 +55,8 @@
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/SVG/SVGScriptElement.h>
#include <LibWeb/SVG/TagNames.h>
#include <stdlib.h>
#include <string.h>
namespace Web::HTML {
@ -65,6 +68,40 @@ static inline void log_parse_error(SourceLocation const& location = SourceLocati
dbgln_if(HTML_PARSER_DEBUG, "Parse error! {}", location);
}
Optional<HTMLParserBackend> html_parser_backend_from_string(StringView backend)
{
if (backend == "cpp"sv)
return HTMLParserBackend::Cpp;
if (backend == "rust"sv)
return HTMLParserBackend::Rust;
return {};
}
StringView html_parser_backend_name(HTMLParserBackend backend)
{
switch (backend) {
case HTMLParserBackend::Cpp:
return "cpp"sv;
case HTMLParserBackend::Rust:
return "rust"sv;
}
VERIFY_NOT_REACHED();
}
HTMLParserBackend default_html_parser_backend()
{
static HTMLParserBackend s_backend = [] {
auto* backend = getenv("LIBWEB_HTML_PARSER");
if (!backend)
return HTMLParserBackend::Cpp;
if (auto parsed_backend = html_parser_backend_from_string(StringView { backend, strlen(backend) }); parsed_backend.has_value())
return parsed_backend.value();
dbgln("Unknown LIBWEB_HTML_PARSER value '{}'; using cpp", backend);
return HTMLParserBackend::Cpp;
}();
return s_backend;
}
static Vector<StringView> const s_quirks_public_ids = {
"+//Silmaril//dtd html Pro v0r11 19970101//"sv,
"-//AS//DTD HTML 3.0 asWedit + extensions//"sv,
@ -159,11 +196,14 @@ static bool is_html_integration_point(DOM::Element const& element)
return false;
}
HTMLParser::HTMLParser(DOM::Document& document, ParserScriptingMode scripting_mode, StringView input, StringView encoding)
HTMLParser::HTMLParser(DOM::Document& document, ParserScriptingMode scripting_mode, StringView input, StringView encoding, HTMLParserBackend backend)
: m_tokenizer(input, encoding)
, m_backend(backend)
, m_scripting_mode(scripting_mode)
, m_document(document)
{
if (m_backend == HTMLParserBackend::Rust)
m_rust_parser = rust_html_parser_create();
m_tokenizer.set_parser({}, *this);
m_document->set_parser({}, *this);
m_stack_of_open_elements.set_on_element_popped([this](DOM::Element& element) {
@ -174,11 +214,14 @@ HTMLParser::HTMLParser(DOM::Document& document, ParserScriptingMode scripting_mo
m_document->set_encoding(MUST(String::from_utf8(standardized_encoding.value())));
}
HTMLParser::HTMLParser(DOM::Document& document, ParserScriptingMode scripting_mode, ScriptCreatedParser script_created)
: m_scripting_mode(scripting_mode)
HTMLParser::HTMLParser(DOM::Document& document, ParserScriptingMode scripting_mode, ScriptCreatedParser script_created, HTMLParserBackend backend)
: m_backend(backend)
, m_scripting_mode(scripting_mode)
, m_script_created(script_created == ScriptCreatedParser::Yes)
, m_document(document)
{
if (m_backend == HTMLParserBackend::Rust)
m_rust_parser = rust_html_parser_create();
m_document->set_parser({}, *this);
m_tokenizer.set_parser({}, *this);
m_stack_of_open_elements.set_on_element_popped([this](DOM::Element& element) {
@ -186,8 +229,15 @@ HTMLParser::HTMLParser(DOM::Document& document, ParserScriptingMode scripting_mo
});
}
HTMLParser::~HTMLParser()
HTMLParser::~HTMLParser() = default;
void HTMLParser::finalize()
{
Base::finalize();
if (m_rust_parser) {
rust_html_parser_destroy(m_rust_parser);
m_rust_parser = nullptr;
}
}
void HTMLParser::visit_edges(Cell::Visitor& visitor)
@ -212,6 +262,11 @@ void HTMLParser::initialize(JS::Realm& realm)
void HTMLParser::run(HTMLTokenizer::StopAtInsertionPoint stop_at_insertion_point)
{
if (m_backend == HTMLParserBackend::Rust) {
auto result = rust_html_parser_run_document(m_rust_parser);
VERIFY(result == RustFfiHtmlParserRunResult::Unsupported);
}
m_stop_parsing = false;
for (;;) {
@ -5189,28 +5244,28 @@ WebIDL::ExceptionOr<Vector<GC::Root<DOM::Node>>> HTMLParser::parse_html_fragment
GC::Ref<HTMLParser> HTMLParser::create_for_scripting(DOM::Document& document)
{
auto scripting_mode = document.is_scripting_enabled() ? ParserScriptingMode::Normal : ParserScriptingMode::Disabled;
return document.realm().create<HTMLParser>(document, scripting_mode, ScriptCreatedParser::Yes);
return document.realm().create<HTMLParser>(document, scripting_mode, ScriptCreatedParser::Yes, default_html_parser_backend());
}
GC::Ref<HTMLParser> HTMLParser::create_with_open_input_stream(DOM::Document& document)
{
auto scripting_mode = document.is_scripting_enabled() ? ParserScriptingMode::Normal : ParserScriptingMode::Disabled;
return document.realm().create<HTMLParser>(document, scripting_mode, ScriptCreatedParser::No);
return document.realm().create<HTMLParser>(document, scripting_mode, ScriptCreatedParser::No, default_html_parser_backend());
}
GC::Ref<HTMLParser> HTMLParser::create_with_uncertain_encoding(DOM::Document& document, ByteBuffer const& input, Optional<MimeSniff::MimeType> maybe_mime_type)
{
auto scripting_mode = document.is_scripting_enabled() ? ParserScriptingMode::Normal : ParserScriptingMode::Disabled;
if (document.has_encoding())
return document.realm().create<HTMLParser>(document, scripting_mode, input, document.encoding().value().to_byte_string());
return document.realm().create<HTMLParser>(document, scripting_mode, input, document.encoding().value().to_byte_string(), default_html_parser_backend());
auto encoding = run_encoding_sniffing_algorithm(document, input, maybe_mime_type);
dbgln_if(HTML_PARSER_DEBUG, "The encoding sniffing algorithm returned encoding '{}'", encoding);
return document.realm().create<HTMLParser>(document, scripting_mode, input, encoding);
return document.realm().create<HTMLParser>(document, scripting_mode, input, encoding, default_html_parser_backend());
}
GC::Ref<HTMLParser> HTMLParser::create(DOM::Document& document, StringView input, ParserScriptingMode scripting_mode, StringView encoding)
GC::Ref<HTMLParser> HTMLParser::create(DOM::Document& document, StringView input, ParserScriptingMode scripting_mode, StringView encoding, HTMLParserBackend backend)
{
return document.realm().create<HTMLParser>(document, scripting_mode, input, encoding);
return document.realm().create<HTMLParser>(document, scripting_mode, input, encoding, backend);
}
enum class AttributeMode {
@ -5902,4 +5957,141 @@ void HTMLParser::insert_an_element_at_the_adjusted_insertion_location(GC::Ref<DO
}
}
static StringView html_parser_ffi_string_view(u8 const* ptr, size_t len)
{
if (ptr == nullptr || len == 0)
return {};
return { ptr, len };
}
static FlyString fly_string_from_html_parser_ffi(u8 const* ptr, size_t len)
{
return MUST(FlyString::from_utf8(html_parser_ffi_string_view(ptr, len)));
}
static String string_from_html_parser_ffi(u8 const* ptr, size_t len)
{
return MUST(String::from_utf8(html_parser_ffi_string_view(ptr, len)));
}
enum class LadybirdHtmlParserNamespace {
HTML,
MathML,
SVG,
};
enum class LadybirdHtmlParserQuirksMode {
No,
Limited,
Yes,
};
struct LadybirdHtmlParserAttribute {
u8 const* local_name_ptr;
size_t local_name_len;
u8 const* value_ptr;
size_t value_len;
};
extern "C" uintptr_t ladybird_html_parser_document_node(void*);
extern "C" void ladybird_html_parser_set_document_quirks_mode(void*, LadybirdHtmlParserQuirksMode);
extern "C" uintptr_t ladybird_html_parser_create_document_type(void*, u8 const*, size_t, u8 const*, size_t, u8 const*, size_t);
extern "C" uintptr_t ladybird_html_parser_create_comment(void*, u8 const*, size_t);
extern "C" uintptr_t ladybird_html_parser_create_text_node(void*, u8 const*, size_t);
extern "C" uintptr_t ladybird_html_parser_create_element(void*, LadybirdHtmlParserNamespace, u8 const*, size_t, LadybirdHtmlParserAttribute const*, size_t);
extern "C" void ladybird_html_parser_append_child(uintptr_t, uintptr_t);
static Optional<FlyString> namespace_from_html_parser_ffi(LadybirdHtmlParserNamespace namespace_)
{
switch (namespace_) {
case LadybirdHtmlParserNamespace::HTML:
return Namespace::HTML;
case LadybirdHtmlParserNamespace::MathML:
return Namespace::MathML;
case LadybirdHtmlParserNamespace::SVG:
return Namespace::SVG;
}
VERIFY_NOT_REACHED();
}
static DOM::QuirksMode quirks_mode_from_html_parser_ffi(LadybirdHtmlParserQuirksMode mode)
{
switch (mode) {
case LadybirdHtmlParserQuirksMode::No:
return DOM::QuirksMode::No;
case LadybirdHtmlParserQuirksMode::Limited:
return DOM::QuirksMode::Limited;
case LadybirdHtmlParserQuirksMode::Yes:
return DOM::QuirksMode::Yes;
}
VERIFY_NOT_REACHED();
}
static HTMLParser& parser_from_html_parser_ffi(void* parser)
{
VERIFY(parser);
return *reinterpret_cast<HTMLParser*>(parser);
}
static DOM::Node& node_from_html_parser_ffi(uintptr_t node)
{
VERIFY(node);
return *reinterpret_cast<DOM::Node*>(node);
}
extern "C" uintptr_t ladybird_html_parser_document_node(void* parser)
{
return reinterpret_cast<uintptr_t>(&parser_from_html_parser_ffi(parser).document());
}
extern "C" void ladybird_html_parser_set_document_quirks_mode(void* parser, LadybirdHtmlParserQuirksMode mode)
{
parser_from_html_parser_ffi(parser).document().set_quirks_mode(quirks_mode_from_html_parser_ffi(mode));
}
extern "C" uintptr_t ladybird_html_parser_create_document_type(void* parser, u8 const* name_ptr, size_t name_len, u8 const* public_id_ptr, size_t public_id_len, u8 const* system_id_ptr, size_t system_id_len)
{
auto& html_parser = parser_from_html_parser_ffi(parser);
auto document_type = html_parser.document().realm().create<DOM::DocumentType>(html_parser.document());
document_type->set_name(string_from_html_parser_ffi(name_ptr, name_len));
document_type->set_public_id(string_from_html_parser_ffi(public_id_ptr, public_id_len));
document_type->set_system_id(string_from_html_parser_ffi(system_id_ptr, system_id_len));
return reinterpret_cast<uintptr_t>(document_type.ptr());
}
extern "C" uintptr_t ladybird_html_parser_create_comment(void* parser, u8 const* data_ptr, size_t data_len)
{
auto& html_parser = parser_from_html_parser_ffi(parser);
auto comment = html_parser.document().realm().create<DOM::Comment>(html_parser.document(), Utf16String::from_utf8(string_from_html_parser_ffi(data_ptr, data_len)));
return reinterpret_cast<uintptr_t>(comment.ptr());
}
extern "C" uintptr_t ladybird_html_parser_create_text_node(void* parser, u8 const* data_ptr, size_t data_len)
{
auto& html_parser = parser_from_html_parser_ffi(parser);
auto text = html_parser.document().realm().create<DOM::Text>(html_parser.document(), Utf16String::from_utf8(string_from_html_parser_ffi(data_ptr, data_len)));
return reinterpret_cast<uintptr_t>(text.ptr());
}
extern "C" uintptr_t ladybird_html_parser_create_element(void* parser, LadybirdHtmlParserNamespace namespace_, u8 const* local_name_ptr, size_t local_name_len, LadybirdHtmlParserAttribute const* attributes, size_t attribute_count)
{
auto& html_parser = parser_from_html_parser_ffi(parser);
auto local_name = fly_string_from_html_parser_ffi(local_name_ptr, local_name_len);
auto element = DOM::create_element(html_parser.document(), local_name, namespace_from_html_parser_ffi(namespace_)).release_value_but_fixme_should_propagate_errors();
for (size_t i = 0; i < attribute_count; ++i) {
auto const& attribute = attributes[i];
DOM::QualifiedName qualified_name { fly_string_from_html_parser_ffi(attribute.local_name_ptr, attribute.local_name_len), {}, {} };
auto dom_attribute = html_parser.document().realm().create<DOM::Attr>(html_parser.document(), move(qualified_name), string_from_html_parser_ffi(attribute.value_ptr, attribute.value_len), element);
element->append_attribute(dom_attribute);
}
return reinterpret_cast<uintptr_t>(element.ptr());
}
extern "C" void ladybird_html_parser_append_child(uintptr_t parent, uintptr_t child)
{
MUST(node_from_html_parser_ffi(parent).append_child(node_from_html_parser_ffi(child)));
}
}

View file

@ -19,6 +19,15 @@
namespace Web::HTML {
enum class HTMLParserBackend : u8 {
Cpp,
Rust,
};
WEB_API Optional<HTMLParserBackend> html_parser_backend_from_string(StringView);
WEB_API StringView html_parser_backend_name(HTMLParserBackend);
WEB_API HTMLParserBackend default_html_parser_backend();
#define ENUMERATE_INSERTION_MODES \
__ENUMERATE_INSERTION_MODE(Initial) \
__ENUMERATE_INSERTION_MODE(BeforeHTML) \
@ -49,12 +58,14 @@ class WEB_API HTMLParser final : public JS::Cell {
friend class HTMLTokenizer;
public:
~HTMLParser();
static constexpr bool OVERRIDES_FINALIZE = true;
virtual ~HTMLParser() override;
static GC::Ref<HTMLParser> create_for_scripting(DOM::Document&);
static GC::Ref<HTMLParser> create_with_open_input_stream(DOM::Document&);
static GC::Ref<HTMLParser> create_with_uncertain_encoding(DOM::Document&, ByteBuffer const& input, Optional<MimeSniff::MimeType> maybe_mime_type = {});
static GC::Ref<HTMLParser> create(DOM::Document&, StringView input, ParserScriptingMode, StringView encoding);
static GC::Ref<HTMLParser> create(DOM::Document&, StringView input, ParserScriptingMode, StringView encoding, HTMLParserBackend = default_html_parser_backend());
void run(HTMLTokenizer::StopAtInsertionPoint = HTMLTokenizer::StopAtInsertionPoint::No);
void run(URL::URL const&, HTMLTokenizer::StopAtInsertionPoint = HTMLTokenizer::StopAtInsertionPoint::No);
@ -106,11 +117,12 @@ private:
Yes,
};
HTMLParser(DOM::Document&, ParserScriptingMode, StringView input, StringView encoding);
HTMLParser(DOM::Document&, ParserScriptingMode, ScriptCreatedParser);
HTMLParser(DOM::Document&, ParserScriptingMode, StringView input, StringView encoding, HTMLParserBackend);
HTMLParser(DOM::Document&, ParserScriptingMode, ScriptCreatedParser, HTMLParserBackend);
virtual void visit_edges(Cell::Visitor&) override;
virtual void initialize(JS::Realm&) override;
virtual void finalize() override;
char const* insertion_mode_name() const;
@ -209,6 +221,8 @@ private:
ListOfActiveFormattingElements m_list_of_active_formatting_elements;
HTMLTokenizer m_tokenizer;
HTMLParserBackend m_backend { HTMLParserBackend::Cpp };
RustFfiHtmlParserHandle* m_rust_parser { nullptr };
bool m_next_line_feed_can_be_ignored { false };

View file

@ -17,6 +17,7 @@
#include <LibWeb/HTML/Parser/HTMLToken.h>
struct RustFfiTokenizerHandle;
struct RustFfiHtmlParserHandle;
namespace Web::HTML {

View file

@ -6,6 +6,7 @@
pub mod entities;
pub mod interned_names;
pub mod parser;
pub mod token;
pub mod tokenizer;

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#[repr(C)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RustFfiHtmlParserRunResult {
Ok = 0,
Unsupported = 1,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RustFfiHtmlNamespace {
Html = 0,
MathMl = 1,
Svg = 2,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RustFfiHtmlQuirksMode {
No = 0,
Limited = 1,
Yes = 2,
}
#[repr(C)]
pub struct RustFfiHtmlParserAttribute {
pub local_name_ptr: *const u8,
pub local_name_len: usize,
pub value_ptr: *const u8,
pub value_len: usize,
}
/// Opaque handle for the Rust HTML parser, passed across the FFI boundary.
pub struct RustFfiHtmlParserHandle {
run_count: u64,
}
/// Create a new Rust HTML parser.
#[unsafe(no_mangle)]
pub extern "C" fn rust_html_parser_create() -> *mut RustFfiHtmlParserHandle {
Box::into_raw(Box::new(RustFfiHtmlParserHandle { run_count: 0 }))
}
/// Run the Rust HTML parser.
///
/// This is intentionally small while the C++ host side is being carved out:
/// it proves the selectable Rust parser object is linked and reachable, and
/// gives the next step a stable ABI to extend with DOM host callbacks.
///
/// # Safety
/// `handle` must be a valid pointer from `rust_html_parser_create`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_html_parser_run_document(
handle: *mut RustFfiHtmlParserHandle,
) -> RustFfiHtmlParserRunResult {
if handle.is_null() {
return RustFfiHtmlParserRunResult::Unsupported;
}
let handle = unsafe { &mut *handle };
handle.run_count = handle.run_count.wrapping_add(1);
RustFfiHtmlParserRunResult::Unsupported
}
/// Return how many times this parser handle has been asked to run.
///
/// # Safety
/// `handle` must be a valid pointer from `rust_html_parser_create`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_html_parser_run_count(handle: *const RustFfiHtmlParserHandle) -> u64 {
if handle.is_null() {
return 0;
}
let handle = unsafe { &*handle };
handle.run_count
}
/// Destroy a Rust HTML parser.
///
/// # Safety
/// `handle` must be a valid pointer from `rust_html_parser_create`,
/// and must not be used after this call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_html_parser_destroy(handle: *mut RustFfiHtmlParserHandle) {
if !handle.is_null() {
drop(unsafe { Box::from_raw(handle) });
}
}

View file

@ -280,7 +280,7 @@ static void dump_tree(Web::DOM::Node const& node, size_t indent)
}
}
static GC::Ref<Web::DOM::Document> parse_html(JS::Realm& realm, Web::DOM::Document const& origin_document, StringView input)
static GC::Ref<Web::DOM::Document> parse_html(JS::Realm& realm, Web::DOM::Document const& origin_document, StringView input, Web::HTML::HTMLParserBackend backend)
{
auto document = Web::DOM::Document::create(realm, URL::about_blank());
document->set_document_type(Web::DOM::Document::Type::HTML);
@ -290,7 +290,7 @@ static GC::Ref<Web::DOM::Document> parse_html(JS::Realm& realm, Web::DOM::Docume
document->set_allow_declarative_shadow_roots(true);
document->set_custom_element_registry(realm.create<Web::HTML::CustomElementRegistry>(realm));
auto parser = Web::HTML::HTMLParser::create(document, input, Web::HTML::ParserScriptingMode::Disabled, "UTF-8"sv);
auto parser = Web::HTML::HTMLParser::create(document, input, Web::HTML::ParserScriptingMode::Disabled, "UTF-8"sv, backend);
parser->run(URL::about_blank());
return document;
}
@ -312,7 +312,8 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
args_parser.add_option(iterations, "Run the parser N times, reporting timing unless --silent is used", "iterations", 'n', "count");
args_parser.parse(arguments);
if (parser_name != "cpp"sv) {
auto backend = Web::HTML::html_parser_backend_from_string(parser_name);
if (!backend.has_value()) {
warnln("Unknown or unavailable parser backend: '{}'", parser_name);
return 1;
}
@ -353,7 +354,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
auto timer = Core::ElapsedTimer::start_new();
for (int i = 0; i < iterations; i++) {
auto document = parse_html(realm, origin_document, input);
auto document = parse_html(realm, origin_document, input, backend.value());
if (!silent && i == 0)
dump_tree(document);
}
@ -362,7 +363,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
auto elapsed_ms = timer.elapsed_milliseconds();
warnln("input={}B parser={} iterations={} total={}ms ({:.3f}ms/iter)",
input_data.size(),
parser_name,
Web::HTML::html_parser_backend_name(backend.value()),
iterations,
elapsed_ms,
static_cast<double>(elapsed_ms) / iterations);