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

Implement Node-API in bun.js (napi) #158

Open
Jarred-Sumner opened this issue May 5, 2022 · 46 comments
Open

Implement Node-API in bun.js (napi) #158

Jarred-Sumner opened this issue May 5, 2022 · 46 comments
Assignees
Labels
tracking An umbrella issue for tracking big features

Comments

@Jarred-Sumner
Copy link
Collaborator

Jarred-Sumner commented May 5, 2022

Node-API is Node.js' native addon API. The goal of this project is for many Node-API addons in bun.js to just work, without asking maintainers of node modules to make changes or rebuild their code specifically for Bun.

The first version will be scoped specifically to the napi_* functions. It will not include support for uv_* functions or the V8 C++ functions. A good initial target is napi.rs support.

  • async_hooks resource tracking will not be supported, it will just ignore them.
  • async cleanup will be stubbed

Internal changes

  • Support resolving .node files
  • process.dlopen support for native modules
  • require support for native modules

API implementation

  • napi_acquire_threadsafe_function
  • napi_add_async_cleanup_hook
  • napi_add_env_cleanup_hook
  • napi_add_finalizer
  • napi_adjust_external_memory (JSC doesn't have an exact version of this. it won't go down)
  • napi_async_destroy
  • napi_async_init
  • napi_call_function
  • napi_call_threadsafe_function
  • napi_cancel_async_work
  • napi_check_object_type_tag
  • napi_close_callback_scope
  • napi_close_escapable_handle_scope
  • napi_close_handle_scope
  • napi_coerce_to_bool
  • napi_coerce_to_number
  • napi_coerce_to_object
  • napi_coerce_to_string
  • napi_create_array
  • napi_create_array_with_length
  • napi_create_arraybuffer
  • napi_create_async_work
  • napi_create_bigint_int64
  • napi_create_bigint_uint64
  • napi_create_bigint_words
  • napi_create_buffer
  • napi_create_buffer_copy
  • napi_create_dataview
  • napi_create_date
  • napi_create_double
  • napi_create_error
  • napi_create_external
  • napi_create_external_arraybuffer
  • napi_create_external_buffer
  • napi_create_function
  • napi_create_int32
  • napi_create_int64
  • napi_create_object
  • napi_create_promise
  • napi_create_range_error
  • napi_create_reference
  • napi_create_string_latin1
  • napi_create_string_utf16
  • napi_create_string_utf8
  • napi_create_symbol
  • napi_create_threadsafe_function
  • napi_create_type_error
  • napi_create_typedarray
  • napi_create_uint32
  • napi_define_class
  • napi_define_properties
  • napi_delete_async_work
  • napi_delete_element
  • napi_delete_property
  • napi_delete_reference
  • napi_detach_arraybuffer
  • napi_escape_handle
  • napi_fatal_error (works but message formatting is low quality)
  • napi_fatal_exception (works but message formatting is low quality)
  • napi_get_all_property_names
  • napi_get_and_clear_last_exception (this uses JSC::VM::lastException())
  • napi_get_array_length
  • napi_get_arraybuffer_info
  • napi_get_boolean
  • napi_get_buffer_info
  • napi_get_cb_info
  • napi_get_dataview_info
  • napi_get_date_value
  • napi_get_element
  • napi_get_global
  • napi_get_instance_data
  • napi_get_last_error_info (stubbed, but with an error message)
  • napi_get_named_property
  • napi_get_new_target
  • napi_get_node_version
  • napi_get_null
  • napi_get_property
  • napi_get_property_names
  • napi_get_prototype
  • napi_get_reference_value
  • napi_get_threadsafe_function_context
  • napi_get_typedarray_info
  • napi_get_undefined
  • napi_get_uv_event_loop
  • napi_get_value_bigint_int64
  • napi_get_value_bigint_uint64
  • napi_get_value_bigint_words
  • napi_get_value_bool
  • napi_get_value_double
  • napi_get_value_external
  • napi_get_value_int32
  • napi_get_value_int64
  • napi_get_value_string_latin1
  • napi_get_value_string_utf16
  • napi_get_value_string_utf8
  • napi_get_value_uint32
  • napi_get_version
  • napi_has_element
  • napi_has_named_property
  • napi_has_own_property
  • napi_has_property
  • napi_instanceof
  • napi_is_array
  • napi_is_arraybuffer
  • napi_is_buffer
  • napi_is_dataview
  • napi_is_date
  • napi_is_detached_arraybuffer
  • napi_is_error
  • napi_is_exception_pending
  • napi_is_promise
  • napi_is_typedarray
  • napi_make_callback
  • napi_module_register
  • napi_new_instance
  • napi_object_freeze
  • napi_object_seal
  • napi_open_callback_scope
  • napi_open_escapable_handle_scope
  • napi_open_handle_scope
  • napi_queue_async_work
  • napi_ref_threadsafe_function
  • napi_reference_ref
  • napi_reference_unref
  • napi_reject_deferred
  • napi_release_threadsafe_function
  • napi_remove_async_cleanup_hook
  • napi_remove_env_cleanup_hook
  • napi_remove_wrap
  • napi_resolve_deferred
  • napi_run_script
  • napi_set_element
  • napi_set_instance_data
  • napi_set_named_property
  • napi_set_property
  • napi_strict_equals
  • napi_throw
  • napi_throw_error
  • napi_throw_range_error
  • napi_throw_type_error
  • napi_type_tag_object
  • napi_typeof
  • napi_unref_threadsafe_function
  • napi_unwrap
  • napi_wrap

Test coverage

The tentative plan is to rely on napi.rs tests, but haven't checked yet if that's possible.

  • napi_acquire_threadsafe_function
  • napi_add_async_cleanup_hook
  • napi_add_env_cleanup_hook
  • napi_add_finalizer
  • napi_adjust_external_memory
  • napi_async_destroy
  • napi_async_init
  • napi_call_function
  • napi_call_threadsafe_function
  • napi_cancel_async_work
  • napi_check_object_type_tag
  • napi_close_callback_scope
  • napi_close_escapable_handle_scope
  • napi_close_handle_scope
  • napi_coerce_to_bool
  • napi_coerce_to_number
  • napi_coerce_to_object
  • napi_coerce_to_string
  • napi_create_array
  • napi_create_array_with_length
  • napi_create_arraybuffer
  • napi_create_async_work
  • napi_create_bigint_int64
  • napi_create_bigint_uint64
  • napi_create_bigint_words
  • napi_create_buffer
  • napi_create_buffer_copy
  • napi_create_dataview
  • napi_create_date
  • napi_create_double
  • napi_create_error
  • napi_create_external
  • napi_create_external_arraybuffer
  • napi_create_external_buffer
  • napi_create_function
  • napi_create_int32
  • napi_create_int64
  • napi_create_object
  • napi_create_promise
  • napi_create_range_error
  • napi_create_reference
  • napi_create_string_latin1
  • napi_create_string_utf16
  • napi_create_string_utf8
  • napi_create_symbol
  • napi_create_threadsafe_function
  • napi_create_type_error
  • napi_create_typedarray
  • napi_create_uint32
  • napi_define_class
  • napi_define_properties
  • napi_delete_async_work
  • napi_delete_element
  • napi_delete_property
  • napi_delete_reference
  • napi_detach_arraybuffer
  • napi_escape_handle
  • napi_fatal_error
  • napi_fatal_exception
  • napi_get_all_property_names
  • napi_get_and_clear_last_exception
  • napi_get_array_length
  • napi_get_arraybuffer_info
  • napi_get_boolean
  • napi_get_buffer_info
  • napi_get_cb_info
  • napi_get_dataview_info
  • napi_get_date_value
  • napi_get_element
  • napi_get_global
  • napi_get_instance_data
  • napi_get_last_error_info
  • napi_get_named_property
  • napi_get_new_target
  • napi_get_node_version
  • napi_get_null
  • napi_get_property
  • napi_get_property_names
  • napi_get_prototype
  • napi_get_reference_value
  • napi_get_threadsafe_function_context
  • napi_get_typedarray_info
  • napi_get_undefined
  • napi_get_uv_event_loop
  • napi_get_value_bigint_int64
  • napi_get_value_bigint_uint64
  • napi_get_value_bigint_words
  • napi_get_value_bool
  • napi_get_value_double
  • napi_get_value_external
  • napi_get_value_int32
  • napi_get_value_int64
  • napi_get_value_string_latin1
  • napi_get_value_string_utf16
  • napi_get_value_string_utf8
  • napi_get_value_uint32
  • napi_get_version
  • napi_has_element
  • napi_has_named_property
  • napi_has_own_property
  • napi_has_property
  • napi_instanceof
  • napi_is_array
  • napi_is_arraybuffer
  • napi_is_buffer
  • napi_is_dataview
  • napi_is_date
  • napi_is_detached_arraybuffer
  • napi_is_error
  • napi_is_exception_pending
  • napi_is_promise
  • napi_is_typedarray
  • napi_make_callback
  • napi_module_register
  • napi_new_instance
  • napi_object_freeze
  • napi_object_seal
  • napi_open_callback_scope
  • napi_open_escapable_handle_scope
  • napi_open_handle_scope
  • napi_queue_async_work
  • napi_ref_threadsafe_function
  • napi_reference_ref
  • napi_reference_unref
  • napi_reject_deferred
  • napi_release_threadsafe_function
  • napi_remove_async_cleanup_hook
  • napi_remove_env_cleanup_hook
  • napi_remove_wrap
  • napi_resolve_deferred
  • napi_run_script
  • napi_set_element
  • napi_set_instance_data
  • napi_set_named_property
  • napi_set_property
  • napi_strict_equals
  • napi_throw
  • napi_throw_error
  • napi_throw_range_error
  • napi_throw_type_error
  • napi_type_tag_object
  • napi_typeof
  • napi_unref_threadsafe_function
  • napi_unwrap
  • napi_wrap
@Jarred-Sumner Jarred-Sumner added the tracking An umbrella issue for tracking big features label May 5, 2022
@kriszyp
Copy link

kriszyp commented May 14, 2022

I made a test repo that exercises a number of things that are currently having problems with the bun napi interface:
https://github.com/kriszyp/bun-test-apw
Clone that, npm install it, and then you can compare node index.js to bun index.js:

  • Occasional segfaults in calls through napi_call_threadsafe_function
  • napi_release_threadsafe_function not triggering thread_finalize_cb
  • NAPI_MODULE macro fails with symbol 'napi_register_module_v1' not found in native module (easy workaround, since NAPI_MODULE_INIT works fine)
  • If you get through those, I think the Callback().Call() ends up triggering escapable scopes, but that's not that important.

You will have uncomment NAPI_MODULE from apw.cpp (and comment NODE_MODULE_INIT) when you want to test that, figured the other things were more important.

Hope that helps, thanks for the great work!

@kriszyp
Copy link

kriszyp commented May 15, 2022

I also wanted to check to see if this is roughly the right approach loading a module for napi + ffi usage. I think the hybrid strategy is good: use napi for general purpose stuff like building objects, async work, and use ffi for "hot" functions that need to be fast. Here is an example from my code where I am loading the platform's binary with the napi loading package, and then, if running in bun, loading it through the FFI loader. But, I think this raises a couple points:

Anyway, here is my loading sequence right now:

import { dirname, join, default as pathModule } from 'path';
import { fileURLToPath } from 'url';
import loadNAPI from 'node-gyp-build'; // loads the correct napi binary, using the current OS/arch

let dirName = (typeof __dirname == 'string' ? __dirname : // for bun, which doesn't have fileURLToPath
	dirname(fileURLToPath(import.meta.url))).replace(/dist$/, ''); // for node, which doesn't have __dirname in ESM

export let nativeAddon = loadNAPI(dirName);
if (process.isBun) {
	const { dlopen, FFIType } = require('bun:ffi');
	// load the same napi module as above, but use bun's ffi
	// ideally we should use require.resolve to find a package, but here just hoping it is a sibling package in node_modules
	let libPath = join(dirName, '..', 'lmdb-linux-' + process.arch, 'node.napi.node');
	let lmdbLib = dlopen(libPath, {
		getByBinary: { args: [FFIType.f64, FFIType.u32], returns: FFIType.u32},
		iterate: { args: [FFIType.f64], returns: FFIType.i32},
		position: { args: [FFIType.f64, FFIType.u32, FFIType.u32, FFIType.u32, FFIType.f64], returns: FFIType.i32},
		write: { args: [FFIType.f64, FFIType.f64], returns: FFIType.i32},
		resetTxn: { args: [FFIType.f64], returns: FFIType.u8},
	});
	Object.assign(nativeAddon, lmdbLib.symbols);
}

@Jarred-Sumner
Copy link
Collaborator Author

Jarred-Sumner commented May 15, 2022

let dirName = (typeof __dirname == 'string' ? __dirname : // for bun, which doesn't have fileURLToPath

For bun you can also do import.meta.dir and/or import.meta.path, which at least saves you from the typeof check

I assume calling dlopen twice (once through the napi loader, once through the ffi loader) is cached at some level of the OS and doesn't result in duplicate binary footprints in memory

This is a good point. I don't know either, but worth checking.

require.resolve doesn't seem to exist in bun, and this seems challenging for loading a platform-specific package with a binary, which is direction that we are going with node-gyp-build: https://github.com/prebuild/node-gyp-build/pull/45/files#diff-e727e4bdf3657fd1d798edcd6b099d6e092f8573cba266154583a746bba0f346R50

This is an oversight – require.resolve should exist but does not currently. There are at least 5 other ways you can resolve without loading modules though:

  • Bun.resolve(path, fromPath) (async)
  • Bun.resolveSync(path, fromPath)
  • import.meta.resolve(path) (async)
  • import.meta.resolveSync(path) (sync)
  • Loader.resolveSync (lower-level, uses JSC builtin ModuleLoader and may be disabled at some point)

@kriszyp
Copy link

kriszyp commented May 15, 2022

assume calling dlopen twice (once through the napi loader

My evidence is that when I set the value of a static C variable from a NAPI call, that value was accessible/correct from an FFI call, so I think working correctly, but I am not claiming this as absolute proof.

This is an oversight – require.resolve should exist but does not currently

Ok, so you plan on adding require.resolve? I think in node-gyp-build it would either need to go through createRequire(libraryPath), or use the two arg version (require.resolve(id, { path: [ libraryPath ] }). node-gyp-build is CJS module, so unfortunately import.meta.resolve can't be used there (and can't even be mentioned, if (false) import.meta.resolve('test') will even fail in Node in CJS mode).

Anyway, thanks for the progress here, I know these are probably annoying details of the napi/node machinery, but I think node-gyp-build is pretty commonly used.

@Jarred-Sumner
Copy link
Collaborator Author

Jarred-Sumner commented May 17, 2022

Ok, so you plan on adding require.resolve? I think in node-gyp-build it would either need to go through createRequire(libraryPath), or use the two arg version (require.resolve(id, { path: [ libraryPath ] }). node-gyp-build is CJS module, so unfortunately import.meta.resolve can't be used there (and can't even be mentioned, if (false) import.meta.resolve('test') will even fail in Node in CJS mode).

Support for both of these was added in Bun v0.0.83 (released today)

@devongovett
Copy link

FYI, I tried to run parcel-css with Bun, but got the following error (logging with DYLD_PRINT_APIS). Not sure if this is useful. I tried to debug with lldb but wasn't able to due to macOS code signing. I guess I'd have to compile bun myself to debug further.

dyld[50777]: dlopen("/Users/devongovett/dev/parcel-css/parcel-css.darwin-arm64.node", 0x00000001)
dyld[50777]:       dlopen(parcel-css.darwin-arm64.node) => 0x2089957e0
dyld[50777]: dlsym(0x2089957e0, "napi_register_module_v1")
dyld[50777]:      dlsym("napi_register_module_v1") => 0x110ab41b0
dyld[50777]: dlsym(0xfffffffffffffffe, "getentropy")
dyld[50777]:      dlsym("getentropy") => 0x183fd8f5c

Example:

const parcelCss  = require('./parcel-css.darwin-arm64.node');
parcelCss.transform({
  filename: 'test.css',
  code: Buffer.from('.foo { color: red }'),
  minify: true
});

@kjvalencik
Copy link
Contributor

kjvalencik commented Jul 7, 2022

If I attempt to load a native module that uses Node-API, I get a crash on missing symbols. Additionally, I don't see any of the Node-API symbols in the bun binary (0.1.1, macOS x86_64).

Is there something additional that needs to be done to load these symbols or was this possibly optimized out in the latest release?

$ symbols $(which bun) | grep napi

Edit: It appears to be only the macOS release where they are missing. The symbols are present in linux.

@Pruxis
Copy link
Contributor

Pruxis commented Jul 10, 2022

What do you mean by async_hooks resource tracking will not be supported, it will just ignore them.

As bluebird uses async_hooks and requires it explicitly like this for example:

var AsyncResource = util.isNode && util.nodeSupportsAsyncResource ?
    require("async_hooks").AsyncResource : null;

Which then results into:

Could not resolve: "async_hooks". Maybe you need to "bun install"?

@Jarred-Sumner
Copy link
Collaborator Author

Jarred-Sumner commented Jul 10, 2022 via email

@droukd
Copy link

droukd commented Jul 24, 2022

Also in bun at least, you should try to avoid Bluebird. It’s a many X slowdown

What would you say is recommended to use (instead)? Par example,

(async () => { for (let i=0; i<800000; i++) { await functionCall(i); } })();

...would work, but only one execution at a time.
(And without limitation program will produce errors.)
(Promise.map([...Array(800000).keys()], functionCall, {concurrency: 8});)

P. S. http://bluebirdjs.com/docs/benchmarks.html claims to be faster (in node) than async-await.
P. S. If I am not mistaken, another problem rises with big arrays: bun took 7 GB (and counting) of RAM for 800 000 keys. Speaking of slowdowns.

@artokun
Copy link

artokun commented Jul 27, 2022

PrismaJS also requires async_hook

@harryrabin
Copy link

Hey folks! Is there any progress on napi_create_external?

@Jarred-Sumner
Copy link
Collaborator Author

Hey folks! Is there any progress on napi_create_external?

Added in dddbce8

@dariocravero
Copy link

When running @parcel/watcher, even just trying to import it with import watcher from '@parcel/watcher' bun fails with Segmentation fault: 11 (that's all it outputs). Since that module is written in C++ I figured it might be NAPI-related, might be related to this issue?

@Jarred-Sumner
Copy link
Collaborator Author

Jarred-Sumner commented Jan 23, 2023

When running @parcel/watcher, even just trying to import it with import watcher from '@parcel/watcher' bun fails with Segmentation fault: 11 (that's all it outputs). Since that module is written in C++ I figured it might be NAPI-related, might be related to this issue?

Yes, there are at least three issues blocking @parcel/watcher from working, of which I just fixed two:

  • napi_create_symbol wasn't handling a missing description string - aa45680
  • napi_define_property didn't support symbol properties - 4570ff7
  • napi_type_tag_object is not implemented yet

@cztomsik
Copy link

Looks like napi_set_instance_data() might be supported (I have seen some cpp impl in the repo) but it's probably not exported. I get this on my macos. Here's repro if anyone wants to have a look.

https://github.com/cztomsik/zig-napigen/tree/main/example

image

@Jarred-Sumner
Copy link
Collaborator Author

@cztomsik Yep, that was a mistake. Please try again once the canary build finishes (bun upgrade --canary)

229f5f7

@cztomsik
Copy link

@Jarred-Sumner Either I did something wrong or did not help :-/

image

@Jarred-Sumner
Copy link
Collaborator Author

Sorry, please try again after 4be3548

This time it should work
image

@jackyzha0
Copy link

Support resolving .node files doesn't seem to work with the --compile flag, bun will report a successful build but no binary will be produced

@TomieAi
Copy link

TomieAi commented Feb 22, 2024

i got the error at uWS.js also

TypeError: symbol 'napi_register_module_v1' not found in native module. Is this a Node API (napi) module?

@kjvalencik
Copy link
Contributor

@TomieAi uWebSockets.js isn't a Node-API addon.

@TomieAi
Copy link

TomieAi commented Feb 22, 2024

@TomieAi uWebSockets.js isn't a Node-API addon.

hmm i know but i just wanna say uws dint work at bun sadly. throwing me error about "napi_register_module_v1" then i google about it XD and it leads me here.

@kjvalencik
Copy link
Contributor

@TomieAi That issue would need to be filed with uWebSockets.js. This isn't an issue with bun. It's unlikely that bun will support native addons that don't use Node-API since they generally interact directly with V8 (which bun doesn't use).

@tomholford
Copy link

For anyone experiencing NAPI issues with bun and the sqlite3 npm package, there is a built-in package available:

https://bun.sh/docs/api/sqlite

@BRAVO68WEB
Copy link

My issue regarding napi-nanoid or any other rust based napi packages

@ArthurTimofey
Copy link

Hey,

Excuse me ignorance but I am working with OpenCV, I see that the NAPI Register module is indeed complete but at the same time I am running into an issue with @u4/opencv4nodejs where by I run into a node API error:

TypeError: symbol 'napi_register_module_v1' not found in native module. Is this a Node API (napi) module?

As mentioned by @kjvalencik, I will also ask opencv4nodejs repo to see if there is a solution other than needing the make .mjs files to run scripts using the shell from bun and pipe the result back to bun. Any suggestions welcome.

@kjvalencik
Copy link
Contributor

@ArthurTimofey that addon looks like it's built with NAN and not Node-API. It won't be portable.

@chung-leong
Copy link

One thing I've noticed is that Node can handle null napi_value in a reasonable manner whereas Bun would crash. For example, I have the following code:

        if (napi_get_reference_value(env, md->js_env, &js_env) == napi_ok
          && napi_get_boolean(env, true, &released) == napi_ok) {
            napi_set_named_property(env, js_env, "released", released);
        }

md->js_env is a weak reference so js_env can be null if the object has been gc'ed. Node doesn't have a problem with this. Preseumbly, the call to napi_set_named_property would just return a error code. napi_create_function in Node also doesn't have a problem with the callback returning null.

@nicolasembleton
Copy link

It seems that napi_type_tag_object is the latest that's not supported in this list. Is there any chance it gets supported sometimes?

@di-sukharev
Copy link

di-sukharev commented Sep 11, 2024

It seems that napi_type_tag_object is the latest that's not supported in this list. Is there any chance it gets supported sometimes?

+1

trying to run tree-sitter binding for node

get:

warning: Node-API function "napi_type_tag_object" is not implemented yet.
 Track the status of Node-API in Bun: https://github.com/oven-sh/bun/issues/158
17 | var uv = (process.versions.uv || '').split('.')[0]
18 | 
19 | module.exports = load
20 | 
21 | function load (dir) {
22 |   return runtimeRequire(load.resolve(dir))
              ^
error: ENOENT: No such file or directory
      at load (/Users/my_path/node_modules/node-gyp-build/node-gyp-build.js:22:10)
      at /Users/my_path/node_modules/tree-sitter-javascript/bindings/node/index.js:3:8

Bun v1.1.27 (macOS arm64)

@ashvardanian
Copy link

Hey everyone! I've been following this issue for over a year and am very excited to see progress. Is it possible to slightly extend the list?

NAPI doesn't provide a single good interface to inspect the contents of strings and other buffers without copying them, which makes writing high-performance native extensions for binary data a complete nightmare. I was stuck with this while working on StringZilla JS bindings and can't afford to copy a string if the operation in question is something like substring search, fuzzy matching, hashing, or something else lightweight.

Assuming Bun's focus on performance and development velocity, I felt it's wiser to propose such extensions here rather than to NAPI directly. I'm happy to open a separate issue if it makes sense 🤗

Thanks to everyone involved!

@jraymakers
Copy link

There's a TODO in the code regarding the lossless out parameter of napi_get_value_bigint_int64 and napi_get_value_bigint_uint64, but these functions are marked as "done" in the list above. Is there a plan to implement this parameter? Is that tracked anywhere (other than this issue)?

(The fact that this parameter is not implement likely leads to this issue with the DuckDB Node "Neo" bindings.)

@190n
Copy link
Contributor

190n commented Dec 16, 2024

Thanks for bringing this up. napi_get_value_bigint_{int,uint}64 have been implemented more correctly in PR #14501, but it'll take a bit of time to merge that PR completely as it's a very extensive rewrite of almost all of our Node-API layer. But since the BigInt issue is affecting a package and the change is much more scoped, I can cherry-pick the fixes onto a new PR which we can ship sooner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tracking An umbrella issue for tracking big features
Projects
None yet
Development

No branches or pull requests