首页
/ Ladybird 开发实战:从零添加一个新的 WebIDL 文件,生成 JavaScript 绑定

Ladybird 开发实战:从零添加一个新的 WebIDL 文件,生成 JavaScript 绑定

2026-09-04 12:53:23作者:申梦珏Efrain

本文讲解 Ladybird 浏览器引擎中新增 Web API 绑定的完整流程:如何按规范创建 LibWeb 下的 IDL 文件、如何在构建系统中注册它、以及如何为生成类添加前置声明。读完之后,你能独立把一个 Web 规范接口(如 HTMLDetailsElement)接入 LibWeb 的代码生成管线,并理解 CMake 与 Python 生成器是如何把 WebIDL 文本自动编译成 C++ 绑定代码的。

为什么大部分工作由构建系统完成,但仍需手动三步

Ladybird 的构建系统承担了把 Web 规范中的 IDL 转化为代码的大量工作:解析 WebIDL 语法、生成 C++ 头文件与实现文件、维护 window 等全局对象上的接口暴露列表与包装器工厂。但仍有三件事必须由开发者手动完成。原文档以新增 HTMLDetailsElement 为例说明了这三步,下面逐条展开,并结合仓库中的真实文件给出佐证与细节。

第一步:创建 IDL 文件,内容取自规范

Libraries/LibWeb/<命名空间>/ 下创建与类名同名的 .idl 文件,内容为规范(spec)中对应接口的 IDL 片段。以 HTMLDetailsElement 为例,需要创建 Libraries/LibWeb/HTML/HTMLDetailsElement.idl,文档给出的示例内容为:

[Exposed=Window]
interface HTMLDetailsElement : HTMLElement {
    [HTMLConstructor] constructor();

    [CEReactions] attribute boolean open;
};

这里几个扩展属性(extended attribute)各有含义:

  • [Exposed=Window]:声明该接口暴露在哪个全局环境中(WindowDedicatedWorkerSharedWorker 等)。生成器会据此把它写入对应的“已暴露接口”清单(见下文 WindowExposedInterfaces 等生成物),不暴露的接口不会挂到全局作用域上;
  • [HTMLConstructor]:表示该构造函数按 HTML 规范的特例处理——通过 document.createElement("details") 创建,而不是普通的 new
  • [CEReactions]:表示该属性访问器属于“CE 反应”(character element reactions),访问时需要在 DOM 事件循环中先运行挂起的任务队列。

仓库中该文件的当前真实内容比文档示例更完整,额外包含 name 属性并带有 Reflect 扩展属性(表示属性值与同名 HTML 属性双向同步):

[Exposed=Window]
interface HTMLDetailsElement : HTMLElement {

    [HTMLConstructor] constructor();

    [CEReactions, Reflect] attribute Utf16DOMString name;
    [CEReactions, Reflect] attribute boolean open;

};

HTMLDetailsElement.idl。可以对照此文件学习各类扩展属性的实际写法。

一个容易被忽略的隐含约束是文件路径与 C++ 命名空间的绑定。生成器源码 generate_libweb_bindings.py 中的 cpp_namespace_for_module_path() 会按路径推断命名空间:Libraries/LibWeb/<namespace>/... 下的模块对应 C++ 命名空间 Web::<namespace>。也就是说,文件必须放在正确的目录层级下,HTML/ 子目录对应 Web::HTML,把文件放错位置会导致生成代码落入错误命名空间。

第二步:把 IDL 文件注册到 idl_files.cmake

创建文件后,必须在 Libraries/LibWeb/idl_files.cmake 中登记一行:

libweb_js_bindings(HTML/HTMLDetailsElement)

仓库中 HTMLDetailsElement 的登记行就在 idl_files.cmake 第 250 行,紧随其后的是 HTMLDialogElement。该文件顶部的注释说明了它的设计意图:虽然它由 Meta/CMake 下的 CMake 脚本包含,但被刻意放在 Libraries/LibWeb/ 目录内,就是为了“添加新 IDL 文件时无需进入 Meta 目录”。

libweb_js_bindings(class) 并不是简单的列表追加,它定义在 Meta/CMake/libweb_generators.cmakegenerate_js_bindings() 函数内部,实际做了三件事:

  1. 确定 IDL 源路径:默认取 ${LibWeb 源码目录}/<class>.idl;若该文件属于由其他生成器先期产出的“已生成 IDL”(如 CSS 数值工厂方法),则改从构建目录读取;
  2. 把生成物挂到编译目标:宏 libweb_add_bindings_source() 会为每个类创建构建目录下的 Bindings/<类名>.hBindings/<类名>.cpp,并通过 target_sources() 加入 LibWeb 编译目标,因此每个 IDL 类最终都会贡献一对真实的编译单元;
  3. 累积参数:把 IDL 路径追加进 LIBWEB_ALL_IDL_FILESLIBWEB_ALL_PARSED_IDL_FILES,供后续统一调用生成器。

第三步:在 Forward.h 中前置声明生成类

最后,在 Libraries/LibWeb/Forward.h 的对应命名空间中添加前置声明:

class HTMLDetailsElement;

仓库中 HTMLDetailsElement 的前置声明位于 Forward.h 第 777 行。这一步的必要性来自绑定架构:每个 IDL 类生成一个包装器(wrapper)类型,LibWeb 各处(事件派发、遍历、跨模块引用)需要以指针/引用方式谈论这个类,但多数头文件不希望为此引入完整的实现头文件。Forward.h 中按命名空间组织的密集前置声明就是解决这个问题的手段;这与 Documentation/WrapperArchitecture.md 描述的包装器架构相衔接。

构建系统如何消费这些 IDL 文件

完成三步后,真正“重活”由 CMake 与 Python 生成器在构建时完成。这条管线值得开发者理解,因为它决定了你写的 IDL 文件会被如何处理、产物落在哪里、以及哪些改动会触发重新生成。

统一的 add_custom_command 与增量构建

libweb_generators.cmake 第 383-393 行 注册了一个覆盖所有绑定源文件的自定义命令:

add_custom_command(
    OUTPUT ${LIBWEB_ALL_BINDINGS_SOURCES} ${exposed_interface_sources}
    COMMAND "${CMAKE_COMMAND}" -E make_directory "Bindings"
    COMMAND "${Python3_EXECUTABLE}" "${bindings_generator}" -o "Bindings"
            --depfile "${LIBWEB_BINDINGS_DEPFILE}"
            ${LIBWEB_ALL_PARSED_IDL_FILES_ARGUMENT}
    VERBATIM
    COMMENT "Generating LibWeb bindings"
    DEPFILE "${LIBWEB_BINDINGS_DEPFILE}"
    DEPENDS ${bindings_generator_dependencies} ${LIBWEB_ALL_IDL_FILES} ${LIBWEB_ALL_PARSED_IDL_FILES}
)

几个值得注意的实现细节:

  • 生成器入口Meta/Generators/generate_libweb_bindings.py,命令行参数为输出目录 -o、依赖文件 --depfile 以及全部 IDL 路径。其参数解析见 第 43-59 行,注释明确路径是“所有可能 Exposed 的 IDL 文件”。
  • 依赖追踪通过生成器写出的 depfile(Bindings/LibWebBindings.d)实现:修改任意 IDL 文件都会精确触发重新生成,而生成器脚本及其全部模块(Meta/Generators/libweb_bindings/ 下的 attributes、arguments、constructors、to_js_value、to_idl_value 等,在 第 302-330 行 逐一列为依赖)的修改同样会触发重建。
  • Windows 特殊处理:因命令行长度限制,第 373-381 行WIN32 下把 IDL 路径列表写入 all_idl_files.txt 响应文件,以 @文件 形式传给生成器;生成器端的 read_input_paths() 会识别 @ 前缀并展开。
  • WebIDL 的解析依赖 Meta/Utils/webidl_parser.py,它被列入 bindings_generator_dependencies,是生成器的语法基础。

每个 IDL 模块生成什么

生成器对每个 IDL 文件(一个 Module)依次做:解析 → 生成 Bindings/<模块名>.h<模块名>.cpp。以 write_idl_header() 为例(第 84-102 行),产物固定为 namespace Web::Bindings 下的声明块,包含接口声明、枚举(enumeration)及其与 JS 值的转换声明、字典(dictionary)及其转换声明。实现文件则写入接口实现、枚举/字典的双向转换函数。

除了“每类一份”的文件,还有一组全局汇总产物,由 generate_libweb_bindings.py 顶部导入的各写入函数生成,并在 libweb_generators.cmake 第 332-340 行 中登记为输出:

  • Bindings/Forward.h:绑定层的前置声明汇总;
  • Bindings/IntrinsicDefinitions.h/.cpp:JS 引擎层 intrinsic 定义;
  • Bindings/WindowExposedInterfaces.h/.cppDedicatedWorkerExposedInterfaces.*SharedWorkerExposedInterfaces.*:按 [Exposed=...] 环境划分的接口清单——你在第一步写下的 [Exposed=Window] 正是驱动这些清单的成员;
  • Bindings/WrapperFactory.cpp:所有包装器类型的创建入口。

这些文件生成到构建目录的 Bindings/ 下(不在源码树内),LibWeb 主目标通过 generate_bindingsgenerate_exposed_interfaces 两个自定义目标(第 395-403 行)依赖它们,并挂入 ladybird_codegen_accumulator 统一调度;同时等待 CSS 等先期生成的 IDL(generate_* 目标)先完成。

用仓库中的真实案例串联全流程

HTMLDetailsElement 恰好是文档示例对应的、且已在仓库中落地的完整例子,可以逐环节核验:

环节 仓库证据
IDL 文件 Libraries/LibWeb/HTML/HTMLDetailsElement.idl
CMake 注册 Libraries/LibWeb/idl_files.cmake 中的 libweb_js_bindings(HTML/HTMLDetailsElement)
前置声明 Libraries/LibWeb/Forward.h 中的 class HTMLDetailsElement;
C++ 实现类 Libraries/LibWeb/HTML/HTMLDetailsElement.h
编译进 LibWeb Libraries/LibWeb/CMakeLists.txt 的源文件列表中列出 HTML/HTMLDetailsElement.cpp

HTMLDetailsElement.h 的类定义能看出 C++ 侧与 IDL 侧的分工:类以 final 继承 HTMLElement,用 WEB_WRAPPABLE(HTMLDetailsElement, HTMLElement) 宏声明其可被绑定系统包装、且包装器类型由父类推导;构造函数是 private 的(HTMLDetailsElement(DOM::Document&, DOM::QualifiedName)),即 JS 侧的 [HTMLConstructor] constructor(); 不会直接映射为 C++ 公开构造器,而是经元素工厂路径创建实例——这一路径在 Libraries/LibWeb/DOM/ElementFactory.cpp 中可见 REGISTER_HTML_ELEMENT(details, HTMLDetailsElement) 按标签名注册。属性访问器(openname 的 getter/setter)与 Reflect 同步逻辑则大部分由绑定层生成,C++ 类只需实现 IDL 中显式声明的其余成员以及 attribute_changed 等 DOM 生命周期钩子。

操作要点与常见坑

把上述流程浓缩为可执行清单:

  1. Libraries/LibWeb/<命名空间>/ 下创建 <ClassName>.idl,内容从对应 Web 规范复制,按需保留 [Exposed=...][HTMLConstructor][CEReactions][Reflect] 等扩展属性;目录名决定 C++ 命名空间(<命名空间> 目录 → Web::<命名空间>),不要放错层级;
  2. Libraries/LibWeb/idl_files.cmake 中添加一行 libweb_js_bindings(<命名空间>/<ClassName>),注意参数是相对 LibWeb 根目录的路径且不带 .idl 后缀;
  3. Libraries/LibWeb/Forward.h 的对应命名空间区块内添加 class <ClassName>;
  4. 若该接口背后有真实行为,还需按项目惯例编写 C++ 实现类(<ClassName>.h/.cpp),实现 IDL 中声明的访问器、生命周期回调等,并把 .cpp 加入 Libraries/LibWeb/CMakeLists.txt 的源文件列表——这一步原文档未展开,但它与“生成绑定”是并行的两条工作线;
  5. 重新配置/构建,观察 “Generating LibWeb bindings” 的自定义命令是否被触发;若改了 IDL 却没有重新生成,优先检查 depfile 与 IDL 路径是否被正确登记。

适用前提:以上均基于当前仓库的构建结构(CMake + Python 生成器、生成物位于构建目录 Bindings/ 下)。IDL 文件内容本身必须忠实于 Web 规范,扩展属性的语义(如 [CEReactions]ReflectExposed 环境)由 Meta/Generators/libweb_bindings/ 中的各模块按 WebIDL 规范逐一解释;遇到不认识的属性报错时,回到规范核对拼写与位置,而不是绕过生成器手写绑定。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341