How Electron Calls Windows Native APIs
How Electron Calls Windows Native APIs
Calling Windows native APIs in Electron applications is like wanting to see the sea but only being able to look at a map. After some trial and error, I’ve finally found a few paths, so I’m writing this article as a memorial and to guide those who come after.
Background
When building Electron desktop applications, you inevitably need to interact with the operating system. On Windows, these requirements are quite numerous:
- Call Microsoft Store API for in-app purchases
- Handle file system virtualization specific to Microsoft Store applications
- Obtain system-level permissions and resources
- Interact with Windows Runtime (WinRT) components
Electron is, after all, a Node.js environment, and Node.js doesn’t natively provide the ability to access Windows native APIs. A bridge is needed between the two.
It’s like wanting to communicate with a friend who doesn’t understand Chinese - you need a translator in between. Electron is written in JavaScript, while Windows APIs are written in C/C++. The languages don’t match, so you need to find a way to build a bridge. This is the harsh reality of the coding world - no human empathy here.
About HagiCode
The solutions shared in this article come from our practical experience in the HagiCode project. HagiCode Desktop needs to call Microsoft Store API to handle subscription purchases and license management, which is why we explored a set of technical solutions. After all, necessity is the mother of invention.
Comparison of Technical Solutions
There are several mainstream solutions for calling Windows native APIs in Electron. Each solution has its applicable scenarios, just like different tools in a toolbox - they can only maximize their utility when used correctly, otherwise they just add trouble.
| Solution | Applicable Scenarios | Pros | Cons |
|---|---|---|---|
| dynwinrt | WinRT API (like Store API) | Type-safe, auto-generated bindings, modern JavaScript support | Only supports WinRT API, requires Windows SDK |
| Native Node.js Extensions | High-performance, any Windows API | Complete control, optimal performance | Requires C++ development skills, complex cross-platform |
| child_process + PowerShell | One-time, temporary calls | Simple and quick, no compilation needed | Poor performance, complex error handling |
| edge.js/ffi-napi | Calling existing DLLs | Can reuse existing libraries | Compatibility issues, high maintenance cost |
HagiCode Desktop adopts a hybrid approach: using dynwinrt to access Microsoft Store API, using native Node.js extensions to handle high-performance Store purchase operations, and using Node.js native fs and path modules to handle file system virtualization specific to Microsoft Store applications. Keep it simple when possible - that’s our principle.
Solution 1: Using dynwinrt to Call WinRT API
dynwinrt is a toolchain provided by Microsoft that can automatically generate JavaScript bindings based on Windows SDK metadata files. It’s specifically designed for calling WinRT APIs, such as Microsoft Store API.
Install dependencies:
{ "optionalDependencies": { "@microsoft/dynwinrt": "0.1.0-preview.6", "@microsoft/dynwinrt-codegen": "0.1.0-preview.6" }}Generate WinRT bindings:
const { execFileSync } = 'node:child_process';
function generateStoreNamespace(windowsWinmdPath) { execFileSync('npx', [ 'dynwinrt-codegen', 'generate', '--winmd', windowsWinmdPath, '--namespace', 'Windows.Services.Store', '--output', 'src/main/subscription/generated-js', '--lang', 'js', ]);}Use generated bindings:
// Use Store API bindings generated by dynwinrtimport { Windows } from '../subscription/generated-js/index.js';
async function queryStoreProduct(storeId: string) { const storeContext = Windows.Services.Store.StoreContext.getDefault(); const result = await storeContext.getAssociatedStoreProductsAsync(['Subscription', 'Durable']);
if (result.extendedError !== 0) { throw new Error(`Store API error: ${result.extendedError}`); }
return result.products.get(storeId);}The benefit of dynwinrt is type safety, and the generated code is consistent with modern JavaScript conventions. However, it can only handle WinRT APIs - if you need to call traditional Win32 APIs, you’ll need to use a different solution. Tools are like that - each has its strengths.
Solution 2: Native Node.js Extensions
When you need high performance or functionality not supported by dynwinrt, native Node.js extensions are the best choice. This solution requires writing code in C++ and then compiling it into .node files using node-gyp.
Create binding.gyp:
{ "targets": [{ "target_name": "windows-store-addon", "sources": ["src/windows-store-addon.cpp"], "include_dirs": [ "<!(node -e \"require('nan')\")" ], "defines": [ "WIN32_LEAN_AND_MEAN" ] }]}C++ native module example:
#include <nan.h>#include <windows.h>#include <wrl.h>#include <windows.services.store.h>
using namespace v8;using namespace Windows::Services::Store;
NAN_METHOD(QueryStoreStatus) { auto async = new Nan::AsyncWorker( []() { // Call Microsoft Store API auto context = StoreContext::GetDefault(); auto products = context->GetAssociatedStoreProductsAsync(...)->GetResults(); // Process results } ); Nan::AsyncQueueWorker(async);}
NAN_MODULE_INIT(InitModule) { Nan::Set(target, Nan::New("queryStoreStatus").ToLocalChecked(), Nan::GetFunction(Nan::New<FunctionTemplate>(QueryStoreStatus)).ToLocalChecked());}
NODE_MODULE(windows_store_addon, InitModule)Compile and use:
node-gyp rebuildimport addon from './build/Release/windows-store-addon.node';
const result = addon.queryStoreStatus({ storeId: 'your-store-id', productKinds: ['Subscription', 'Durable']});Native extensions offer the best performance, but development costs are high. You need to know C++ and handle cross-platform compatibility issues. If your team has C++ experience or performance requirements are exceptionally high, this solution is worth the investment. It’s just that walking this path is, ultimately, a bit harder.
Solution 3: Handling Microsoft Store Application Virtualization
Microsoft Store applications run in a virtualized environment, and path mapping requires special handling. HagiCode Desktop uses the following function to handle this issue:
export function resolveWindowsStorePackageFamilyName(executablePath: string): string | null { const WINDOWS_APPS_SEGMENT = '\\windowsapps\\'; const windowsPath = executablePath.replace(/\//g, '\\'); const markerIndex = windowsPath.toLowerCase().indexOf(WINDOWS_APPS_SEGMENT);
if (markerIndex < 0) return null;
const relativePath = windowsPath.slice(markerIndex + WINDOWS_APPS_SEGMENT.length); const packageFullName = relativePath.split('\\', 1)[0]?.trim(); return packageFullName || null;}
export function resolveWindowsStoreVirtualizedPhysicalPath( logicalPath: string, options: ResolveWindowsStorePathDisplayOptions = {}): string | null { const packageFamilyName = options.packageFamilyName ?? resolveWindowsStorePackageFamilyName(options.execPath ?? process.execPath); if (!packageFamilyName) return null;
const packageStorageRoot = path.win32.join( options.env.LOCALAPPDATA, 'Packages', packageFamilyName );
// Map virtualized path to physical path if (isPathWithinWindowsRoot(logicalPath, options.env.APPDATA)) { return path.win32.join( packageStorageRoot, 'LocalCache', 'Roaming', path.win32.relative(options.env.APPDATA, logicalPath) ); }
return null;}Virtualization is actually quite complex when you talk about it. Simply put, the file paths seen by Microsoft Store applications are different from their actual storage locations, so you need to do some translation. The code above is doing this translation work. Like memory and reality, sometimes they don’t overlap, requiring a bit of patience to distinguish.
Practical Experience
Platform Detection
Always check process.platform === 'win32' to avoid executing Windows-specific code on non-Windows platforms. This is a good habit, like checking the weather before going out - so you don’t get caught in the rain and blame the weather.
if (process.platform !== 'win32') { return { availability: 'not-supported' };}Error Handling
Windows API calls may fail and need proper error handling. We’ve fallen into this pit - without comprehensive error handling, users don’t know what happened when they encounter problems. Actually, after writing enough code, you realize error handling isn’t for anything else, it’s just to save yourself trouble.
function normalizeThrownError(error: unknown): { errorCode: string | null; errorMessage: string | null } { if (error instanceof Error) { const errorWithCode = error as Error & { code?: unknown }; return { errorCode: normalizeErrorCode(errorWithCode.code) ?? error.name, errorMessage: error.message, }; } return { errorCode: null, errorMessage: error == null ? null : String(error) };}Async Processing
Most Microsoft Store APIs are asynchronous, use Promise or async/await. When writing async code, remember to handle edge cases like timeouts and cancellations. After all, no one wants to experience the bitterness of waiting.
async function queryStatus(): Promise<RawStoreLicenseState> { try { const result = await storeContext.getAssociatedStoreProductsAsync(productKinds); return buildSupportedStateFromProductQueries(result); } catch (error) { return buildUnavailableState(error); }}Resource Cleanup
Ensure native resources are released when no longer needed. C++ resources won’t be automatically recycled, manual release is a good habit. Like some things, you can only travel light after letting go.
class MicrosoftStoreSubscriptionBroker { private broker: StoreLicensePlatformBroker | null = null;
dispose(): void { this.broker?.dispose(); this.broker = null; }}Timestamp Conversion
Windows uses 1601-01-01 as epoch, needs conversion to Unix timestamp. This detail is easily overlooked, but if not handled correctly, dates will be completely wrong. Time is like that - a small difference makes a big difference.
const WINDOWS_EPOCH_OFFSET_MILLISECONDS = 11644473600000n;const HUNDRED_NANOSECONDS_PER_MILLISECOND = 10000n;
function toIsoDate(value: unknown): string | null { const universalTime = (value as { universalTime?: unknown } | null)?.universalTime; const ticks = typeof universalTime === 'bigint' ? universalTime : null;
if (ticks == null) return null;
const unixMilliseconds = ticks / HUNDRED_NANOSECONDS_PER_MILLISECOND - WINDOWS_EPOCH_OFFSET_MILLISECONDS; return new Date(Number(unixMilliseconds)).toISOString();}Best Practices
Based on our experience in the HagiCode project, here are a few recommendations:
- Prioritize dynwinrt: For WinRT APIs, dynwinrt provides type-safe and modern JavaScript bindings
- Minimize native extensions: Use native extensions only when you truly need high performance or functionality not supported by dynwinrt
- Cross-platform compatibility: Use conditional compilation or runtime detection to handle different platforms
- Test coverage: Thoroughly test native API calls on Windows, including error scenarios
- Documentation: Clearly record the purpose and potential side effects of each native API call
When writing code, keep it simple when possible. If dynwinrt can solve the problem, don’t write C++ extensions. Maintenance costs will be much lower. This is a small insight, nothing profound really.
Summary
Calling Windows native APIs is an important means for Electron applications to implement advanced features on the Windows platform. This article shares several technical solutions used in the HagiCode Desktop project: dynwinrt for WinRT APIs, native Node.js extensions for high-performance scenarios, and virtualized path handling for Store application file access.
Which solution to choose depends on your specific needs. If you’re only calling WinRT APIs, dynwinrt is the simplest choice. If you need high performance or traditional Win32 APIs, native extensions are necessary. For one-time operations, using child_process to call PowerShell also works. All roads lead to Rome, some just easier to walk than others, a bit more winding.
Regardless of which solution you use, remember these principles: do platform detection properly, improve error handling, handle async well, and clean up resources in time. These details determine the robustness of your code. After writing code for a while, you’ll realize that details are often more important than the big framework.
If you’re doing similar development, I hope these experiences help you. Technology is like that - the more pits you fall into, the more experience you gain. Like life, the more you fall, the more you learn how to walk…
References
- Windows.Services.Store namespace - WinRT Documentation
- Node-API ThreadSafeFunction Documentation
- HagiCode Official Website
- HagiCode-org/site GitHub Repository
- Electron Documentation
Summary
Regarding “How Electron Calls Windows Native APIs”, a more prudent approach is to first gradually work through key configurations, dependency boundaries, and implementation paths, then fill in optimization details.
When objectives, steps, and acceptance criteria are all clear, such solutions can typically proceed more smoothly into actual delivery.
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。