diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 34972fb6b8..804f6a5c89 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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() diff --git a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp index 497cb38c53..5978bf7580 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,8 @@ #include #include #include +#include +#include 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 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 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>> HTMLParser::parse_html_fragment GC::Ref HTMLParser::create_for_scripting(DOM::Document& document) { auto scripting_mode = document.is_scripting_enabled() ? ParserScriptingMode::Normal : ParserScriptingMode::Disabled; - return document.realm().create(document, scripting_mode, ScriptCreatedParser::Yes); + return document.realm().create(document, scripting_mode, ScriptCreatedParser::Yes, default_html_parser_backend()); } GC::Ref HTMLParser::create_with_open_input_stream(DOM::Document& document) { auto scripting_mode = document.is_scripting_enabled() ? ParserScriptingMode::Normal : ParserScriptingMode::Disabled; - return document.realm().create(document, scripting_mode, ScriptCreatedParser::No); + return document.realm().create(document, scripting_mode, ScriptCreatedParser::No, default_html_parser_backend()); } GC::Ref HTMLParser::create_with_uncertain_encoding(DOM::Document& document, ByteBuffer const& input, Optional maybe_mime_type) { auto scripting_mode = document.is_scripting_enabled() ? ParserScriptingMode::Normal : ParserScriptingMode::Disabled; if (document.has_encoding()) - return document.realm().create(document, scripting_mode, input, document.encoding().value().to_byte_string()); + return document.realm().create(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(document, scripting_mode, input, encoding); + return document.realm().create(document, scripting_mode, input, encoding, default_html_parser_backend()); } -GC::Ref HTMLParser::create(DOM::Document& document, StringView input, ParserScriptingMode scripting_mode, StringView encoding) +GC::Ref HTMLParser::create(DOM::Document& document, StringView input, ParserScriptingMode scripting_mode, StringView encoding, HTMLParserBackend backend) { - return document.realm().create(document, scripting_mode, input, encoding); + return document.realm().create(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 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(parser); +} + +static DOM::Node& node_from_html_parser_ffi(uintptr_t node) +{ + VERIFY(node); + return *reinterpret_cast(node); +} + +extern "C" uintptr_t ladybird_html_parser_document_node(void* parser) +{ + return reinterpret_cast(&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(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(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(html_parser.document(), Utf16String::from_utf8(string_from_html_parser_ffi(data_ptr, data_len))); + return reinterpret_cast(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(html_parser.document(), Utf16String::from_utf8(string_from_html_parser_ffi(data_ptr, data_len))); + return reinterpret_cast(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(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(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))); +} + } diff --git a/Libraries/LibWeb/HTML/Parser/HTMLParser.h b/Libraries/LibWeb/HTML/Parser/HTMLParser.h index 8d67bff963..91f1ac38fe 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLParser.h +++ b/Libraries/LibWeb/HTML/Parser/HTMLParser.h @@ -19,6 +19,15 @@ namespace Web::HTML { +enum class HTMLParserBackend : u8 { + Cpp, + Rust, +}; + +WEB_API Optional 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 create_for_scripting(DOM::Document&); static GC::Ref create_with_open_input_stream(DOM::Document&); static GC::Ref create_with_uncertain_encoding(DOM::Document&, ByteBuffer const& input, Optional maybe_mime_type = {}); - static GC::Ref create(DOM::Document&, StringView input, ParserScriptingMode, StringView encoding); + static GC::Ref 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 }; diff --git a/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.h b/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.h index dac5355f00..41eb81c536 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.h +++ b/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.h @@ -17,6 +17,7 @@ #include struct RustFfiTokenizerHandle; +struct RustFfiHtmlParserHandle; namespace Web::HTML { diff --git a/Libraries/LibWeb/HTML/Parser/Rust/src/lib.rs b/Libraries/LibWeb/HTML/Parser/Rust/src/lib.rs index 0a1cf4468b..479dc4c677 100644 --- a/Libraries/LibWeb/HTML/Parser/Rust/src/lib.rs +++ b/Libraries/LibWeb/HTML/Parser/Rust/src/lib.rs @@ -6,6 +6,7 @@ pub mod entities; pub mod interned_names; +pub mod parser; pub mod token; pub mod tokenizer; diff --git a/Libraries/LibWeb/HTML/Parser/Rust/src/parser.rs b/Libraries/LibWeb/HTML/Parser/Rust/src/parser.rs new file mode 100644 index 0000000000..3c64cc03cf --- /dev/null +++ b/Libraries/LibWeb/HTML/Parser/Rust/src/parser.rs @@ -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) }); + } +} diff --git a/Utilities/dump-html-tree.cpp b/Utilities/dump-html-tree.cpp index 8b4f0b42ea..4af3beecc6 100644 --- a/Utilities/dump-html-tree.cpp +++ b/Utilities/dump-html-tree.cpp @@ -280,7 +280,7 @@ static void dump_tree(Web::DOM::Node const& node, size_t indent) } } -static GC::Ref parse_html(JS::Realm& realm, Web::DOM::Document const& origin_document, StringView input) +static GC::Ref 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 parse_html(JS::Realm& realm, Web::DOM::Docume document->set_allow_declarative_shadow_roots(true); document->set_custom_element_registry(realm.create(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 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 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 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(elapsed_ms) / iterations);