From native to JavaScript in Electron
How do Electron's features written in C++ or Objective-C get to JavaScript so they're available to an end-user?
Background
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.
In reality, Electron handles the native code for you so that you can focus on a single JavaScript API.
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?
To trace this pathway, let's start with the app
module.
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');
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
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.
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
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.
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
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()
.
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)
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)
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)
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
}
})
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.