Skip to main content

Electron 中从原生到 JavaScript

· 8 min read

Electron 中用 C++ 或 Objective-C 编写的功能是如何转化为 JavaScript 供终端用户使用的?

¥How do Electron's features written in C++ or Objective-C get to JavaScript so they're available to an end-user?


背景

¥Background

Electron 是一个 JavaScript 平台,其主要目的是降低开发者构建健壮桌面应用的门槛,而无需担心特定于平台的实现。然而,从本质上讲,Electron 本身仍然需要用特定的系统语言编写特定于平台的功能。

¥Electron is a JavaScript platform whose primary purpose is to lower the barrier to entry for developers to build robust desktop apps without worrying about platform-specific implementations. However, at its core, Electron itself still needs platform-specific functionality to be written in a given system language.

实际上,Electron 会为你处理原生代码,以便你可以专注于单个 JavaScript API。

¥In reality, Electron handles the native code for you so that you can focus on a single JavaScript API.

但是,这到底是怎么回事?Electron 中用 C++ 或 Objective-C 编写的功能是如何转化为 JavaScript 供终端用户使用的?

¥How does that work, though? How do Electron's features written in C++ or Objective-C get to JavaScript so they're available to an end-user?

要追溯这条路径,让我们从 app 模块 开始。

¥To trace this pathway, let's start with the app module.

通过在 lib/ 中打开 app.ts 文件目录,你会在顶部找到以下代码行:

¥By opening the app.ts file inside our lib/ directory, you'll find the following line of code towards the top:

const binding = process.electronBinding('app');

此行代码直接指向 Electron 将其 C++/Objective-C 模块绑定到 JavaScript 以供开发者使用的机制。此功能由标头和 ElectronBindings 类的 实现文件 创建。

¥This line points directly to Electron's mechanism for binding its C++/Objective-C modules to JavaScript for use by developers. This function is created by the header and implementation file for the ElectronBindings class.

process.electronBinding

这些文件添加了 process.electronBinding 函数,其行为类似于 Node.js 的 process.bindingprocess.binding 是 Node.js require() 方法的低级实现,不同之处在于它允许用户 require 原生代码,而不是其他 JS 代码。此自定义 process.electronBinding 函数允许从 Electron 加载原生代码。

¥These files add the process.electronBinding function, which behaves like Node.js’ process.binding. process.binding is a lower-level implementation of Node.js' require() method, except it allows users to require native code instead of other code written in JS. This custom process.electronBinding function confers the ability to load native code from Electron.

当顶层 JavaScript 模块(如 app)需要此原生代码时,如何确定和设置该原生代码的状态?暴露给 JavaScript 的方法在哪里?属性呢?

¥When a top-level JavaScript module (like app) requires this native code, how is the state of that native code determined and set? Where are the methods exposed up to JavaScript? What about the properties?

native_mate

目前,这个问题的答案可以在 native_mate 中找到:Chromium gin 的一个 fork,可以更轻松地在 C++ 和 JavaScript 之间编组类型。

¥At present, answers to this question can be found in native_mate: a fork of Chromium's gin library that makes it easier to marshal types between C++ and JavaScript.

native_mate/native_mate 内部包含 object_template_builder 的头文件和实现文件。这使得我们能够使用原生代码构建模块,其结构符合 JavaScript 开发者的期望。

¥Inside native_mate/native_mate there's a header and implementation file for object_template_builder. This is what allow us to form modules in native code whose shape conforms to what JavaScript developers would expect.

mate::ObjectTemplateBuilder

如果我们将每个 Electron 模块视为 object,就更容易理解为什么我们要使用 object_template_builder 来构建它们。此类构建于公开的类之上由谷歌开源高性能 JavaScript 和 WebAssembly 引擎 V8 开发,使用 C++ 编写。V8 实现了 JavaScript(ECMAScript)规范,因此其原生功能实现可以直接与 JavaScript 中的实现关联。例如,v8::ObjectTemplate 为我们提供了 JavaScript 对象,而无需专用的构造函数和原型。它使用 Object[.prototype],在 JavaScript 中相当于 Object.create()

¥If we look at every Electron module as an object, it becomes easier to see why we would want to use object_template_builder to construct them. This class is built on top of a class exposed by V8, which is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. V8 implements the JavaScript (ECMAScript) specification, so its native functionality implementations can be directly correlated to implementations in JavaScript. For example, v8::ObjectTemplate gives us JavaScript objects without a dedicated constructor function and prototype. It uses Object[.prototype], and in JavaScript would be equivalent to Object.create().

要查看实际运行情况,请查看应用模块 atom_api_app.cc 的实现文件。底部内容如下:

¥To see this in action, look to the implementation file for the app module, atom_api_app.cc. At the bottom is the following:

mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("getGPUInfo", &App::GetGPUInfo)

在上面的一行中,.SetMethodmate::ObjectTemplateBuilder 上被调用。可以在 ObjectTemplateBuilder 类的任何实例上调用 .SetMethod,以 JavaScript 方式设置 对象原型 上的方法,语法如下:

¥In the above line, .SetMethod is called on mate::ObjectTemplateBuilder. .SetMethod can be called on any instance of the ObjectTemplateBuilder class to set methods on the Object prototype in JavaScript, with the following syntax:

.SetMethod("method_name", &function_to_bind)

这是 JavaScript 等效的:

¥This is the JavaScript equivalent of:

function App{}
App.prototype.getGPUInfo = function () {
// implementation here
}

此类还包含用于设置模块属性的函数:

¥This class also contains functions to set properties on a module:

.SetProperty("property_name", &getter_function_to_bind)

or

.SetProperty("property_name", &getter_function_to_bind, &setter_function_to_bind)

这些将是 Object.defineProperty 的 JavaScript 实现:

¥These would in turn be the JavaScript implementations of Object.defineProperty:

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
})

and

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
set(newPropertyValue) {
_myProperty = newPropertyValue
}
})

现在,我们可以按照开发者的期望,创建由原型和属性构成的 JavaScript 对象,并且更清晰地推断在这个较低的系统级别实现的函数和属性!

¥It’s possible to create JavaScript objects formed with prototypes and properties as developers expect them, and more clearly reason about functions and properties implemented at this lower system level!

关于在何处实现任何给定模块方法的决定本身就是一个复杂且常常不确定的问题,我们将在以后的文章中讨论。

¥The decision around where to implement any given module method is itself a complex and oft-nondeterministic one, which we'll cover in a future post.