Serenity Operating System
1/*
2 * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/DeprecatedString.h>
8#include <AK/LexicalPath.h>
9#include <AK/SourceGenerator.h>
10#include <AK/StringBuilder.h>
11#include <LibCore/ArgsParser.h>
12#include <LibCore/File.h>
13#include <LibIDL/IDLParser.h>
14#include <LibIDL/Types.h>
15#include <LibMain/Main.h>
16
17static ErrorOr<void> add_to_interface_sets(IDL::Interface&, Vector<IDL::Interface&>& intrinsics, Vector<IDL::Interface&>& window_exposed, Vector<IDL::Interface&>& dedicated_worker_exposed, Vector<IDL::Interface&>& shared_worker_exposed);
18static DeprecatedString s_error_string;
19
20struct LegacyConstructor {
21 DeprecatedString name;
22 DeprecatedString constructor_class;
23};
24
25static void consume_whitespace(GenericLexer& lexer)
26{
27 bool consumed = true;
28 while (consumed) {
29 consumed = lexer.consume_while(is_ascii_space).length() > 0;
30
31 if (lexer.consume_specific("//")) {
32 lexer.consume_until('\n');
33 lexer.ignore();
34 consumed = true;
35 }
36 }
37}
38
39static Optional<LegacyConstructor> const& lookup_legacy_constructor(IDL::Interface& interface)
40{
41 static HashMap<StringView, Optional<LegacyConstructor>> s_legacy_constructors;
42 if (auto cache = s_legacy_constructors.get(interface.name); cache.has_value())
43 return cache.value();
44
45 auto attribute = interface.extended_attributes.get("LegacyFactoryFunction"sv);
46 if (!attribute.has_value()) {
47 s_legacy_constructors.set(interface.name, {});
48 return s_legacy_constructors.get(interface.name).value();
49 }
50
51 GenericLexer function_lexer(attribute.value());
52 consume_whitespace(function_lexer);
53
54 auto name = function_lexer.consume_until([](auto ch) { return is_ascii_space(ch) || ch == '('; });
55 auto constructor_class = DeprecatedString::formatted("{}Constructor", name);
56
57 s_legacy_constructors.set(interface.name, LegacyConstructor { name, move(constructor_class) });
58 return s_legacy_constructors.get(interface.name).value();
59}
60
61static ErrorOr<void> generate_forwarding_header(StringView output_path, Vector<IDL::Interface&>& exposed_interfaces)
62{
63 StringBuilder builder;
64 SourceGenerator generator(builder);
65
66 generator.append(R"~~~(
67#pragma once
68
69namespace Web::Bindings {
70)~~~");
71
72 auto add_interface = [](SourceGenerator& gen, StringView prototype_class, StringView constructor_class, Optional<LegacyConstructor> const& legacy_constructor) {
73 gen.set("prototype_class", prototype_class);
74 gen.set("constructor_class", constructor_class);
75
76 gen.append(R"~~~(
77class @prototype_class@;
78class @constructor_class@;)~~~");
79
80 if (legacy_constructor.has_value()) {
81 gen.set("legacy_constructor_class", legacy_constructor->constructor_class);
82 gen.append(R"~~~(
83class @legacy_constructor_class@;)~~~");
84 }
85 };
86
87 for (auto& interface : exposed_interfaces) {
88 auto gen = generator.fork();
89 add_interface(gen, interface.prototype_class, interface.constructor_class, lookup_legacy_constructor(interface));
90 }
91
92 // FIXME: Special case WebAssembly. We should convert WASM to use IDL.
93 {
94 auto gen = generator.fork();
95 add_interface(gen, "WebAssemblyMemoryPrototype"sv, "WebAssemblyMemoryConstructor"sv, {});
96 add_interface(gen, "WebAssemblyInstancePrototype"sv, "WebAssemblyInstanceConstructor"sv, {});
97 add_interface(gen, "WebAssemblyModulePrototype"sv, "WebAssemblyModuleConstructor"sv, {});
98 add_interface(gen, "WebAssemblyTablePrototype"sv, "WebAssemblyTableConstructor"sv, {});
99 }
100
101 generator.append(R"~~~(
102
103}
104)~~~");
105
106 auto generated_forward_path = LexicalPath(output_path).append("Forward.h"sv).string();
107 auto generated_forward_file = TRY(Core::File::open(generated_forward_path, Core::File::OpenMode::Write));
108 TRY(generated_forward_file->write_until_depleted(generator.as_string_view().bytes()));
109
110 return {};
111}
112
113static ErrorOr<void> generate_intrinsic_definitions(StringView output_path, Vector<IDL::Interface&>& exposed_interfaces)
114{
115 StringBuilder builder;
116 SourceGenerator generator(builder);
117
118 generator.append(R"~~~(
119#include <LibJS/Heap/DeferGC.h>
120#include <LibJS/Runtime/Object.h>
121#include <LibWeb/Bindings/Intrinsics.h>)~~~");
122
123 for (auto& interface : exposed_interfaces) {
124 auto gen = generator.fork();
125 gen.set("prototype_class", interface.prototype_class);
126 gen.set("constructor_class", interface.constructor_class);
127
128 gen.append(R"~~~(
129#include <LibWeb/Bindings/@constructor_class@.h>
130#include <LibWeb/Bindings/@prototype_class@.h>)~~~");
131
132 if (auto const& legacy_constructor = lookup_legacy_constructor(interface); legacy_constructor.has_value()) {
133 gen.set("legacy_constructor_class", legacy_constructor->constructor_class);
134 gen.append(R"~~~(
135#include <LibWeb/Bindings/@legacy_constructor_class@.h>)~~~");
136 }
137 }
138
139 // FIXME: Special case WebAssembly. We should convert WASM to use IDL.
140 generator.append(R"~~~(
141#include <LibWeb/WebAssembly/WebAssemblyMemoryConstructor.h>
142#include <LibWeb/WebAssembly/WebAssemblyMemoryPrototype.h>
143#include <LibWeb/WebAssembly/WebAssemblyInstanceConstructor.h>
144#include <LibWeb/WebAssembly/WebAssemblyInstanceObjectPrototype.h>
145#include <LibWeb/WebAssembly/WebAssemblyModuleConstructor.h>
146#include <LibWeb/WebAssembly/WebAssemblyModulePrototype.h>
147#include <LibWeb/WebAssembly/WebAssemblyTableConstructor.h>
148#include <LibWeb/WebAssembly/WebAssemblyTablePrototype.h>)~~~");
149
150 generator.append(R"~~~(
151
152namespace Web::Bindings {
153)~~~");
154
155 auto add_interface = [&](SourceGenerator& gen, StringView name, StringView prototype_class, StringView constructor_class, Optional<LegacyConstructor> const& legacy_constructor) {
156 gen.set("interface_name", name);
157 gen.set("prototype_class", prototype_class);
158 gen.set("constructor_class", constructor_class);
159
160 gen.append(R"~~~(
161template<>
162void Intrinsics::create_web_prototype_and_constructor<@prototype_class@>(JS::Realm& realm)
163{
164 auto& vm = realm.vm();
165
166 auto prototype = heap().allocate<@prototype_class@>(realm, realm).release_allocated_value_but_fixme_should_propagate_errors();
167 m_prototypes.set("@interface_name@"sv, prototype);
168
169 auto constructor = heap().allocate<@constructor_class@>(realm, realm).release_allocated_value_but_fixme_should_propagate_errors();
170 m_constructors.set("@interface_name@"sv, constructor);
171
172 prototype->define_direct_property(vm.names.constructor, constructor.ptr(), JS::Attribute::Writable | JS::Attribute::Configurable);
173 constructor->define_direct_property(vm.names.name, JS::PrimitiveString::create(vm, "@interface_name@"sv).release_allocated_value_but_fixme_should_propagate_errors(), JS::Attribute::Configurable);
174)~~~");
175
176 if (legacy_constructor.has_value()) {
177 gen.set("legacy_interface_name", legacy_constructor->name);
178 gen.set("legacy_constructor_class", legacy_constructor->constructor_class);
179 gen.append(R"~~~(
180 auto legacy_constructor = heap().allocate<@legacy_constructor_class@>(realm, realm).release_allocated_value_but_fixme_should_propagate_errors();
181 m_constructors.set("@legacy_interface_name@"sv, legacy_constructor);
182
183 legacy_constructor->define_direct_property(vm.names.name, JS::PrimitiveString::create(vm, "@legacy_interface_name@"sv).release_allocated_value_but_fixme_should_propagate_errors(), JS::Attribute::Configurable);)~~~");
184 }
185
186 gen.append(R"~~~(
187}
188)~~~");
189 };
190
191 for (auto& interface : exposed_interfaces) {
192 auto gen = generator.fork();
193 add_interface(gen, interface.name, interface.prototype_class, interface.constructor_class, lookup_legacy_constructor(interface));
194 }
195
196 // FIXME: Special case WebAssembly. We should convert WASM to use IDL.
197 {
198 auto gen = generator.fork();
199 add_interface(gen, "WebAssembly.Memory"sv, "WebAssemblyMemoryPrototype"sv, "WebAssemblyMemoryConstructor"sv, {});
200 add_interface(gen, "WebAssembly.Instance"sv, "WebAssemblyInstancePrototype"sv, "WebAssemblyInstanceConstructor"sv, {});
201 add_interface(gen, "WebAssembly.Module"sv, "WebAssemblyModulePrototype"sv, "WebAssemblyModuleConstructor"sv, {});
202 add_interface(gen, "WebAssembly.Table"sv, "WebAssemblyTablePrototype"sv, "WebAssemblyTableConstructor"sv, {});
203 }
204
205 generator.append(R"~~~(
206}
207)~~~");
208
209 auto generated_intrinsics_path = LexicalPath(output_path).append("IntrinsicDefinitions.cpp"sv).string();
210 auto generated_intrinsics_file = TRY(Core::File::open(generated_intrinsics_path, Core::File::OpenMode::Write));
211 TRY(generated_intrinsics_file->write_until_depleted(generator.as_string_view().bytes()));
212
213 return {};
214}
215
216static ErrorOr<void> generate_exposed_interface_header(StringView class_name, StringView output_path)
217{
218 StringBuilder builder;
219 SourceGenerator generator(builder);
220
221 generator.set("global_object_snake_name", DeprecatedString(class_name).to_snakecase());
222 generator.append(R"~~~(
223#pragma once
224
225#include <LibJS/Forward.h>
226
227namespace Web::Bindings {
228
229void add_@global_object_snake_name@_exposed_interfaces(JS::Object&);
230
231}
232
233)~~~");
234
235 auto generated_header_path = LexicalPath(output_path).append(DeprecatedString::formatted("{}ExposedInterfaces.h", class_name)).string();
236 auto generated_header_file = TRY(Core::File::open(generated_header_path, Core::File::OpenMode::Write));
237 TRY(generated_header_file->write_until_depleted(generator.as_string_view().bytes()));
238
239 return {};
240}
241
242static ErrorOr<void> generate_exposed_interface_implementation(StringView class_name, StringView output_path, Vector<IDL::Interface&>& exposed_interfaces)
243{
244 StringBuilder builder;
245 SourceGenerator generator(builder);
246
247 generator.set("global_object_name", class_name);
248 generator.set("global_object_snake_name", DeprecatedString(class_name).to_snakecase());
249
250 generator.append(R"~~~(
251#include <LibJS/Runtime/Object.h>
252#include <LibWeb/Bindings/Intrinsics.h>
253#include <LibWeb/Bindings/@global_object_name@ExposedInterfaces.h>
254)~~~");
255 for (auto& interface : exposed_interfaces) {
256 auto gen = generator.fork();
257 gen.set("prototype_class", interface.prototype_class);
258 gen.set("constructor_class", interface.constructor_class);
259
260 gen.append(R"~~~(#include <LibWeb/Bindings/@constructor_class@.h>
261#include <LibWeb/Bindings/@prototype_class@.h>
262)~~~");
263
264 if (auto const& legacy_constructor = lookup_legacy_constructor(interface); legacy_constructor.has_value()) {
265 gen.set("legacy_constructor_class", legacy_constructor->constructor_class);
266 gen.append(R"~~~(#include <LibWeb/Bindings/@legacy_constructor_class@.h>
267)~~~");
268 }
269 }
270
271 generator.append(R"~~~(
272namespace Web::Bindings {
273
274void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
275{
276 static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;
277)~~~");
278
279 auto add_interface = [](SourceGenerator& gen, StringView name, StringView prototype_class, Optional<LegacyConstructor> const& legacy_constructor) {
280 gen.set("interface_name", name);
281 gen.set("prototype_class", prototype_class);
282
283 gen.append(R"~~~(
284 global.define_intrinsic_accessor("@interface_name@", attr, [](auto& realm) -> JS::Value { return &ensure_web_constructor<@prototype_class@>(realm, "@interface_name@"sv); });)~~~");
285
286 if (legacy_constructor.has_value()) {
287 gen.set("legacy_interface_name", legacy_constructor->name);
288 gen.append(R"~~~(
289 global.define_intrinsic_accessor("@legacy_interface_name@", attr, [](auto& realm) -> JS::Value { return &ensure_web_constructor<@prototype_class@>(realm, "@legacy_interface_name@"sv); });)~~~");
290 }
291 };
292
293 for (auto& interface : exposed_interfaces) {
294 auto gen = generator.fork();
295 add_interface(gen, interface.name, interface.prototype_class, lookup_legacy_constructor(interface));
296 }
297
298 generator.append(R"~~~(
299}
300
301}
302)~~~");
303
304 auto generated_implementation_path = LexicalPath(output_path).append(DeprecatedString::formatted("{}ExposedInterfaces.cpp", class_name)).string();
305 auto generated_implementation_file = TRY(Core::File::open(generated_implementation_path, Core::File::OpenMode::Write));
306 TRY(generated_implementation_file->write_until_depleted(generator.as_string_view().bytes()));
307
308 return {};
309}
310
311ErrorOr<int> serenity_main(Main::Arguments arguments)
312{
313 Core::ArgsParser args_parser;
314
315 StringView output_path;
316 StringView base_path;
317 Vector<DeprecatedString> paths;
318
319 args_parser.add_option(output_path, "Path to output generated files into", "output-path", 'o', "output-path");
320 args_parser.add_option(base_path, "Path to root of IDL file tree", "base-path", 'b', "base-path");
321 args_parser.add_positional_argument(paths, "Paths of every IDL file that could be Exposed", "paths");
322 args_parser.parse(arguments);
323
324 VERIFY(!paths.is_empty());
325 VERIFY(!base_path.is_empty());
326
327 const LexicalPath lexical_base(base_path);
328
329 // Read in all IDL files, we must own the storage for all of these for the lifetime of the program
330 Vector<DeprecatedString> file_contents;
331 for (DeprecatedString const& path : paths) {
332 auto file_or_error = Core::File::open(path, Core::File::OpenMode::Read);
333 if (file_or_error.is_error()) {
334 s_error_string = DeprecatedString::formatted("Unable to open file {}", path);
335 return Error::from_string_view(s_error_string);
336 }
337 auto file = file_or_error.release_value();
338 auto string = MUST(file->read_until_eof());
339 file_contents.append(DeprecatedString(ReadonlyBytes(string)));
340 }
341 VERIFY(paths.size() == file_contents.size());
342
343 Vector<IDL::Parser> parsers;
344 Vector<IDL::Interface&> intrinsics;
345 Vector<IDL::Interface&> window_exposed;
346 Vector<IDL::Interface&> dedicated_worker_exposed;
347 Vector<IDL::Interface&> shared_worker_exposed;
348 // TODO: service_worker_exposed
349
350 for (size_t i = 0; i < paths.size(); ++i) {
351 IDL::Parser parser(paths[i], file_contents[i], lexical_base.string());
352 TRY(add_to_interface_sets(parser.parse(), intrinsics, window_exposed, dedicated_worker_exposed, shared_worker_exposed));
353 parsers.append(move(parser));
354 }
355
356 TRY(generate_forwarding_header(output_path, intrinsics));
357 TRY(generate_intrinsic_definitions(output_path, intrinsics));
358
359 TRY(generate_exposed_interface_header("Window"sv, output_path));
360 TRY(generate_exposed_interface_header("DedicatedWorker"sv, output_path));
361 TRY(generate_exposed_interface_header("SharedWorker"sv, output_path));
362 // TODO: ServiceWorkerExposed.h
363
364 TRY(generate_exposed_interface_implementation("Window"sv, output_path, window_exposed));
365 TRY(generate_exposed_interface_implementation("DedicatedWorker"sv, output_path, dedicated_worker_exposed));
366 TRY(generate_exposed_interface_implementation("SharedWorker"sv, output_path, shared_worker_exposed));
367 // TODO: ServiceWorkerExposed.cpp
368
369 return 0;
370}
371
372enum ExposedTo {
373 Nobody = 0x0,
374 DedicatedWorker = 0x1,
375 SharedWorker = 0x2,
376 ServiceWorker = 0x4,
377 AudioWorklet = 0x8,
378 Window = 0x10,
379 AllWorkers = 0xF, // FIXME: Is "AudioWorklet" a Worker? We'll assume it is for now
380 All = 0x1F,
381};
382AK_ENUM_BITWISE_OPERATORS(ExposedTo);
383
384static ErrorOr<ExposedTo> parse_exposure_set(IDL::Interface& interface)
385{
386 // NOTE: This roughly follows the definitions of https://webidl.spec.whatwg.org/#Exposed
387 // It does not remotely interpret all the abstract operations therein though.
388
389 auto maybe_exposed = interface.extended_attributes.get("Exposed");
390 if (!maybe_exposed.has_value()) {
391 s_error_string = DeprecatedString::formatted("Interface {} is missing extended attribute Exposed", interface.name);
392 return Error::from_string_view(s_error_string);
393 }
394 auto exposed = maybe_exposed.value().trim_whitespace();
395 if (exposed == "*"sv)
396 return ExposedTo::All;
397 if (exposed == "Window"sv)
398 return ExposedTo::Window;
399 if (exposed == "Worker"sv)
400 return ExposedTo::AllWorkers;
401 if (exposed == "AudioWorklet"sv)
402 return ExposedTo::AudioWorklet;
403
404 if (exposed[0] == '(') {
405 ExposedTo whom = Nobody;
406 for (StringView candidate : exposed.substring_view(1, exposed.length() - 1).split_view(',')) {
407 candidate = candidate.trim_whitespace();
408 if (candidate == "Window"sv) {
409 whom |= ExposedTo::Window;
410 } else if (candidate == "Worker"sv) {
411 whom |= ExposedTo::AllWorkers;
412 } else if (candidate == "DedicatedWorker"sv) {
413 whom |= ExposedTo::DedicatedWorker;
414 } else if (candidate == "SharedWorker"sv) {
415 whom |= ExposedTo::SharedWorker;
416 } else if (candidate == "ServiceWorker"sv) {
417 whom |= ExposedTo::ServiceWorker;
418 } else if (candidate == "AudioWorklet"sv) {
419 whom |= ExposedTo::AudioWorklet;
420 } else {
421 s_error_string = DeprecatedString::formatted("Unknown Exposed attribute candidate {} in {} in {}", candidate, exposed, interface.name);
422 return Error::from_string_view(s_error_string);
423 }
424 }
425 if (whom == ExposedTo::Nobody) {
426 s_error_string = DeprecatedString::formatted("Unknown Exposed attribute {} in {}", exposed, interface.name);
427 return Error::from_string_view(s_error_string);
428 }
429 return whom;
430 }
431
432 s_error_string = DeprecatedString::formatted("Unknown Exposed attribute {} in {}", exposed, interface.name);
433 return Error::from_string_view(s_error_string);
434}
435
436ErrorOr<void> add_to_interface_sets(IDL::Interface& interface, Vector<IDL::Interface&>& intrinsics, Vector<IDL::Interface&>& window_exposed, Vector<IDL::Interface&>& dedicated_worker_exposed, Vector<IDL::Interface&>& shared_worker_exposed)
437{
438 // TODO: Add service worker exposed and audio worklet exposed
439 auto whom = TRY(parse_exposure_set(interface));
440 VERIFY(whom != ExposedTo::Nobody);
441
442 if ((whom & ExposedTo::Window) || (whom & ExposedTo::DedicatedWorker) || (whom & ExposedTo::SharedWorker))
443 intrinsics.append(interface);
444
445 if (whom & ExposedTo::Window)
446 window_exposed.append(interface);
447
448 if (whom & ExposedTo::DedicatedWorker)
449 dedicated_worker_exposed.append(interface);
450
451 if (whom & ExposedTo::SharedWorker)
452 shared_worker_exposed.append(interface);
453
454 return {};
455}