Start Debugging

Node.js Addons на C#: .NET Native AOT заменяет C++ и node-gyp

Команда C# Dev Kit сменила свой C++ Node.js addon на библиотеку .NET 10 Native AOT, используя N-API, UnmanagedCallersOnly и LibraryImport для производства единого файла .node без Python и node-gyp.

Drew Noakes из команды C# Dev Kit анонсировал 20 апреля 2026, что нативный Node.js addon расширения теперь полностью написан на C# и скомпилирован .NET 10 Native AOT. Это значит, что доступ к Windows Registry, от которого зависит расширение, поставляется как обычный .node файл, произведённый dotnet publish, без C++, без Python, и без node-gyp в build-цепочке.

Почему это большое событие для Node tooling

Node.js addons исторически были C или C++ проектами, склеенными node-gyp, который в свою очередь требует Python, C++ toolchain и совместимый MSBuild на Windows. Любой, кто поддерживал кроссплатформенное Electron-расширение, знает, насколько хрупкой становится эта цепочка в CI. Native AOT сжимает весь пайплайн в единственный dotnet publish, производя специфичную для платформы shared library (.dll, .so, или .dylib), которую Node грузит напрямую, как только вы переименуете её в .node. C# Dev Kit использует ровно этот поток, чтобы читать Windows Registry, убирая Python из contributor setup.

Экспорт napi_register_module_v1 из C#

Трюк в том, что у N-API (Node-API) стабильный ABI, так что любой язык, который может произвести нативный export с C calling conventions, может реализовать Node addon. В .NET 10 [UnmanagedCallersOnly] делает эту работу: фиксирует имя export и calling convention в AOT-образ. Entry point, который ищет Node, это napi_register_module_v1.

public static unsafe partial class HelloAddon
{
    [UnmanagedCallersOnly(
        EntryPoint = "napi_register_module_v1",
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Init(nint env, nint exports)
    {
        RegisterFunction(env, exports, "hello"u8, &SayHello);
        return exports;
    }

    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
    private static nint SayHello(nint env, nint info)
    {
        return CreateString(env, "Hello from .NET!");
    }
}

Литерал "hello"u8 - это UTF-8 byte string, именно то, что хочет N-API, а &SayHello - это function pointer, переживающий AOT, потому что UnmanagedCallersOnly запрещает managed-only фичи вроде generics и async на этой сигнатуре.

Разрешение N-API против host process

Вторая половина головоломки - вызов обратно в N-API. Нет node.dll, против которой линковаться, потому что на многих платформах бинарник Node - это сам executable. Пост использует [LibraryImport("node")] вместе с кастомным NativeLibrary.SetDllImportResolver, возвращающим handle текущего процесса, так что каждый N-API вызов разрешается против запущенного Node executable во время загрузки.

[LibraryImport("node", EntryPoint = "napi_create_string_utf8")]
private static partial int CreateStringUtf8(
    nint env, byte[] str, nuint length, out nint result);

NativeLibrary.SetDllImportResolver(typeof(HelloAddon).Assembly,
    (name, _, _) => name == "node"
        ? NativeLibrary.GetMainProgramHandle()
        : 0);

Файл проекта

Включить AOT - изменение в две строки. AllowUnsafeBlocks требуется, потому что N-API interop опирается на function pointers и spans над нативной памятью.

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <PublishAot>true</PublishAot>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

После dotnet publish -c Release переименуйте выходную библиотеку в HelloAddon.node и require() её из JavaScript как любой другой нативный модуль.

Для более богатых сценариев пост также указывает на microsoft/node-api-dotnet, оборачивающий N-API в абстракции более высокого уровня и поддерживающий полный interop между JS и CLR типами. Но для случая “поставить небольшой быстрый нативный addon без C++ toolchain”, путь сырого N-API плюс Native AOT теперь production-проверен внутри собственных VS Code расширений Microsoft.

< Назад