From 2acff8d853c7c3859467104151997d9949027cfe Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Sat, 8 Oct 2022 16:58:55 -0600 Subject: [PATCH] LibWeb: Add new code generator for creating exposed interface helpers This new code generator takes all the .idl files in LibWeb, looks for each top level interface in there with an [Exposed=Foo] attribute, and adds code to add the constructor and prototype for each of those exposed interfaces to the realm of the relevant global object we're initialzing. It will soon replace WindowObjectHelper as the way that web interfaces are added to the Window object, and will be used in the future for creating proper WorkerGlobalScope objects for dedicated and shared workers. --- Meta/CMake/libweb_generators.cmake | 28 ++ .../CodeGenerators/LibWeb/CMakeLists.txt | 1 + .../GenerateWindowOrWorkerInterfaces.cpp | 311 ++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateWindowOrWorkerInterfaces.cpp diff --git a/Meta/CMake/libweb_generators.cmake b/Meta/CMake/libweb_generators.cmake index d4c0fec9cf..cce8ce7c33 100644 --- a/Meta/CMake/libweb_generators.cmake +++ b/Meta/CMake/libweb_generators.cmake @@ -158,8 +158,36 @@ function (generate_js_bindings target) add_dependencies(all_generated generate_${basename}Prototype.h) add_custom_target(generate_${basename}Prototype.cpp DEPENDS ${LIBWEB_OUTPUT_FOLDER}Bindings/${basename}Prototype.cpp) add_dependencies(all_generated generate_${basename}Prototype.cpp) + + list(APPEND LIBWEB_ALL_IDL_FILES "${LIBWEB_INPUT_FOLDER}/${class}.idl") + set(LIBWEB_ALL_IDL_FILES ${LIBWEB_ALL_IDL_FILES} PARENT_SCOPE) + endfunction() + + function(generate_exposed_interface_files) + set(exposed_interface_sources DedicatedWorkerExposedInterfaces.cpp DedicatedWorkerExposedInterfaces.h + SharedWorkerExposedInterfaces.cpp SharedWorkerExposedInterfaces.h + WindowExposedInterfaces.cpp WindowExposedInterfaces.h) + list(TRANSFORM exposed_interface_sources PREPEND "${LIBWEB_OUTPUT_FOLDER}Bindings/") + add_custom_command( + OUTPUT ${exposed_interface_sources} + COMMAND "${CMAKE_COMMAND}" -E make_directory "tmp" + COMMAND $ -o "${CMAKE_CURRENT_BINARY_DIR}/tmp" -b "${LIBWEB_INPUT_FOLDER}" ${LIBWEB_ALL_IDL_FILES} + COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/DedicatedWorkerExposedInterfaces.h "${LIBWEB_OUTPUT_FOLDER}Bindings/DedicatedWorkerExposedInterfaces.h" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/DedicatedWorkerExposedInterfaces.cpp "${LIBWEB_OUTPUT_FOLDER}Bindings/DedicatedWorkerExposedInterfaces.cpp" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/SharedWorkerExposedInterfaces.h "${LIBWEB_OUTPUT_FOLDER}Bindings/SharedWorkerExposedInterfaces.h" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/SharedWorkerExposedInterfaces.cpp "${LIBWEB_OUTPUT_FOLDER}Bindings/SharedWorkerExposedInterfaces.cpp" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/WindowExposedInterfaces.h "${LIBWEB_OUTPUT_FOLDER}Bindings/WindowExposedInterfaces.h" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/WindowExposedInterfaces.cpp "${LIBWEB_OUTPUT_FOLDER}Bindings/WindowExposedInterfaces.cpp" + COMMAND "${CMAKE_COMMAND}" -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/tmp" + VERBATIM + DEPENDS Lagom::GenerateWindowOrWorkerInterfaces ${LIBWEB_ALL_IDL_FILES} + ) + target_sources(${target} PRIVATE ${exposed_interface_sources}) + add_custom_target("generate_${LIBWEB_META_PREFIX}exposed_interfaces" DEPENDS ${exposed_interface_sources}) + add_dependencies(all_generated "generate_${LIBWEB_META_PREFIX}exposed_interfaces") endfunction() include("${LIBWEB_INPUT_FOLDER}/idl_files.cmake") + generate_exposed_interface_files() endfunction() diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt index 0b9d1f95f6..a56b9b87f0 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt @@ -5,5 +5,6 @@ lagom_tool(GenerateCSSMediaFeatureID SOURCES GenerateCSSMediaFeatureID.cpp lagom_tool(GenerateCSSPropertyID SOURCES GenerateCSSPropertyID.cpp LIBS LibMain) lagom_tool(GenerateCSSTransformFunctions SOURCES GenerateCSSTransformFunctions.cpp LIBS LibMain) lagom_tool(GenerateCSSValueID SOURCES GenerateCSSValueID.cpp LIBS LibMain) +lagom_tool(GenerateWindowOrWorkerInterfaces SOURCES GenerateWindowOrWorkerInterfaces.cpp LIBS LibMain LibIDL) add_subdirectory(BindingsGenerator) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateWindowOrWorkerInterfaces.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateWindowOrWorkerInterfaces.cpp new file mode 100644 index 0000000000..1a6a361637 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateWindowOrWorkerInterfaces.cpp @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2022, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static ErrorOr add_to_interface_sets(IDL::Interface&, Vector& window_exposed, Vector& dedicated_worker_exposed, Vector& shared_worker_exposed); +static String s_error_string; + +static ErrorOr generate_exposed_interface_header(StringView class_name, StringView output_path) +{ + StringBuilder builder; + SourceGenerator generator(builder); + + generator.set("global_object_snake_name", String(class_name).to_snakecase()); + generator.append(R"~~~( +#pragma once + +#include + +namespace Web::Bindings { + +void add_@global_object_snake_name@_exposed_interfaces(JS::Object&, JS::Realm&); + +} + +)~~~"); + + auto generated_header_path = LexicalPath(output_path).append(String::formatted("{}ExposedInterfaces.h", class_name)).string(); + auto generated_header_file = TRY(Core::Stream::File::open(generated_header_path, Core::Stream::OpenMode::Write)); + TRY(generated_header_file->write(generator.as_string_view().bytes())); + + return {}; +} + +static ErrorOr generate_exposed_interface_implementation(StringView class_name, StringView output_path, Vector& exposed_interfaces) +{ + StringBuilder builder; + SourceGenerator generator(builder); + + generator.set("global_object_name", class_name); + generator.set("global_object_snake_name", String(class_name).to_snakecase()); + + generator.append(R"~~~( +#include +#include +#include +)~~~"); + for (auto& interface : exposed_interfaces) { + auto gen = generator.fork(); + gen.set("prototype_class", interface.prototype_class); + gen.set("constructor_class", interface.constructor_class); + + gen.append(R"~~~(#include +)~~~"); + if (interface.parent_name != "[Synthetic Interface]"sv) + gen.append(R"~~~(#include +)~~~"); + } + + // FIXME: Special case window. We should convert Window, Location, and Navigator to use IDL + if (class_name == "Window"sv) { + generator.append(R"~~~(#include +#include +#include +#include +#include +#include +)~~~"); + } + + generator.append(R"~~~( +namespace Web::Bindings { + +void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global, JS::Realm& realm) +{ + auto& vm = global.vm(); + // FIXME: Should we use vm.current_realm() here? +)~~~"); + + auto add_interface = [](SourceGenerator& gen, StringView name, StringView prototype_class, StringView constructor_class) { + gen.set("interface_name", name); + gen.set("prototype_class", prototype_class); + gen.set("constructor_class", constructor_class); + + gen.append(R"~~~( { + auto& prototype = Bindings::ensure_web_prototype(realm, "@interface_name@"); + auto& constructor = Bindings::ensure_web_constructor(realm, "@interface_name@"); + global.define_direct_property("@interface_name@", &constructor, JS::Attribute::Writable | JS::Attribute::Configurable); + prototype.define_direct_property(vm.names.constructor, &constructor, JS::Attribute::Writable | JS::Attribute::Configurable); + constructor.define_direct_property(vm.names.name, js_string(vm, "@interface_name@"), JS::Attribute::Configurable); + } +)~~~"); }; + + for (auto& interface : exposed_interfaces) { + auto gen = generator.fork(); + add_interface(gen, interface.name, interface.prototype_class, interface.constructor_class); + } + + // FIXME: Special case window. We should convert Window, Location, and Navigator to use IDL + if (class_name == "Window"sv) { + auto gen = generator.fork(); + add_interface(gen, "Window"sv, "WindowPrototype"sv, "WindowConstructor"sv); + add_interface(gen, "Location"sv, "LocationPrototype"sv, "LocationConstructor"sv); + add_interface(gen, "Navigator"sv, "NavigatorPrototype"sv, "NavigatorConstructor"sv); + } + + generator.append(R"~~~( +} +} +)~~~"); + auto generated_implementation_path = LexicalPath(output_path).append(String::formatted("{}ExposedInterfaces.cpp", class_name)).string(); + auto generated_implementation_file = TRY(Core::Stream::File::open(generated_implementation_path, Core::Stream::OpenMode::Write)); + TRY(generated_implementation_file->write(generator.as_string_view().bytes())); + + return {}; +} + +ErrorOr serenity_main(Main::Arguments arguments) +{ + Core::ArgsParser args_parser; + + StringView output_path; + StringView base_path; + Vector paths; + + args_parser.add_option(output_path, "Path to output generated files into", "output-path", 'o', "output-path"); + args_parser.add_option(base_path, "Path to root of IDL file tree", "base-path", 'b', "base-path"); + args_parser.add_positional_argument(paths, "Paths of every IDL file that could be Exposed", "paths"); + args_parser.parse(arguments); + + VERIFY(!paths.is_empty()); + VERIFY(!base_path.is_empty()); + + const LexicalPath lexical_base(base_path); + + // Read in all IDL files, we must own the storage for all of these for the lifetime of the program + Vector file_contents; + for (String const& path : paths) { + auto file_or_error = Core::Stream::File::open(path, Core::Stream::OpenMode::Read); + if (file_or_error.is_error()) { + s_error_string = String::formatted("Unable to open file {}", path); + return Error::from_string_view(s_error_string); + } + auto file = file_or_error.release_value(); + auto string = MUST(file->read_all()); + file_contents.append(String(ReadonlyBytes(string))); + } + VERIFY(paths.size() == file_contents.size()); + + Vector parsers; + Vector window_exposed; + Vector dedicated_worker_exposed; + Vector shared_worker_exposed; + // TODO: service_worker_exposed + + for (size_t i = 0; i < paths.size(); ++i) { + IDL::Parser parser(paths[i], file_contents[i], lexical_base.string()); + TRY(add_to_interface_sets(parser.parse(), window_exposed, dedicated_worker_exposed, shared_worker_exposed)); + parsers.append(move(parser)); + } + + TRY(generate_exposed_interface_header("Window"sv, output_path)); + TRY(generate_exposed_interface_header("DedicatedWorker"sv, output_path)); + TRY(generate_exposed_interface_header("SharedWorker"sv, output_path)); + // TODO: ServiceWorkerExposed.h + + TRY(generate_exposed_interface_implementation("Window"sv, output_path, window_exposed)); + TRY(generate_exposed_interface_implementation("DedicatedWorker"sv, output_path, dedicated_worker_exposed)); + TRY(generate_exposed_interface_implementation("SharedWorker"sv, output_path, shared_worker_exposed)); + // TODO: ServiceWorkerExposed.cpp + + return 0; +} + +static void consume_whitespace(GenericLexer& lexer) +{ + bool consumed = true; + while (consumed) { + consumed = lexer.consume_while(is_ascii_space).length() > 0; + + if (lexer.consume_specific("//")) { + lexer.consume_until('\n'); + lexer.ignore(); + consumed = true; + } + } +} + +enum ExposedTo { + Nobody = 0x0, + DedicatedWorker = 0x1, + SharedWorker = 0x2, + ServiceWorker = 0x4, + AudioWorklet = 0x8, + Window = 0x10, + AllWorkers = 0xF, // FIXME: Is "AudioWorklet" a Worker? We'll assume it is for now + All = 0x1F, +}; +AK_ENUM_BITWISE_OPERATORS(ExposedTo); + +static ErrorOr parse_exposure_set(IDL::Interface& interface) +{ + // NOTE: This roughly follows the definitions of https://webidl.spec.whatwg.org/#Exposed + // It does not remotely interpret all the abstract operations therein though. + + auto maybe_exposed = interface.extended_attributes.get("Exposed"); + if (!maybe_exposed.has_value()) { + s_error_string = String::formatted("Interface {} is missing extended attribute Exposed", interface.name); + return Error::from_string_view(s_error_string); + } + auto exposed = maybe_exposed.value().trim_whitespace(); + if (exposed == "*"sv) + return ExposedTo::All; + if (exposed == "Window"sv) + return ExposedTo::Window; + if (exposed == "Worker"sv) + return ExposedTo::AllWorkers; + if (exposed == "AudioWorklet"sv) + return ExposedTo::AudioWorklet; + + if (exposed[0] == '(') { + ExposedTo whom = Nobody; + for (StringView candidate : exposed.substring_view(1, exposed.length() - 1).split_view(',')) { + candidate = candidate.trim_whitespace(); + if (candidate == "Window"sv) { + whom |= ExposedTo::Window; + } else if (candidate == "Worker"sv) { + whom |= ExposedTo::AllWorkers; + } else if (candidate == "DedicatedWorker"sv) { + whom |= ExposedTo::DedicatedWorker; + } else if (candidate == "SharedWorker"sv) { + whom |= ExposedTo::SharedWorker; + } else if (candidate == "ServiceWorker"sv) { + whom |= ExposedTo::ServiceWorker; + } else if (candidate == "AudioWorklet"sv) { + whom |= ExposedTo::AudioWorklet; + } else { + s_error_string = String::formatted("Unknown Exposed attribute candidate {} in {} in {}", candidate, exposed, interface.name); + return Error::from_string_view(s_error_string); + } + } + if (whom == ExposedTo::Nobody) { + s_error_string = String::formatted("Unknown Exposed attribute {} in {}", exposed, interface.name); + return Error::from_string_view(s_error_string); + } + return whom; + } + + s_error_string = String::formatted("Unknown Exposed attribute {} in {}", exposed, interface.name); + return Error::from_string_view(s_error_string); +} + +static IDL::Interface& add_synthetic_interface(IDL::Interface& reference_interface) +{ + static Vector> s_synthetic_interfaces; + + GenericLexer function_lexer(reference_interface.extended_attributes.get("LegacyFactoryFunction").value()); + consume_whitespace(function_lexer); + auto name = function_lexer.consume_until([](auto ch) { return is_ascii_space(ch) || ch == '('; }); + + auto new_interface = make(); + new_interface->name = name; + new_interface->constructor_class = String::formatted("{}Constructor", new_interface->name); + new_interface->prototype_class = reference_interface.prototype_class; + new_interface->parent_name = "[Synthetic Interface]"; + + s_synthetic_interfaces.append(move(new_interface)); + return *s_synthetic_interfaces.last(); +} + +ErrorOr add_to_interface_sets(IDL::Interface& interface, Vector& window_exposed, Vector& dedicated_worker_exposed, Vector& shared_worker_exposed) +{ + // TODO: Add service worker exposed and audio worklet exposed + auto whom = TRY(parse_exposure_set(interface)); + VERIFY(whom != ExposedTo::Nobody); + + if (whom & ExposedTo::Window) + window_exposed.append(interface); + + if (whom & ExposedTo::DedicatedWorker) + dedicated_worker_exposed.append(interface); + + if (whom & ExposedTo::SharedWorker) + shared_worker_exposed.append(interface); + + if (interface.extended_attributes.contains("LegacyFactoryFunction")) { + auto& synthetic_interface = add_synthetic_interface(interface); + if (whom & ExposedTo::Window) + window_exposed.append(synthetic_interface); + + if (whom & ExposedTo::DedicatedWorker) + dedicated_worker_exposed.append(synthetic_interface); + + if (whom & ExposedTo::SharedWorker) + shared_worker_exposed.append(synthetic_interface); + } + + return {}; +}