libwin is a native Node.js addon that provides bindings to the Windows API. This allows developers to interact with Windows just like they would when developing in C++, be it displaying a simple message box from a CLI script, or going all out and creating an entire GUI application.
This is a very ambitious project that is still very much in the early stages of development. I started this project in an attempt to learn more about the Windows API but also, to become more familiar with C++ and native software development. As a web developer, I obviously don’t have much experience with these technologies, so this is very much a learning exercise for me. That said, I do plan on making this a production ready library that can be used by anyone.
How It Works
The project is built using C++20 and leverages the Node API directly to ensure maximum compatibility across different Node.js versions and the highest performance possible. Originally I planned on using Koffi, the same FFI I used in AutoIt JS, but the niceties and conveniences that Koffi provides come at the cost of increased complexity when trying to map a low-level API like the Windows API. For example, when returning a string pointer from C++, Koffi will automatically convert that to a JS string, reading all the way until it hits a null terminator. This is fantastic for most use cases, but there are cases where you just want the raw pointer but you simply just can’t do that, at least not easily. The Node API on the other hand gives me full control over how data is marshaled between C++ and JS, so I can handle these edge cases as they arise.
To aid in creating this bindings, I created a header-only library called QuickBind that provides a whole myriad of functions and macros to make binding C++ functions to JS easier and less error prone. The general layout of QuickBind looks something like this:
// Create a function that returns a certain type wrapped in std::optional
[[nodiscard]] std::optional<uint32_t> inline ReadUint32(const Napi::Value &value,
const qb::detail::Location &location,
const bool required) {
// Check if the value is null or undefined and throw if it's required but missing
QB_CHECK_NULLISH(value, required, qb::detail::EXPECTED_NUMBER, location);
// Check if the value matches the expected type
if (!value.IsNumber()) {
// If not, throw a type error
qb::detail::ThrowTypeError(value.Env(), qb::detail::EXPECTED_NUMBER, location);
// And return an empty optional
return std::nullopt;
}
// Convert the Napi::Value to the desired type
const uint32_t uint32Value = value.As<Napi::Number>().Uint32Value();
// Return the converted value wrapped in std::optional
return uint32Value;
};From there, we create public helper functions that can be used without having to pass a ton of arguments every time:
// Read a required value from the function arguments
[[nodiscard]] inline uint32_t ReadRequiredUint32(const Napi::CallbackInfo &info,
const uint16_t index) {
return qb::detail::ReadUint32(info[index], qb::detail::Argument(index), true).value_or(0);
};
// Read a required value from an object property
[[nodiscard]] inline uint32_t ReadRequiredUint32(const Napi::Object &object,
const std::string &key) {
return qb::detail::ReadUint32(object.Get(key), qb::detail::Property(key), true).value_or(0);
};
// Read an optional value from the function arguments
[[nodiscard]] inline std::optional<uint32_t> ReadOptionalUint32(const Napi::CallbackInfo &info,
const uint16_t index) {
return qb::detail::ReadUint32(info[index], qb::detail::Argument(index), false);
};
// Read an optional value from an object property
[[nodiscard]] inline std::optional<uint32_t> ReadOptionalUint32(const Napi::Object &object,
const std::string &key) {
return qb::detail::ReadUint32(object.Get(key), qb::detail::Property(key), false);
};Finally, we have a macro that simplifies error checking after each read operation; the auto keyword really helps me out here:
#define QB_ARG(variable, expression)
auto variable = expression;
if (info.Env().IsExceptionPending()) {
return info.Env().Undefined();
}With these in place, creating the bindings becomes a lot simpler:
Napi::Value User32::MessageBoxW(const Napi::CallbackInfo &info) {
const QB_ARG(hWnd, qb::ReadOptionalHandle<HWND>(info, 0));
const QB_ARG(lpText, qb::ReadOptionalWideString(info, 1));
const QB_ARG(lpCaption, qb::ReadOptionalWideString(info, 2));
const QB_ARG(uType, qb::ReadRequiredUint32(info, 3));
const int result = ::MessageBoxW(hWnd ? hWnd.value() : nullptr,
lpText ? lpText->c_str() : nullptr,
lpCaption ? lpCaption->c_str() : nullptr,
uType);
return Napi::Number::New(info.Env(), result);
}Duplicate the above pattern a few thousand times, and you have yourself a Windows API binding library! Obviously the project is nowhere near complete yet but with the foundation laid out, I can now start adding things more rapidly.
Use Cases
To be completely honest, I don’t have a clear vision of what this library will be used for yet. While you could use it to create Windows applications in JavaScript and Node.js, it’s not exactly the most practical thing in the world. One use case I could potentially see is for frameworks like Electron to use libwin to provide deeper Windows integration for their applications. Another possibility is creating a sort of driver library for creating an all new automation framework for desktop applications. But for now, the main goal is to simply learn and explore the Windows API and C++ development.