ladybird/Meta/Generators/libweb_bindings/intrinsics.py
Shannon Booth dba62aefc0 LibWeb/Bindings: Port IDL bindings generator to Python
Replace the Lagom C++ bindings generator invocation with the new Python
generator under Meta/Generators/libweb_bindings.

Fold the exposed-interface generation into the same generator entry
point, and keep generated overload metadata using the existing LibIDL
types for now.
2026-06-08 01:11:40 +02:00

662 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause
from dataclasses import dataclass
from dataclasses import field
from typing import List
from typing import Optional
from typing import Set
from typing import TextIO
from Generators.libweb_bindings.named_and_indexed_properties import interface_supports_named_properties
from Utils.utils import title_case_to_snake_case
from Utils.webidl_parser import Interface
from Utils.webidl_parser import Module
ALL_WORKERS_EXPOSURE = {
"DedicatedWorker",
"SharedWorker",
"ServiceWorker",
"AudioWorklet",
}
ALL_EXPOSURE = ALL_WORKERS_EXPOSURE | {
"Window",
"Worklet",
}
@dataclass
class InterfaceSets:
intrinsics: List[Interface] = field(default_factory=list)
window_exposed: List[Interface] = field(default_factory=list)
dedicated_worker_exposed: List[Interface] = field(default_factory=list)
shared_worker_exposed: List[Interface] = field(default_factory=list)
def add_interface(self, interface: Interface) -> None:
exposed_value = interface.extended_attributes.get("Exposed")
if exposed_value is None:
raise RuntimeError(f"Interface {interface.name} is missing extended attribute Exposed")
exposures = parse_exposure_set(interface.name, exposed_value)
self.intrinsics.append(interface)
if "Window" in exposures:
self.window_exposed.append(interface)
if "DedicatedWorker" in exposures:
self.dedicated_worker_exposed.append(interface)
if "SharedWorker" in exposures:
self.shared_worker_exposed.append(interface)
@dataclass
class LegacyConstructor:
name: str
constructor_class: str
def collect_interface_sets(modules: List[Module]) -> InterfaceSets:
interface_sets = InterfaceSets()
for module in modules:
interface = module.interface
if interface is None:
continue
if not should_have_interface_object(interface):
continue
interface_sets.add_interface(interface)
return interface_sets
def parse_exposure_set(interface_name: str, exposed_value: str) -> Set[str]:
exposed_value = exposed_value.strip()
if exposed_value == "*":
return set(ALL_EXPOSURE)
if exposed_value == "Nobody":
return set()
def parse_single_candidate(candidate: str) -> Optional[Set[str]]:
if candidate == "Window":
return {"Window"}
if candidate == "Worker":
return set(ALL_WORKERS_EXPOSURE)
if candidate == "DedicatedWorker":
return {"DedicatedWorker"}
if candidate == "SharedWorker":
return {"SharedWorker"}
if candidate == "ServiceWorker":
return {"ServiceWorker"}
if candidate == "AudioWorklet":
return {"AudioWorklet"}
if candidate == "LayoutWorklet":
return {"LayoutWorklet"}
if candidate == "PaintWorklet":
return {"PaintWorklet"}
if candidate == "Worklet":
return {"Worklet"}
return None
if (parsed_candidate := parse_single_candidate(exposed_value)) is not None:
return parsed_candidate
if exposed_value.startswith("(") and exposed_value.endswith(")"):
exposures: Set[str] = set()
for candidate in exposed_value[1:-1].split(","):
candidate_name = candidate.strip()
if (parsed_candidate := parse_single_candidate(candidate_name)) is None:
raise RuntimeError(
f"Unknown Exposed attribute candidate {candidate_name} in {exposed_value!r} in {interface_name}"
)
exposures.update(parsed_candidate)
if exposures:
return exposures
raise RuntimeError(f"Unknown Exposed attribute {exposed_value!r} in {interface_name}")
def should_have_interface_object(interface: Interface) -> bool:
if interface.is_callback_interface:
return bool(interface.constants)
return True
def can_use_shared_interface_constructor(interface: Interface) -> bool:
return not interface.is_namespace and not interface.is_callback_interface
def can_use_shared_interface_prototype(interface: Interface) -> bool:
return (
not interface.is_namespace
and not interface.is_callback_interface
and "Global" not in interface.extended_attributes
and not interface.has_special_member
and interface.named_property_getter is None
and interface.indexed_property_getter is None
and interface.named_property_setter is None
and interface.named_property_deleter is None
and interface.indexed_property_setter is None
)
def interface_prototype_has_immutable_prototype(interface: Interface) -> bool:
# NB: This currently assumes only Workers and Window can be globals.
return (
"Global" in interface.extended_attributes
or interface.name == "WorkerGlobalScope"
or interface.name == "EventTarget"
)
def lookup_legacy_constructor(interface: Interface) -> Optional[LegacyConstructor]:
legacy_factory_function = interface.extended_attributes.get("LegacyFactoryFunction")
if not legacy_factory_function:
return None
legacy_factory_function = legacy_factory_function.lstrip()
name = []
for character in legacy_factory_function:
if character.isspace() or character == "(":
break
name.append(character)
constructor_name = "".join(name)
return LegacyConstructor(
name=constructor_name,
constructor_class=f"{constructor_name}Constructor",
)
def write_intrinsic_definitions_header(out: TextIO, interface_sets: InterfaceSets) -> None:
out.write(
"""#pragma once
#include <AK/Types.h>
namespace Web::Bindings {
enum class InterfaceName : u16 {
Unknown = 0,
"""
)
for index, interface in enumerate(interface_sets.intrinsics, start=1):
out.write(f" {interface.name} = {index},\n")
out.write(
"""};
bool is_exposed(InterfaceName, JS::Realm&);
}
"""
)
def write_intrinsic_definitions_implementation(out: TextIO, interface_sets: InterfaceSets) -> None:
out.write(
"""#include <LibGC/DeferGC.h>
#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/InterfaceObject.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/Export.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/DedicatedWorkerGlobalScope.h>
#include <LibWeb/HTML/SharedWorkerGlobalScope.h>
"""
)
for interface in interface_sets.intrinsics:
out.write(f"#include <LibWeb/Bindings/{interface.implemented_name}.h>\n")
legacy_constructor = lookup_legacy_constructor(interface)
if legacy_constructor is not None:
out.write(f"#include <LibWeb/Bindings/{legacy_constructor.constructor_class}.h>\n")
out.write(
"""
namespace Web::Bindings {
"""
)
write_secure_context_switch(out, interface_sets.intrinsics)
write_experimental_switch(out, interface_sets.intrinsics)
write_global_exposed_switch(out, "window", interface_sets.window_exposed)
write_global_exposed_switch(out, "dedicated_worker", interface_sets.dedicated_worker_exposed)
write_global_exposed_switch(out, "shared_worker", interface_sets.shared_worker_exposed)
out.write(
"""
// An interface, callback interface, namespace, or member construct is exposed in a given realm realm if the following steps return true:
bool is_exposed(InterfaceName name, JS::Realm& realm)
{
auto const& global_object = realm.global_object();
// 1. If constructs exposure set is not *, and realm.[[GlobalObject]] does not implement an interface that is in constructs exposure set, then return false.
if (is<HTML::Window>(global_object)) {
if (!is_window_exposed(name))
return false;
} else if (is<HTML::DedicatedWorkerGlobalScope>(global_object)) {
if (!is_dedicated_worker_exposed(name))
return false;
} else if (is<HTML::SharedWorkerGlobalScope>(global_object)) {
if (!is_shared_worker_exposed(name))
return false;
} else {
TODO(); // FIXME: ServiceWorkerGlobalScope and WorkletGlobalScope.
}
// 2. If realms settings object is not a secure context, and construct is conditionally exposed on
// [SecureContext], then return false.
if (is_secure_context_interface(name) && HTML::is_non_secure_context(principal_host_defined_environment_settings_object(realm)))
return false;
// AD-HOC: Do not expose experimental interfaces unless instructed to do so.
if (!HTML::UniversalGlobalScopeMixin::expose_experimental_interfaces() && is_experimental_interface(name))
return false;
// FIXME: 3. If realms settings objects cross-origin isolated capability is false, and construct is
// conditionally exposed on [CrossOriginIsolated], then return false.
// 4. Return true.
return true;
}
"""
)
for interface in interface_sets.intrinsics:
if interface.is_namespace:
write_namespace_creation(out, interface, interface_sets.intrinsics)
else:
write_interface_creation(out, interface)
out.write(
"""}
"""
)
def write_secure_context_switch(out: TextIO, interfaces: List[Interface]) -> None:
out.write(
"""static constexpr bool is_secure_context_interface(InterfaceName name)
{
switch (name) {
"""
)
for interface in interfaces:
if "SecureContext" not in interface.extended_attributes:
continue
out.write(f" case InterfaceName::{interface.name}:\n")
out.write(
""" return true;
default:
return false;
}
}
"""
)
def write_experimental_switch(out: TextIO, interfaces: List[Interface]) -> None:
out.write(
"""static constexpr bool is_experimental_interface(InterfaceName name)
{
switch (name) {
"""
)
for interface in interfaces:
if "Experimental" not in interface.extended_attributes:
continue
out.write(f" case InterfaceName::{interface.name}:\n")
out.write(
""" return true;
default:
return false;
}
}
"""
)
def write_global_exposed_switch(out: TextIO, global_name: str, interfaces: List[Interface]) -> None:
out.write(
f"""static constexpr bool is_{global_name}_exposed(InterfaceName name)
{{
switch (name) {{
"""
)
for interface in interfaces:
out.write(f" case InterfaceName::{interface.name}:\n")
out.write(
""" return true;
default:
return false;
}
}
"""
)
def write_namespace_creation(out: TextIO, interface: Interface, interfaces: List[Interface]) -> None:
out.write(
f"""template<>
void Intrinsics::create_web_namespace<{interface.namespace_class}>(JS::Realm& realm)
{{
auto namespace_object = realm.create<{interface.namespace_class}>(realm);
m_namespaces.set("{interface.name}"_fly_string, namespace_object);
[[maybe_unused]] static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;
"""
)
for owned_interface in interfaces:
if owned_interface.extended_attributes.get("LegacyNamespace") != interface.name:
continue
out.write(
f""" namespace_object->define_intrinsic_accessor("{owned_interface.name}"_utf16_fly_string, attr, [](auto& realm) -> JS::Value {{ return &Bindings::ensure_web_constructor<{owned_interface.prototype_class}>(realm, "{interface.name}.{owned_interface.name}"_fly_string); }});
"""
)
out.write(
"""}
"""
)
def write_interface_creation(out: TextIO, interface: Interface) -> None:
if interface.is_callback_interface:
out.write(
f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
static constexpr InterfaceObjectMetadata metadata {{
.name = "{interface.name}"sv,
.namespaced_name = "{interface.namespaced_name}"sv,
.initialize_constructor = &{interface.constructor_class}::initialize,
.initialize_prototype = &{interface.prototype_class}::initialize,
}};
create_web_prototype_and_constructor(realm, metadata);
}}
"""
)
return
if can_use_shared_interface_prototype(interface):
out.write(
f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
"""
)
ensure_parent_prototype = "nullptr"
ensure_parent_constructor = "nullptr"
if interface.parent_name:
ensure_parent_prototype = f"""[](JS::Realm& realm) -> JS::Object& {{ return Web::Bindings::ensure_web_prototype<{interface.parent_name}Prototype>(realm, "{interface.parent_name}"_fly_string); }}"""
ensure_parent_constructor = f"""[](JS::Realm& realm) -> JS::NativeFunction& {{ return Web::Bindings::ensure_web_constructor<{interface.parent_name}Prototype>(realm, "{interface.parent_name}"_fly_string); }}"""
out.write(
f""" static constexpr InterfaceObjectMetadata metadata {{
.name = "{interface.name}"sv,
.namespaced_name = "{interface.namespaced_name}"sv,
.ensure_parent_prototype = {ensure_parent_prototype},
.ensure_parent_constructor = {ensure_parent_constructor},
.initialize_constructor = &{interface.constructor_class}::initialize,
.initialize_prototype = &{interface.prototype_class}::initialize,
.define_unforgeable_attributes = &{interface.prototype_class}::define_unforgeable_attributes,
.construct = &{interface.constructor_class}::construct,
.has_immutable_prototype = {str(interface_prototype_has_immutable_prototype(interface)).lower()},
}};
create_web_prototype_and_constructor(realm, metadata);
"""
)
legacy_constructor = lookup_legacy_constructor(interface)
if legacy_constructor is not None:
out.write(
f""" auto legacy_constructor = realm.create<{legacy_constructor.constructor_class}>(realm);
m_constructors.set("{legacy_constructor.name}"_fly_string, legacy_constructor.ptr());
"""
)
out.write(
"""}
"""
)
return
if can_use_shared_interface_constructor(interface):
out.write(
f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
"""
)
ensure_parent_constructor = "nullptr"
if interface.parent_name:
ensure_parent_constructor = f"""[](JS::Realm& realm) -> JS::NativeFunction& {{ return Web::Bindings::ensure_web_constructor<{interface.parent_name}Prototype>(realm, "{interface.parent_name}"_fly_string); }}"""
out.write(
f""" static constexpr InterfaceObjectMetadata metadata {{
.name = "{interface.name}"sv,
.namespaced_name = "{interface.namespaced_name}"sv,
.ensure_parent_constructor = {ensure_parent_constructor},
.initialize_constructor = &{interface.constructor_class}::initialize,
.construct = &{interface.constructor_class}::construct,
}};
"""
)
if interface_supports_named_properties(interface):
out.write(
f""" auto named_properties_object = realm.create<{interface.name}Properties>(realm);
m_prototypes.set("{interface.name}Properties"_fly_string, named_properties_object);
"""
)
out.write(
f""" auto prototype = realm.create<{interface.prototype_class}>(realm);
m_prototypes.set("{interface.namespaced_name}"_fly_string, prototype);
create_web_constructor(realm, metadata, prototype);
"""
)
legacy_constructor = lookup_legacy_constructor(interface)
if legacy_constructor is not None:
out.write(
f""" auto legacy_constructor = realm.create<{legacy_constructor.constructor_class}>(realm);
m_constructors.set("{legacy_constructor.name}"_fly_string, legacy_constructor.ptr());
"""
)
out.write(
"""}
"""
)
return
out.write(
f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
auto& vm = realm.vm();
"""
)
if interface_supports_named_properties(interface):
out.write(
f""" auto named_properties_object = realm.create<{interface.name}Properties>(realm);
m_prototypes.set("{interface.name}Properties"_fly_string, named_properties_object);
"""
)
out.write(
f""" auto prototype = realm.create<{interface.prototype_class}>(realm);
m_prototypes.set("{interface.namespaced_name}"_fly_string, prototype);
auto constructor = realm.create<{interface.constructor_class}>(realm);
m_constructors.set("{interface.namespaced_name}"_fly_string, constructor);
prototype->define_direct_property(vm.names.constructor, constructor.ptr(), JS::Attribute::Writable | JS::Attribute::Configurable);
"""
)
legacy_constructor = lookup_legacy_constructor(interface)
if legacy_constructor is not None:
out.write(
f""" auto legacy_constructor = realm.create<{legacy_constructor.constructor_class}>(realm);
m_constructors.set("{legacy_constructor.name}"_fly_string, legacy_constructor.ptr());
"""
)
out.write(
"""}
"""
)
def write_exposed_interface_header(out: TextIO, class_name: str) -> None:
snake_name = title_case_to_snake_case(class_name)
out.write(
f"""#pragma once
#include <LibJS/Forward.h>
namespace Web::Bindings {{
void add_{snake_name}_exposed_interfaces(JS::Object&);
}}
"""
)
def write_exposed_interface_implementation(out: TextIO, class_name: str, exposed_interfaces: List[Interface]) -> None:
snake_name = title_case_to_snake_case(class_name)
out.write(
f"""#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/{class_name}ExposedInterfaces.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/UniversalGlobalScope.h>
"""
)
for interface in exposed_interfaces:
out.write(f"#include <LibWeb/Bindings/{interface.implemented_name}.h>\n")
out.write(
f"""
namespace Web::Bindings {{
void add_{snake_name}_exposed_interfaces(JS::Object& global)
{{
static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;
[[maybe_unused]] bool is_secure_context = HTML::is_secure_context(HTML::relevant_settings_object(global));
[[maybe_unused]] bool expose_experimental_interfaces = HTML::UniversalGlobalScopeMixin::expose_experimental_interfaces();
"""
)
for interface in exposed_interfaces:
if interface.is_namespace:
write_namespace_global_accessor(out, interface)
continue
if "LegacyNamespace" in interface.extended_attributes:
continue
if "LegacyNoInterfaceObject" in interface.extended_attributes:
continue
if not should_have_interface_object(interface):
continue
write_interface_global_accessor(out, class_name, interface)
out.write(
"""}
}
"""
)
def write_interface_global_accessor(out: TextIO, class_name: str, interface: Interface) -> None:
indentation = " "
def write_constructor_accessor(name: str, constructor_name: str) -> None:
out.write(
f'{indentation}global.define_intrinsic_accessor("{name}"_utf16_fly_string, attr, [](auto& realm) -> JS::Value {{ return &ensure_web_constructor<{interface.prototype_class}>(realm, "{constructor_name}"_fly_string); }});\n'
)
if "SecureContext" in interface.extended_attributes:
out.write(
""" if (is_secure_context) {
"""
)
indentation += " "
if "Experimental" in interface.extended_attributes:
out.write(
f"""{indentation}if (expose_experimental_interfaces) {{
"""
)
indentation += " "
write_constructor_accessor(interface.namespaced_name, interface.namespaced_name)
if class_name == "Window":
legacy_window_alias = interface.extended_attributes.get("LegacyWindowAlias")
if legacy_window_alias:
if legacy_window_alias.startswith("(") and legacy_window_alias.endswith(")"):
aliases = [alias.strip() for alias in legacy_window_alias[1:-1].split(",")]
else:
aliases = [legacy_window_alias]
for alias in aliases:
write_constructor_accessor(alias, interface.namespaced_name)
legacy_constructor = lookup_legacy_constructor(interface)
if legacy_constructor is not None:
write_constructor_accessor(legacy_constructor.name, legacy_constructor.name)
if "Experimental" in interface.extended_attributes:
indentation = indentation[:-4]
out.write(
f"""{indentation}}}
"""
)
if "SecureContext" in interface.extended_attributes:
indentation = indentation[:-4]
out.write(
f"""{indentation}}}
"""
)
def write_namespace_global_accessor(out: TextIO, interface: Interface) -> None:
out.write(
f""" global.define_intrinsic_accessor("{interface.name}"_utf16_fly_string, attr, [](auto& realm) -> JS::Value {{ return &ensure_web_namespace<{interface.namespace_class}>(realm, "{interface.name}"_fly_string); }});
"""
)