Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zig and Node.js N-API example #3000

Closed
jorangreef opened this issue Aug 3, 2019 · 11 comments · Fixed by #10314
Closed

Zig and Node.js N-API example #3000

jorangreef opened this issue Aug 3, 2019 · 11 comments · Fixed by #10314
Labels
use case Describes a real use case that is difficult or impossible, but does not propose a solution.
Milestone

Comments

@jorangreef
Copy link
Sponsor Contributor

jorangreef commented Aug 3, 2019

Given Zig's promise of C header includes, it would be cool for the docs or blog to have an example of writing a "Hello Node.js" native addon for Node.js using N-API and Zig.

Node's N-API is fairly recent, and many existing native addons for Node written using Nan etc. are probably being ported. Now might be a great time for some of these native Node modules to consider using Zig instead of C/C++.

@daurnimator
Copy link
Contributor

It seems like n-api requires that modules register themselves from constructors that run when their dll is opened. If you don't then node's process.dlopen fails with:

Error: Module did not self-register.

This issue will require a way to register a dynamic library constructor.


The code I was playing with:

// https://nodejs.org/api/n-api.html

const std = @import("std");

const napi = @cImport({
    @cInclude("node_api.h");
});

fn NAPI_MODULE_X(modname: []const u8, regfunc: napi.napi_addon_register_func, priv: ?*c_void, flags: c_uint) void {
    napi_module_register(&napi_module {
        .nm_version = napi.NAPI_MODULE_VERSION,
        .nm_flags = flags,
        .nm_filename = null,
        .nm_register_func = regfunc,
        .nm_modname = modname,
        .nm_priv = priv,
        .reserved = c_void(null) ** 4,
    });
}

fn NAPI_MODULE(modname: []const u8, regfunc: napi.napi_addon_register_func) void {
    return NAPI_MODULE_X(modname, regfunc, null, 0);
}

extern fn myfunc(env: napi.napi_env, exports: napi.napi_value) napi.napi_value {
    var answer: napi.napi_value = undefined;
    var status: napi.napi_status = undefined;

    status = napi.napi_create_int64(env, 42, &answer);
    if (status != .napi_ok) return null;

    status = napi.napi_set_named_property(env, exports, &"answer", answer);
    if (status != .napi_ok) return null;

    return exports;
}
comptime {
    @export("napi_register_module_v1", myfunc, .Strong);
}
zig build-lib -dynamic --library c -isystem /usr/include/node/ n-api.zig

@andrewrk andrewrk added this to the 0.6.0 milestone Aug 11, 2019
@ceymard
Copy link

ceymard commented Sep 21, 2019

I have managed to do exactly that thanks to the help of the nice zig people in freenode by doing that on linux:

export const init_array linksection(".init_array") = [_]extern fn () void {
  myFunc
};

@daurnimator
Copy link
Contributor

@andrewrk this issue is a good example use case for why we need the ability to support constructors. The workaround found is linux and elf specific.

@andrewrk andrewrk reopened this Feb 11, 2020
@andrewrk andrewrk modified the milestones: 0.6.0, 0.7.0 Feb 11, 2020
@andrewrk andrewrk modified the milestones: 0.7.0, 0.8.0 Oct 30, 2020
@SpexGuy SpexGuy added the use case Describes a real use case that is difficult or impossible, but does not propose a solution. label Mar 24, 2021
@ifreund
Copy link
Member

ifreund commented Apr 16, 2021

Since you need to link C code anyways for this use case, I think calling your constructor from C is a perfectly fine solution and avoids adding any language features to support this use case. The C file can be as simple as

#include <node_api.h>
napi_value init(napi_env env, napi_value exports);
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

Example repo here: https://github.com/ifreund/zig-napi-example

@ifreund
Copy link
Member

ifreund commented Apr 16, 2021

It turns out I wasn't actually satisfied with that solution. I'm not sure if this has changed since @daurnimator's initial investigation, but node no longer requires the constructor to be run if the module exports a function with the proper name:
https://github.com/nodejs/node/blob/6cb314bbe5ee5c9af3c111516fb7f7689c10ecda/src/node_binding.cc#L478

For example:

const c = @cImport({
    @cInclude("node_api.h");
});

export fn napi_register_module_v1(env: c.napi_env, exports: c.napi_value) c.napi_value {
    var function: c.napi_value = undefined;
    if (c.napi_create_function(env, null, 0, foo, null, &function) != .napi_ok) {
        _ = c.napi_throw_error(env, null, "Failed to create function");
        return null;
    }

    if (c.napi_set_named_property(env, exports, "foo", function) != .napi_ok) {
        _ = c.napi_throw_error(env, null, "Failed to add function to exports");
        return null;
    }

    return exports;
}

fn foo(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
    var result: c.napi_value = undefined;
    if (c.napi_create_int32(env, 42, &result) != .napi_ok) {
        _ = c.napi_throw_error(env, null, "Failed to create return value");
        return null;
    }

    return result;
}
const example = require('./example.node');
console.log(example.foo());

build with

zig build-lib -dynamic -lc -isystem /usr/include/node example.zig -femit-bin=example.node

@daurnimator
Copy link
Contributor

I'm not sure if this has changed since @daurnimator's initial investigation,

Looks like it might have, e.g. nodejs/node#26175

@andrewrk andrewrk modified the milestones: 0.8.0, 0.9.0 Jun 4, 2021
@scheibo
Copy link
Sponsor Contributor

scheibo commented Nov 10, 2021

Hi @ifreund - thanks for the example! I ran into some problems trying to get it to run zig 0.9.0-dev.1583+a7d215759:

  • it seems like the compiler complains about info being unused in foo, so I changed it to _
  • the compiler complains about .napi_ok so I just replaced them with 0 (I first tried @enumToInt(...) but that didn't seem to work)
./example.zig:7:68: error: incompatible types: 'c_uint' and '(enum literal)'
    if (c.napi_create_function(env, null, 0, foo, null, &function) != .napi_ok) {

I then ended up with the following code:

const c = @cImport({
    @cInclude("node_api.h");
});

export fn napi_register_module_v1(env: c.napi_env, exports: c.napi_value) c.napi_value {
    var function: c.napi_value = undefined;
    if (c.napi_create_function(env, null, 0, foo, null, &function) != 0) {
        _ = c.napi_throw_error(env, null, "Failed to create function");
        return null;
    }

    if (c.napi_set_named_property(env, exports, "foo", function) != 0) {
        _ = c.napi_throw_error(env, null, "Failed to add function to exports");
        return null;
    }

    return exports;
}

fn foo(env: c.napi_env, _: c.napi_callback_info) callconv(.C) c.napi_value {
    var result: c.napi_value = undefined;
    if (c.napi_create_int32(env, 42, &result) != 0) {
        _ = c.napi_throw_error(env, null, "Failed to create return value");
        return null;
    }

    return result;
}

I tried running this on Debian Linux and it seems to work as expected (nice!):

$ zig build-lib -dynamic -lc -isystem $INCLUDE example.zig -femit-bin=example.node
$ node example.js
42

However, I was not successful in getting this to run on macOS 12.0.1:

$ zig build-lib -dynamic -lc -isystem $INCLUDE example.zig -femit-bin=example.node
error(link): undefined reference to symbol '_napi_throw_error'
error(link):   first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error(link): undefined reference to symbol '_napi_set_named_property'
error(link):   first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error(link): undefined reference to symbol '_napi_create_int32'
error(link):   first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error(link): undefined reference to symbol '_napi_create_function'
error(link):   first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error: UndefinedSymbolReference

https://github.com/kristoff-it/zig-cuckoofilter/blob/master/c-abi-examples/node_example.js#L13-L17 seems to imply macOS might need a different approach, but I tried doing a build-obj and running gcc as described there and couldn't get anything workable. Is this a known issue? Is it possible you could update your example with the latest and greatest commands for macOS as well? Thanks!

@daurnimator
Copy link
Contributor

daurnimator commented Nov 10, 2021

$ zig build-lib -dynamic -lc -isystem $INCLUDE example.zig -femit-bin=example.node
error(link): undefined reference to symbol '_napi_throw_error'
error(link): first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error(link): undefined reference to symbol '_napi_set_named_property'
error(link): first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error(link): undefined reference to symbol '_napi_create_int32'
error(link): first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error(link): undefined reference to symbol '_napi_create_function'
error(link): first referenced in '/tmp/zig-cache/o/910087189ff92cf8ebf244f7e7a8e01b/example.o'
error: UndefinedSymbolReference

This is the same error you'd traditionally get for C if you forgot to specify --allow-shlib-undefined for linux; or more specifically for OSX: -undefined dynamic_lookup.
Previous issues #3085 (comment) and #8016
I think this is covered by #8180

@scheibo
Copy link
Sponsor Contributor

scheibo commented Nov 10, 2021

Thanks! I tried the same command with -target native-macos and then with -fallow-shlib-undefined. In both cases the zig build-lib fails with the same errors as without using those workaround flags. I guess the answer is to wait on #8180 (which looks very relevant) to be closed before it is possible to create a Node extension from Zig on macOS (this is perhaps what you were explaining above and I am just slow to get there)?

@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 24, 2021
kubkon added a commit that referenced this issue Dec 11, 2021
We now respect both `-fallow-shlib-undefined` and
`-Wl,"-undefined=dynamic_lookup"` flags. This is the first step
towards solving issues #8180 and #3000. We currently do not expose
any other ld64 equivalent flag for `-undefined` flag - we basically
throw an error should the user specify a different flag. Support for
those is conditional on closing #8180. As a result of this change,
it is now possible to generate a valid native Node.js addon with Zig
for macOS.
@andrewrk andrewrk modified the milestones: 0.10.0, 0.9.0 Dec 11, 2021
kubkon added a commit that referenced this issue Dec 11, 2021
We now respect both `-fallow-shlib-undefined` and
`-Wl,"-undefined=dynamic_lookup"` flags. This is the first step
towards solving issues #8180 and #3000. We currently do not expose
any other ld64 equivalent flag for `-undefined` flag - we basically
throw an error should the user specify a different flag. Support for
those is conditional on closing #8180. As a result of this change,
it is now possible to generate a valid native Node.js addon with Zig
for macOS.
@wmertens
Copy link

BTW, this repo implements a module with self-registration: https://github.com/staltz/zig-nodejs-example/blob/main/src/lib.zig#L6-L9

@osztenkurden
Copy link

Hey, it seems its not fixed on Windows, trying to compile with zig build-lib -dynamic -lc -isystem deps/node-v20.9.0/include/node/ src/lib.zig -femit-bin=dist/lib.node -fallow-shlib-undefined -Dtarget=x86_64-windows gives

warning(link): unexpected LLD stderr:
lld-link: warning: undefined symbol: napi_create_function
>>> referenced by F:\Repositories\zig\zig-nodejs-example\src\translate.zig:16
>>>               dist\lib.node.obj:(translate.register_function__anon_3078)

lld-link: warning: undefined symbol: napi_set_named_property
>>> referenced by F:\Repositories\zig\zig-nodejs-example\src\translate.zig:20
>>>               dist\lib.node.obj:(translate.register_function__anon_3078)

lld-link: warning: undefined symbol: napi_throw_error
>>> referenced by F:\Repositories\zig\zig-nodejs-example\src\translate.zig:27
>>>               dist\lib.node.obj:(translate.throw__anon_3080)
>>> referenced by F:\Repositories\zig\zig-nodejs-example\src\translate.zig:27
>>>               dist\lib.node.obj:(translate.throw__anon_3081)
>>> referenced by F:\Repositories\zig\zig-nodejs-example\src\translate.zig:27
>>>               dist\lib.node.obj:(translate.throw__anon_3082)

lld-link: warning: undefined symbol: napi_create_string_utf8
>>> referenced by F:\Repositories\zig\zig-nodejs-example\src\translate.zig:358
>>>               dist\lib.node.obj:(translate.create_string)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
use case Describes a real use case that is difficult or impossible, but does not propose a solution.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants