Introduction
Windows on Arm (WoA) devices, such as the Surface Pro X, can run x86 Windows apps under emulation. With Insider builds, WoA devices can also emulate Windows apps built for x64. Emulation translates the x86 CPU instructions to ARM instructions. Of course, this is not as fast as running applications natively (that is, directly compiled for the ARM architecture and instruction set) — so if you want to take advantage of the platform’s power and maximize battery life, you will want to create native apps.
There are several approaches to creating a native app for WoA. For example, you could create a Universal Windows Platform app targeting Arm devices, as explained in Today’s Best Practices for Migrating Windows Apps.
Another great way to create a portable, native app is with a WebView framework like CEF or Electron. Electron enables building lightweight, native applications on web technologies, making it easy to create applications portable to various platforms, including Windows on Arm. With Electron, you can write desktop applications in HTML, CSS, and JavaScript because it enables you to embed a web app within a native application shell built on top of Chromium. Then, you can package and distribute the resulting application as a standalone app. You can also use Electron as part of a more extensive native application. Or, you can use it for the entire application interface. Some well-known applications built entirely on Electron are the editor Visual Studio Code and the communication platform Slack.
It’s worth keeping in mind that your packaged application will be quite large because Chromium gets shipped with it. Also, consider that security can be an inherent challenge as Chromium publishes vulnerabilities before Electron can adopt fixes.
In this article, we will look at how you can create a new Electron app targeted at 64-bit WoA devices and how you can port existing Electron apps.
A New Electron App
As a demonstration, we create a new Electron app targeted at a Windows device with a 64-bit Arm Cortex-A (Aarch64) CPU, but built on an x86-64 Windows machine. First, we must have Node.js and Git installed. (Git is a dependency of the electron-forge packager we use in this demo.)
We start by creating a new directory for our Electron app and initialize it using npm:
mkdir electron-arm
cd electron-arm
npm init -y
Then we set the npm_config_arch
environment variable, telling npm that we want to download the Arch64 version of our dependencies. In this case, npm calls the architecture arm64
:
set npm_config_arch=arm64
Note: If you are using PowerShell, this will not correctly set the environment variable. You must do this:
$env:npm_config_arch = 'arm64'
Now we can install the electron dependency for our application:
npm install --save-dev electron
Because we have set the architecture to arm64
, npm has installed the Aarch64 version in node_modules. We can verify this by going to node_modules\electron\dist and trying to run electron.exe. The application should not be able to run because of the architecture mismatch.
If you get a successful Electron window instead of this error, you have installed the x64 version instead. In that situation, delete the node_modules directory, recheck the previous steps regarding architecture, and retry npm install
.
Of course, because we have installed the Aarch64 version of Electron, you will not be able to run the app on your x64 machine. While that is no problem for this small demo, in general, you will want to run and test your app on your development machine as well. To switch between architectures, delete the node_modules directory, set npm_config_arch
as desired, and run npm install
.
Let’s now modify package.json to change the entry point to main.js (instead of index.js, as the value of the main
property) and the name of the author
property. In addition, we add a description to the description
property (both are a prerequisite for electron-forge, which we use later in this demo).
...
"author": "Your Name",
"description": "Electron on WoA demo",
"main": "main.js",
...
We use code closely based on Electron’s Quick Start Guide for the HTML and script files, but instead of displaying the version numbers of Node.js, Chrome, and Electron, we display the CPU architecture (from the process.arch file). So now, our main.js file is identical to the one from the Quick Start Guide. Create the file main.js in your editor with these contents:
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
Then create index.html with the following HTML document:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron on WoA demo</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body style="background: white;">
<p>
Architecture: <span id="arch"></span>
</p>
</body>
</html>
In preload.js, we fill the <span>
element with the contents of process.arch:
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('arch').textContent = process.arch;
})
Now we can package our app for a Windows-on-Arm device using electron-forge. But first, we install electron-forge as a dependency and import it to the app folder.
npm install --save-dev @electron-forge/cli
npx electron-forge import
This modifies package.json to add extra information for electron-forge, including package and make scripts.
"package": "electron-forge package",
"make": "electron-forge make"
This means that we can now run the npm run package (to create a directory with the Electron application and all its necessary resource files). Or we can run npm run
make (to create a single, distributable, executable file). However, Electron Forge uses the architecture of our development machine unless we specify otherwise using the —arch
flag. We add two additional values in package.json to the scripts
property to package the application for arm64, alongside package and make.
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"package-arm": "electron-forge package --arch=arm64",
"make-arm": "electron-forge make --arch=arm64"
}
Then we execute:
npm run make-arm
In the out directory, we can see the packaged application in electron-arm-win32-arm64
(the name depends on the name property in package.json) and a distributable setup executable in make\squirrel.windows\arm64. If we try running either on a development machine, they will not work. We get a similar error to trying to run the Aarch64 electron.exe file. However, if we put the package or the setup file on a Windows on Arm device, they will work just fine.
If you run our application on the Arm device, we see this:
As you can see, the application is running natively on an Arm device. For comparison, we can try packaging an app for architecture ia32 (which is x86) instead of arm64 (which is Aarch64). If we run that on an Arm device, we see this, indicating that it is using emulation:
Porting an Existing App
Suppose we have already have written an Electron app, and now we want to port it to Windows on Arm. Suppose further that our application does not use any native modules. In this scenario, we delete the node_modules folder, and then follow the previous section’s steps: Set npm_config_arch
to arm64
, reinstall dependencies, and package the application. We must be careful with if-statements based on process.arch because its value can now also be arm64
.
If we have native modules (and assuming we are using Visual Studio 2019 with the Desktop development with C++ workload already), we must install the C++ build tools for MSVC. This tutorial uses version 16.9.31313.79 of Visual Studio 2019, but the steps should work for any version on Visual Studio 2019.
Using the Visual Studio installer, select Modify for our installation, navigate to Individual components, and look for MSVC v142 - VS 2019 C++ ARM64 build tools (Latest). Then install that component.
Once we have done that, everything is as simple as before. Electron Forge compiles our native modules when we package the application, and if we have the ARM64 build tools installed, it can cross-compile them for an Aarch64 CPU target just fine.
If you do not have an application with native modules, but still want to try it, you can modify the earlier demo application to include microtime as a dependency. Microtime is a native module to get the current time in microseconds. Then we update our index.html and preload.js to display the value of the microtime.now
function in the window. For example, this element could be added to index.html:
<a><p>
Microtime: <span id="microtime"></span>
</p></a>
And preload.js can contain this code to update the element regularly:
<a>const microtime = require('microtime');
window.addEventListener('DOMContentLoaded', () => {
setInterval(() => {
document.getElementById('microtime').textContent = microtime.now();
}, 500);
})</a>
We make sure we have Visual Studio 2019, with the Desktop development with C++ workload and the MSVC v142 - VS 2019 C++ ARM64 build tools (Latest) component. Then we try packaging the application with Electron Forge as in the previous demo, and running it on our Windows on Arm device.
To take full advantage of the Windows on Arm platform, you will want to create your applications natively to the ARM architecture. Doing so is easy using the Electron framework. It does not take much more than changing some configuration settings and installing a toolset for native modules. Now that you know how to write native Electron apps for WoA, try it on your own applications.
If you are interested in further reading, refer to these articles: