Files
Samuel Attard 08c729bdf3 fix: cppgc WebContents stack follow-ups (#54163)
* fix: throw a clear error from a Session made inert at shutdown

Build Session's template with gin_helper::ObjectTemplateBuilder so a call on
a wrapper the MicrotasksRunner has invalidated throws "Object has been
destroyed" instead of gin's "Illegal invocation". Report the result of the
shutdown-order spec through stdout, since app.exit() cannot change the exit
code once the dispose pass is running.

* fix: dispose the MicrotasksRunner when startup exits early

When toolkit initialisation fails (e.g. no display) content never runs
PostMainMessageLoopRun(), so ~JavascriptEnvironment destroyed the runner
after the isolate had been disposed and with observers still registered.

* fix: emit debugger 'detach' for contents disposed at quit

The lifecycle client is disposed before its WebContents now, so notify the
Debugger when that detaches an attached client instead of dropping the
event.

* fix: do not wrap a collected WebContents that is awaiting teardown

Between the wrapper being collected and the deferred NativeLifecycle
teardown the content::WebContents is still alive; FromOrCreate() minted a
second, remote wrapper for it and emitted web-contents-created. Return null
for that window instead.

* chore: do not alias an owned MicrotasksRunner observer with a raw_ptr

* fix: identify Electron wrappers by WrapperInfo in the serializer

Unwrapping foreign API wrappers with Electron's tag range trips a V8 DCHECK
in is_debug builds; unwrap with the generic range and check the type info,
as gin does.

* fix: let BluetoothChooser settle requests when its wrapper is gone

* docs: add the ObjectTemplateBuilder include to the creating-api sample
2026-09-21 18:59:13 +09:00

6.8 KiB

Creating a New Electron Browser Module

Welcome to the Electron API guide! If you are unfamiliar with creating a new Electron API module within the browser directory, this guide serves as a checklist for some of the necessary steps that you will need to implement.

This is not a comprehensive end-all guide to creating an Electron Browser API, rather an outline documenting some of the more unintuitive steps.

Add your files to Electron's project configuration

Electron uses GN as a meta build system to generate files for its compiler, Ninja. This means that in order to tell Electron to compile your code, we have to add your API's code and header file names into filenames.gni.

You will need to append your API file names alphabetically into the appropriate files like so:

lib_sources = [
    "path/to/api/api_name.cc",
    "path/to/api/api_name.h",
]

lib_sources_mac = [
    "path/to/api/api_name_mac.h",
    "path/to/api/api_name_mac.mm",
]

lib_sources_win = [
    "path/to/api/api_name_win.cc",
    "path/to/api/api_name_win.h",
]

lib_sources_linux = [
    "path/to/api/api_name_linux.cc",
    "path/to/api/api_name_linux.h",
]

Note that the Windows, macOS and Linux array additions are optional and should only be added if your API has specific platform implementations.

Create API documentation

Type definitions are generated by Electron using @electron/docs-parser and @electron/typescript-definitions. This step is necessary to ensure consistency across Electron's API documentation. This means that for your API type definition to appear in the electron.d.ts file, we must create a .md file. Examples can be found in this folder.

Set up ObjectTemplateBuilder and Wrappable

Electron constructs its modules using object_template_builder.

wrappable is a base class for C++ objects that have corresponding v8 wrapper objects.

Here is a basic example of code that you may need to add, in order to incorporate object_template_builder and wrappable into your API. For further reference, you can find more implementations here.

In your api_name.h file:


#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_{API_NAME}_H_
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_{API_NAME}_H_

#include "gin/wrappable.h"

namespace electron {

namespace api {

class ApiName final : public gin::Wrappable<ApiName> {
 public:
  static ApiName* Create(v8::Isolate* isolate);

  // gin::Wrappable
  static const gin::WrapperInfo kWrapperInfo;
  void Trace(cppgc::Visitor* visitor) const override;
  const gin::WrapperInfo* wrapper_info() const override;
  gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
      v8::Isolate* isolate) override;
  const char* GetHumanReadableName() const override;

  // Public for cppgc::MakeGarbageCollected.
  ApiName();
  ~ApiName() override;

  void MethodName();
};

}  // namespace api
}  // namespace electron

In your api_name.cc file:

#include "shell/browser/api/electron_api_api_name.h"

#include "gin/object_template_builder.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/wrappable_pointer_tags.h"
#include "shell/common/node_includes.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"

namespace electron {

namespace api {

const gin::WrapperInfo ApiName::kWrapperInfo =
    electron::MakeWrapperInfo(electron::kElectronApiName);

ApiName::ApiName() = default;
ApiName::~ApiName() = default;

// static
ApiName* ApiName::Create(v8::Isolate* isolate) {
  return cppgc::MakeGarbageCollected<ApiName>(
      isolate->GetCppHeap()->GetAllocationHandle());
}

void ApiName::Trace(cppgc::Visitor* visitor) const {
  gin::Wrappable<ApiName>::Trace(visitor);
}

const gin::WrapperInfo* ApiName::wrapper_info() const {
  return &kWrapperInfo;
}

gin::ObjectTemplateBuilder ApiName::GetObjectTemplateBuilder(
    v8::Isolate* isolate) {
  return gin::Wrappable<ApiName>::GetObjectTemplateBuilder(isolate)
      .SetMethod("methodName", &ApiName::MethodName);
}

const char* ApiName::GetHumanReadableName() const {
  return "Electron / ApiName";
}

void ApiName::MethodName() {}

}  // namespace api

}  // namespace electron

namespace {

void Initialize(v8::Local<v8::Object> exports,
                v8::Local<v8::Value> unused,
                v8::Local<v8::Context> context,
                void* priv) {
  v8::Isolate* const isolate = v8::Isolate::GetCurrent();
  gin_helper::Dictionary dict(isolate, exports);
  dict.Set("apiName", electron::api::ApiName::Create(isolate));
}

}  // namespace

Add a unique kElectronApiName value to shell/common/gin_helper/wrappable_pointer_tags.h for the API's gin::WrapperInfo.

In the typings/internal-ambient.d.ts file, we need to append a new property onto the Process interface like so:

interface Process {
  _linkedBinding(name: 'electron_browser_{api_name}'): Electron.ApiName;
}

At the very bottom of your api_name.cc file:

NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_{api_name},Initialize)

In your shell/common/node_bindings.cc file, add your node binding name to Electron's built-in modules.

#define ELECTRON_BROWSER_MODULES(V)      \
  V(electron_browser_{api_name})

Note

More technical details on how Node links with Electron can be found on our blog.

Expose your API to TypeScript

Export your API as a module

We will need to create a new TypeScript file in the path that follows:

"lib/browser/api/{electron_browser_{api_name}}.ts"

An example of the contents of this file can be found here.

Expose your module to TypeScript

Add your module to the module list found at "lib/browser/api/module-list.ts" like so:

export const browserModuleList: ElectronInternal.ModuleEntry[] = [
  { name: 'apiName', loader: () => require('./api-name') }
];