Logo
Back to perspectives
October 16, 2024From the dev desk 4 min

Electron spins: a special case of Chromium mods

By John Turpish

Challenges of modifying Chromium, compounded

Chromium is a powerful project, implementing functionality that's critical to the web and web-related technologies. Perhaps the greatest credit to its quality and completeness is the fact that it's become an almost-de facto standard. So it's no surprise that other projects would want to inherit much of that codebase.

You can't upstream everything you'd want to do with it, as your needs are not necessarily the needs of everyone everywhere. Chromium would be a chaotic mess if everyone upstreamed their Chromium mods.

Chromium is also very active, with code changes happening nearly constantly, and some of them are quite important. So forking Chromium isn't attractive, since you'd want to benefit from the rest of the community.

This leaves a number of projects in the sometimes-hairy situation of being “downstream,” defining their code as a series of patches, file additions, and/or file deletions on top of Chromium's codebase. And of course this means a fair amount of work needs to be done to accommodate upstream changes. Electron is in this camp, and as a very active project in its own right, they do an excellent job of it.

As a testament to how effective and useful Electron is, just look at the applications using it. Millions of people are starting up Electron every day without even realizing it. Just consider these examples:

  • Messaging apps’ desktop versions, like:
    • Matrix
    • Signal
    • Slack
    • Discord
  • Developer tools like:
    • Visual Studio Code
    • Postman
    • Github Desktop
    • Hyper
  • Most user-facing app categories, including:
    • Loom
    • Asana
    • Twitch
    • WordPress

So what if someone wanted a slightly modified Electron? Perhaps their application could use some feature that is too specific to be in-scope for the upstream Electron project. Should they create their own spin on Electron? They need to ask themselves a very serious question: are they really going to maintain a project that is a set of patches on top of a rapidly-moving set of patches on top of a rapidly-moving project? How difficult is this? Could we make it easier?

A proposal for sane native extensibility

What if, instead of upstreaming all our niche changes, we could upstream just some extension points to make this process easier?

Adding files wholesale is considerably easier than patching upstream files. We get to skip the merge conflicts, and it might be easier to test them in isolation. So for build-time extension, a build-script could change its behavior based on the presence of a particular file path. That way if you don't change anything, you get the normal build behavior, but if you do make changes, Electron hooks into your code without you changing anything.

On the downstream side, this means you would:

  • Add a directory for your module, complete with its own BUILD.gn 
  • Have a source header with a predefined name that implements a predefined function signature for each hook you want to use

On the upstream (Electron) side, this means:

  • The build script notes the presence of your BUILD.gn and adds a dependency so it can call into your code
  • For each predefined header file name that exists, the build script flips a build flag
  • In the appropriate part of the code, if the compile-time flag is set, it includes your header and compiles a call into your function

Some extension points

Presumably this proposed approach would/could change in the future. I haven't imagined every conceivable use case. But I did a few demonstrations of the technique, and for them I found these sufficient:

  • Initializing (if needed) immediately after PreMainMessageLoopRun
  • Registering additional preferences
  • Creating URL Loader Request interceptor(s)
  • Creating sub-resource URL loader factories
  • Returning a list of additional protocol schemes to consider “handled”

Example demos

To whet your appetite, here are a few demonstrations I cooked up.

Rendering markdown

This spin on Electron uses a URL Loader Request interceptor that kicks in when a user navigates (e.g., clicks a link to) a raw Markdown document. It uses CMark to convert it to HTML, and renders that. One can also substitute `cmark-gfm` if one prefers GitHub-flavored markdown.

Electron spin_rendering markdown.png

Replacing resources with static data

This Electron tweak uses a sub-resources URL Loader to intercept requests from `<img>`, fetch, etc. whose URLs end in ".jpg" and then respond to them with a chunk of static data. Obviously this is a silly toy example, but replacing HTTP requests with static resources could have uses.

Electron spin_Replacing resources with static data.png

Custom protocols

This Electron spin uses all five of the above hooks and supports `ipfs://` and `ipns://` schemes.

Electron spin_Custom protocols.png

Bottom line

If the Electron project adopted this approach to extensibility, it could open up a world where installable JavaScript apps can use their leading platform, but also have access to features which aren't suitable to be included in a global-scoped project like Chromium or Electron —  features that are too new, too non-standard, too niche, or too integrated with the OS. This also applies to features with implications that are tricky in contexts outside of their app. And they can do this with only modest ongoing maintenance cost.

It also becomes quite reasonable to isolate just the feature and its modification into a project that a few JS apps could share simply by changing `electron-prebuilt` to `my-electron-prebuilt` in their build system.

Is your project already using a modified Electron? What use-case-specific functionality would you like your app to have? What do you think about this approach? Don't be afraid to join the conversation on GitHub or to reach out to me directly. I'd love to hear your thoughts!

 

John Turpish
John TurpishSenior Backend Engineer, Little Bear LabsLinkedIn

John's a dev who likes to build to last; most known for his work in native-compiling languages like Rust & C++. Dive deep or collaborate: why not both?


More perspectives