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 {}; +}