Skip to main content

Electron 内部原理:将 Chromium 构建为库

· 14 min read

Electron基于谷歌的开源项目Chromium,而Chromium本身并不一定是为了被其他项目使用而设计的。本文介绍了Chromium如何被构建为供Electron使用的库,以及其构建系统多年来的发展演变。

🌐 Electron is based on Google's open-source Chromium, a project that is not necessarily designed to be used by other projects. This post introduces how Chromium is built as a library for Electron's use, and how the build system has evolved over the years.


使用 CEF

🌐 Using CEF

Chromium 嵌入式框架(CEF)是一个将 Chromium 转换为库的项目,并提供基于 Chromium 代码库的稳定 API。Atom 编辑器和 NW.js 的非常早期版本曾使用过 CEF。

🌐 The Chromium Embedded Framework (CEF) is a project that turns Chromium into a library, and provides stable APIs based on Chromium's codebase. Very early versions of Atom editor and NW.js used CEF.

为了保持稳定的 API,CEF 隐藏了 Chromium 的所有细节,并用自己的接口封装了 Chromium 的 API。因此,当我们需要访问底层的 Chromium API,比如将 Node.js 集成到网页中时,CEF 的优势反而成了障碍。

🌐 To maintain a stable API, CEF hides all the details of Chromium and wraps Chromium's APIs with its own interface. So when we needed to access underlying Chromium APIs, like integrating Node.js into web pages, the advantages of CEF became blockers.

所以到最后,Electron 和 NW.js 都直接改用 Chromium 的 API。

🌐 So in the end both Electron and NW.js switched to using Chromium's APIs directly.

作为 Chromium 的一部分构建

🌐 Building as part of Chromium

尽管 Chromium 官方不支持外部项目,但其代码库是模块化的,并且很容易基于 Chromium 构建一个最小化浏览器。提供浏览器接口的核心模块称为内容模块(Content Module)。

🌐 Even though Chromium does not officially support outside projects, the codebase is modular and it is easy to build a minimal browser based on Chromium. The core module providing the browser interface is called Content Module.

要使用内容模块开发项目,最简单的方法是将项目作为 Chromium 的一部分进行构建。这可以通过首先检出 Chromium 的源代码,然后将项目添加到 Chromium 的 DEPS 文件中来完成。

🌐 To develop a project with Content Module, the easiest way is to build the project as part of Chromium. This can be done by first checking out Chromium's source code, and then adding the project to Chromium's DEPS file.

NW.js 和 Electron 的早期版本都使用这种方式进行构建。

🌐 NW.js and very early versions of Electron are using this way for building.

缺点是,Chromium 的代码库非常庞大,需要非常强大的机器才能编译。对于普通注意本电脑,这可能需要超过 5 小时。因此,这大大影响了能够为该项目做出贡献的开发者数量,也使开发速度变慢。

🌐 The downside is, Chromium is a very large codebase and requires very powerful machines to build. For normal laptops, that can take more than 5 hours. So this greatly impacts the number of developers that can contribute to the project, and it also makes development slower.

将 Chromium 构建为单个共享库

🌐 Building Chromium as a single shared library

作为 Content 模块的用户,在大多数情况下 Electron 不需要修改 Chromium 的代码,所以一个明显的方法来改进 Electron 的构建是将 Chromium 构建为共享库,然后在 Electron 中链接它。这样开发者在为 Electron 做贡献时就不再需要重新构建整个 Chromium。

🌐 As a user of Content Module, Electron does not need to modify Chromium's code under most cases, so an obvious way to improve the building of Electron is to build Chromium as a shared library, and then link with it in Electron. In this way developers no longer need to build all off Chromium when contributing to Electron.

libchromiumcontent 项目是由 @aroben 为此目的创建的。它将 Chromium 的内容模块构建为共享库,并提供 Chromium 的头文件和预构建的二进制文件供下载。libchromiumcontent 初始版本的代码可以在 在这个链接中 找到。

🌐 The libchromiumcontent project was created by @aroben for this purpose. It builds the Content Module of Chromium as a shared library, and then provides Chromium's headers and prebuilt binaries for download. The code of the initial version of libchromiumcontent can be found in this link.

brightray 项目也是作为 libchromiumcontent 的一部分诞生的,该项目在内容模块周围提供了一层薄薄的封装层。

🌐 The brightray project was also born as part of libchromiumcontent, which provides a thin layer around Content Module.

通过同时使用 libchromiumcontent 和 brightray,开发者可以快速构建浏览器,而无需深入了解 Chromium 的构建细节。同时,它也消除了构建该项目对高速网络和高性能计算机的需求。

🌐 By using libchromiumcontent and brightray together, developers can quickly build a browser without getting into the details of building Chromium. And it removes the requirement of a fast network and powerful machine for building the project.

除了 Electron,还有其他基于 Chromium 的项目以这种方式构建,比如 Breach 浏览器

🌐 Apart from Electron, there were also other Chromium-based projects built in this way, like the Breach browser.

过滤导出的符号

🌐 Filtering exported symbols

在 Windows 上,一个共享库可以导出的符号数量是有限制的。随着 Chromium 代码库的增长,libchromiumcontent 导出的符号数量很快就超过了这个限制。

🌐 On Windows there is a limitation of how many symbols one shared library can export. As the codebase of Chromium grew, the number of symbols exported in libchromiumcontent soon exceeded the limitation.

解决方法是在生成 DLL 文件时过滤掉不需要的符号。它通过 向链接器提供一个 .def 文件 工作,然后使用脚本来 判断命名空间下的符号是否应被导出

🌐 The solution was to filter out unneeded symbols when generating the DLL file. It worked by providing a .def file to the linker, and then using a script to judge whether symbols under a namespace should be exported.

通过采取这种方法,尽管 Chromium 不断添加新的导出符号,libchromiumcontent 仍然可以通过剥离更多符号来生成共享库文件。

🌐 By taking this approach, though Chromium kept adding new exported symbols, libchromiumcontent could still generate shared library files by stripping more symbols.

组件构建

🌐 Component build

在讨论 libchromiumcontent 的下一步操作之前,首先有必要介绍一下 Chromium 中的组件构建概念。

🌐 Before talking about the next steps taken in libchromiumcontent, it is important to introduce the concept of component build in Chromium first.

作为一个庞大的项目,在 Chromium 中构建时链接步骤需要很长时间。通常,当开发者做一个小改动时,看到最终输出可能需要 10 分钟。为了解决这个问题,Chromium 引入了组件构建,它将 Chromium 中的每个模块构建为独立的共享库,因此在最终链接步骤中所花费的时间变得可以忽略不计。

🌐 As a huge project, the linking step takes very long in Chromium when building. Normally when a developer makes a small change, it can take 10 minutes to see the final output. To solve this, Chromium introduced component build, which builds each module in Chromium as separated shared libraries, so the time spent in the final linking step becomes unnoticeable.

发送原始二进制文件

🌐 Shipping raw binaries

随着 Chromium 的持续发展,Chromium 中导出的符号数量非常多,以至于即使是 Content 模块和 Webkit 的符号也超过了限制。仅仅通过剥离符号是不可能生成可用的共享库的。

🌐 With Chromium continuing to grow, there were so many exported symbols in Chromium that even the symbols of Content Module and Webkit were more than the limitation. It was impossible to generate a usable shared library by simply stripping symbols.

最终,我们不得不 发送 Chromium 的原始二进制文件,而不是生成一个单独的共享库。

🌐 In the end, we had to ship the raw binaries of Chromium instead of generating a single shared library.

如前所述,Chromium 有两种构建模式。因此,由于需要发布原始二进制文件,我们必须在 libchromiumcontent 中发布两种不同的二进制分发版本。一种称为 static_library 构建,它包含 Chromium 正常构建生成的每个模块的所有静态库。另一种是 shared_library,它包含组件构建生成的每个模块的所有共享库。

🌐 As introduced earlier there are two build modes in Chromium. As a result of shipping raw binaries, we have to ship two different distributions of binaries in libchromiumcontent. One is called static_library build, which includes all static libraries of each module generated by the normal build of Chromium. The other is shared_library, which includes all shared libraries of each module generated by the component build.

在 Electron 中,调试版本链接的是 shared_library 版本的 libchromiumcontent,因为它下载体积小,且在链接最终可执行文件时耗时短。而 Electron 的发布版本则链接的是 static_library 版本的 libchromiumcontent,因此编译器可以生成完整的符号,这对于调试非常重要,同时链接器能够进行更好的优化,因为它知道哪些目标文件是需要的,哪些是不需要的。

🌐 In Electron, the Debug version is linked with the shared_library version of libchromiumcontent, because it is small to download and takes little time when linking the final executable. And the Release version of Electron is linked with the static_library version of libchromiumcontent, so the compiler can generate full symbols which are important for debugging, and the linker can do much better optimization since it knows which object files are needed and which are not.

所以对于正常开发,开发者只需要构建调试版本,这不需要良好的网络或高性能的机器。虽然发布版本需要更好的硬件来构建,但它可以生成更优化的二进制文件。

🌐 So for normal development, developers only need to build the Debug version, which does not require a good network or powerful machine. Though the Release version then requires much better hardware to build, it can generate better optimized binaries.

gn 更新

🌐 The gn update

作为世界上最大的项目之一,大多数普通系统不适合构建 Chromium,因此 Chromium 团队开发了自己的构建工具。

🌐 Being one of the largest projects in the world, most normal systems are not suitable for building Chromium, and the Chromium team develops their own build tools.

早期版本的 Chromium 使用 gyp 作为构建系统,但它存在速度慢的问题,对于复杂项目,其配置文件也变得难以理解。经过多年的开发,Chromium 切换到了 gn 作为构建系统,它速度更快且具有清晰的架构。

🌐 Earlier versions of Chromium were using gyp as a build system, but it suffers from being slow, and its configuration file becomes hard to understand for complex projects. After years of development, Chromium switched to gn as a build system, which is much faster and has a clear architecture.

gn 的一个改进是引入 source_set,它表示一组目标文件。在 gyp 中,每个模块要么由 static_library 表示,要么由 shared_library 表示,对于 Chromium 的常规构建,每个模块会生成一个静态库,并在最终可执行文件中链接在一起。通过使用 gn,每个模块现在只生成一堆目标文件,最终可执行文件只是将所有目标文件链接在一起,因此不再生成中间的静态库文件。

🌐 One of the improvements of gn is to introduce source_set, which represents a group of object files. In gyp, each module was represented by either static_library or shared_library, and for the normal build of Chromium, each module generated a static library and they were linked together in the final executable. By using gn, each module now only generates a bunch of object files, and the final executable just links all the object files together, so the intermediate static library files are no longer generated.

然而,这种改进给 libchromiumcontent 带来了很大麻烦,因为 libchromiumcontent 实际上需要这些中间的静态库文件。

🌐 This improvement however made great trouble to libchromiumcontent, because the intermediate static library files were actually needed by libchromiumcontent.

解决这个问题的第一次尝试是使用 修补 gn 以生成静态库文件,虽然它解决了问题,但离一个体面的解决方案还差得远。

🌐 The first try to solve this was to patch gn to generate static library files, which solved the problem, but was far from a decent solution.

@alespergl进行了第二次尝试,针对从对象文件列表生成自定义静态库。 它使用了一种技巧,先运行一个虚拟构建以收集生成的对象文件列表,然后通过将列表提供给gn来实际构建静态库。它对Chromium的源代码只做了最小的修改,并保持了Electron的构建架构不变。

🌐 The second try was made by @alespergl to produce custom static libraries from the list of object files. It used a trick to first run a dummy build to collect a list of generated object files, and then actually build the static libraries by feeding gn with the list. It only made minimal changes to Chromium's source code, and kept Electron's building architecture still.

概括

🌐 Summary

正如你所见,与作为 Chromium 一部分来构建 Electron 相比,将 Chromium 构建为一个库需要更多的努力并持续进行维护。然而,后者取消了构建 Electron 对高性能硬件的需求,从而使更广泛的开发者能够构建和贡献 Electron。这一努力完全值得。

🌐 As you can see, compared to building Electron as part of Chromium, building Chromium as a library takes greater efforts and requires continuous maintenance. However the latter removes the requirement of powerful hardware to build Electron, thus enabling a much larger range of developers to build and contribute to Electron. The effort is totally worth it.