diff --git a/packages/host/cpp/AddonRegistry.cpp b/packages/host/cpp/AddonRegistry.cpp new file mode 100644 index 00000000..b4e74c7b --- /dev/null +++ b/packages/host/cpp/AddonRegistry.cpp @@ -0,0 +1,226 @@ +#include "AddonRegistry.hpp" +#include // for std::isalnum + +#ifndef NODE_API_DEFAULT_MODULE_API_VERSION +#define NODE_API_DEFAULT_MODULE_API_VERSION 8 +#endif + +using namespace facebook; + +namespace { +napi_status napi_emplace_named_property_object(napi_env env, + napi_value object, + const char *utf8Name, + napi_value *outObject) { + bool propertyFound = false; + napi_status status = napi_has_named_property(env, object, utf8Name, &propertyFound); + assert(napi_ok == status); + + assert(nullptr != outObject); + if (propertyFound) { + status = napi_get_named_property(env, object, utf8Name, outObject); + } else { + // Need to create it first + status = napi_create_object(env, outObject); + assert(napi_ok == status); + + status = napi_set_named_property(env, object, utf8Name, *outObject); + } + + return status; +} + +bool endsWith(const std::string_view &str, const std::string_view &suffix) { +#if __cplusplus >= 202002L // __cpp_lib_starts_ends_with + return str.ends_with(suffix); +#else + return str.size() >= suffix.size() + && std::equal(suffix.rbegin(), suffix.rend(), str.rbegin()); +#endif +} + +std::string_view stripSuffix(const std::string_view &str, const std::string_view &suffix) { + if (endsWith(str, suffix)) { + return str.substr(0, str.size() - suffix.size()); + } else { + return str; + } +} + +void sanitizeLibraryNameInplace(std::string &name) { + // Strip the extension (if present) + // NOTE: This is needed when working with updated Babel plugin + name = stripSuffix(name, ".node"); + + for (char &c : name) { + if (!std::isalnum(c)) { + c = '-'; + } + } +} +} // namespace + +namespace callstack::nodeapihost { + +AddonRegistry::NodeAddon& AddonRegistry::loadAddon(std::string packageName, + std::string subpath) { + const std::string fqan = packageName + subpath.substr(1); + auto [it, inserted] = + trackedAddons_.try_emplace(fqan, NodeAddon(packageName, subpath)); + NodeAddon &addon = it->second; + + sanitizeLibraryNameInplace(packageName); + sanitizeLibraryNameInplace(subpath); + const std::string libraryName = packageName + subpath; + + if (inserted || !it->second.isLoaded()) { +#if defined(__APPLE__) + const std::string libraryPath = "@rpath/" + libraryName + ".framework/" + libraryName; + tryLoadAddonAsDynamicLib(addon, libraryPath); +#elif defined(__ANDROID__) + const std::string libraryPath = "lib" + libraryName + ".so"; + tryLoadAddonAsDynamicLib(addon, libraryPath); +#else + abort(); +#endif + } + + return addon; +} + +bool AddonRegistry::tryLoadAddonAsDynamicLib(NodeAddon &addon, const std::string &path) { + { + // There can be only a SINGLE pending module (the same limitation + // has Node.js since Jan 28, 2014 commit 76b9846, see link below). + // We MUST clear it before attempting to load next addon. + // https://github.com/nodejs/node/blob/76b98462e589a69d9fd48ccb9fb5f6e96b539715/src/node.cc#L1949) + assert(nullptr == pendingRegistration_); + } + + // Load addon as dynamic library + typename LoaderPolicy::Module library = LoaderPolicy::loadLibrary(path.c_str()); + if (nullptr != library) { + addon.moduleApiVersion_ = NODE_API_DEFAULT_MODULE_API_VERSION; + if (nullptr != pendingRegistration_) { + // there is a pending addon that used the deprecated `napi_register_module()` + addon.initFun_ = pendingRegistration_; + } else { + // pending addon remains empty, we should look for the symbols... + typename LoaderPolicy::Symbol initFn = LoaderPolicy::getSymbol(library, "napi_register_module_v1"); + if (nullptr != initFn) { + addon.initFun_ = (napi_addon_register_func)initFn; + // This solves https://github.com/callstackincubator/react-native-node-api-modules/issues/4 + typename LoaderPolicy::Symbol getVersionFn = LoaderPolicy::getSymbol(library, "node_api_module_get_api_version_v1"); + if (nullptr != getVersionFn) { + addon.moduleApiVersion_ = ((node_api_addon_get_api_version_func)getVersionFn)(); + } + } + } + + if (nullptr != addon.initFun_) { + addon.moduleHandle_ = (void *)library; + addon.loadedFilePath_ = path; + } + } + + // We MUST clear the `pendingAddon_`, even when the module failed to load! + // See: https://github.com/nodejs/node/commit/a60056df3cad2867d337fc1d7adeebe66f89031a + pendingRegistration_ = nullptr; + return addon.isLoaded(); +} + +jsi::Value AddonRegistry::instantiateAddonInRuntime(jsi::Runtime &rt, NodeAddon &addon) { + // We should check if the module has already been initialized + assert(true == addon.isLoaded()); + assert(addon.moduleApiVersion_ > 0 && addon.moduleApiVersion_ <= 10); + + napi_status status = napi_ok; + napi_env env = reinterpret_cast(rt.createNodeApiEnv(addon.moduleApiVersion_)); + + // Create the "exports" object + napi_value exports; + status = napi_create_object(env, &exports); + assert(napi_ok == status); + + // Call the addon init function to populate the "exports" object + // Allowing it to replace the value entirely by its return value + // TODO: Check the return value (see Node.js specs) + exports = addon.initFun_(env, exports); + + // "Compute" the Fully Qualified Addon Path + const std::string fqap = addon.packageName_ + addon.subpath_.substr(1); + + { + napi_value descriptor; + status = createAddonDescriptor(env, exports, &descriptor); + assert(napi_ok == status); + + napi_value global; + napi_get_global(env, &global); + assert(napi_ok == status); + + status = storeAddonByFullPath(env, global, fqap, descriptor); + assert(napi_ok == status); + } + + return lookupAddonByFullPath(rt, fqap); +} + +bool AddonRegistry::handleOldNapiModuleRegister(napi_addon_register_func addonInitFunc) { + assert(nullptr == pendingRegistration_); + pendingRegistration_ = addonInitFunc; + return true; +} + +napi_status AddonRegistry::createAddonDescriptor(napi_env env, napi_value exports, napi_value *outDescriptor) { + // Create the descriptor object + assert(nullptr != outDescriptor); + napi_status status = napi_create_object(env, outDescriptor); + + // Point the `env` property to the current `napi_env` + if (napi_ok == status) { + napi_value env_value; + status = napi_create_external(env, env, nullptr, nullptr, &env_value); + if (napi_ok == status) { + status = napi_set_named_property(env, *outDescriptor, "env", env_value); + } + } + + // Cache the addons exports in descriptor's `exports` property + if (napi_ok == status) { + status = napi_set_named_property(env, *outDescriptor, "exports", exports); + } + + return status; +} + +napi_status AddonRegistry::storeAddonByFullPath(napi_env env, napi_value global, const std::string &fqap, napi_value descriptor) { + // Get the internal registry object + napi_value registryObject; + napi_status status = napi_emplace_named_property_object(env, global, kInternalRegistryKey, ®istryObject); + assert(napi_ok == status); + + status = napi_set_named_property(env, registryObject, fqap.c_str(), descriptor); + return status; +} + +jsi::Value AddonRegistry::lookupAddonByFullPath(jsi::Runtime &rt, const std::string &fqap) { + // Get the internal registry object + jsi::Object global = rt.global(); + if (!global.hasProperty(rt, kInternalRegistryKey)) { + // Create it first + jsi::Object registryObject = jsi::Object(rt); + global.setProperty(rt, kInternalRegistryKey, registryObject); + } + jsi::Value registryValue = global.getProperty(rt, kInternalRegistryKey); + jsi::Object registryObject = registryValue.asObject(rt); + + // Lookup by addon path + jsi::Value addonValue(nullptr); + if (registryObject.hasProperty(rt, fqap.c_str())) { + addonValue = registryObject.getProperty(rt, fqap.c_str()); + } + return addonValue; +} + +} // namespace callstack::nodeapihost diff --git a/packages/host/cpp/AddonRegistry.hpp b/packages/host/cpp/AddonRegistry.hpp new file mode 100644 index 00000000..d395ef3a --- /dev/null +++ b/packages/host/cpp/AddonRegistry.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include "AddonLoaders.hpp" + +namespace callstack::nodeapihost { + +class AddonRegistry { +public: + struct NodeAddon { + NodeAddon(std::string packageName, std::string subpath) + : packageName_(packageName) + , subpath_(subpath) + {} + + inline bool isLoaded() const { return nullptr != initFun_; } + + std::string packageName_; + std::string subpath_; + std::string loadedFilePath_; + void *moduleHandle_ = nullptr; + napi_addon_register_func initFun_ = nullptr; + int32_t moduleApiVersion_; + }; + + NodeAddon& loadAddon(std::string packageName, std::string subpath); + facebook::jsi::Value instantiateAddonInRuntime(facebook::jsi::Runtime &rt, NodeAddon &addon); + bool handleOldNapiModuleRegister(napi_addon_register_func addonInitFunc); + + using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround + // for my lazyness (works on iOS and Android) +private: + bool tryLoadAddonAsDynamicLib(NodeAddon &addon, const std::string &path); + napi_status createAddonDescriptor(napi_env env, napi_value exports, napi_value *outDescriptor); + napi_status storeAddonByFullPath(napi_env env, napi_value global, const std::string &fqap, napi_value descriptor); + facebook::jsi::Value lookupAddonByFullPath(facebook::jsi::Runtime &rt, const std::string &fqap); + + static constexpr const char *kInternalRegistryKey = "$NodeApiHost"; + std::unordered_map trackedAddons_; + napi_addon_register_func pendingRegistration_; +}; + +} // namespace callstack::nodeapihost diff --git a/packages/host/cpp/CxxNodeApiHostModule.cpp b/packages/host/cpp/CxxNodeApiHostModule.cpp index 727241cc..4b5bc59d 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.cpp +++ b/packages/host/cpp/CxxNodeApiHostModule.cpp @@ -1,15 +1,54 @@ +#include // std::move, std::pair, std::make_pair +#include // std::vector +#include // std::string +#include // std::equal, std::all_of +#include // std::isalnum #include "CxxNodeApiHostModule.hpp" +#include "AddonRegistry.hpp" #include "Logger.hpp" using namespace facebook; +namespace { + +bool startsWith(const std::string_view &str, const std::string_view &prefix) { +#if __cplusplus >= 202002L // __cpp_lib_starts_ends_with + return str.starts_with(prefix); +#else + return str.size() >= prefix.size() + && std::equal(prefix.begin(), prefix.end(), str.begin()); +#endif // __cplusplus >= 202002L +} + +bool isModulePathLike(const std::string_view &path) { + return std::all_of(path.begin(), path.end(), [](unsigned char c) { + return std::isalnum(c) || '_' == c || '-' == c + || '.' == c || '/' == c || ':' == c; + }); +} + +std::pair +rpartition(const std::string_view &input, char delimiter) { + if (const size_t pos = input.find_last_of(delimiter); std::string_view::npos != pos) { + const auto head = std::string_view(input).substr(0, pos); + const auto tail = std::string_view(input).substr(pos + 1); + return std::make_pair(head, tail); + } else { + return std::make_pair(std::string_view(), input); + } +} + +} // namespace + namespace callstack::nodeapihost { +AddonRegistry g_platformAddonRegistry; + CxxNodeApiHostModule::CxxNodeApiHostModule( std::shared_ptr jsInvoker) : TurboModule(CxxNodeApiHostModule::kModuleName, jsInvoker) { methodMap_["requireNodeAddon"] = - MethodMetadata{1, &CxxNodeApiHostModule::requireNodeAddon}; + MethodMetadata{3, &CxxNodeApiHostModule::requireNodeAddon}; } jsi::Value @@ -17,114 +56,111 @@ CxxNodeApiHostModule::requireNodeAddon(jsi::Runtime &rt, react::TurboModule &turboModule, const jsi::Value args[], size_t count) { auto &thisModule = static_cast(turboModule); - if (1 == count && args[0].isString()) { - return thisModule.requireNodeAddon(rt, args[0].asString(rt)); + if (3 == count) { + // Must be `requireNodeAddon(requiredPath: string, requiredPackageName: string, originalId: string)` + return thisModule.requireNodeAddon(rt, + args[0].asString(rt), + args[1].asString(rt), + args[2].asString(rt)); } - // TODO: Throw a meaningful error - return jsi::Value::undefined(); + throw jsi::JSError(rt, "Invalid number of arguments to requireNodeAddon()"); } jsi::Value CxxNodeApiHostModule::requireNodeAddon(jsi::Runtime &rt, - const jsi::String libraryName) { - const std::string libraryNameStr = libraryName.utf8(rt); + const jsi::String &requiredPath, + const jsi::String &requiredPackageName, + const jsi::String &originalId) { + return requireNodeAddon(rt, + requiredPath.utf8(rt), + requiredPackageName.utf8(rt), + originalId.utf8(rt)); +} - auto [it, inserted] = nodeAddons_.emplace(libraryNameStr, NodeAddon()); - NodeAddon &addon = it->second; +jsi::Value +CxxNodeApiHostModule::requireNodeAddon(jsi::Runtime &rt, + const std::string &requiredPath, + const std::string &requiredPackageName, + const std::string &originalId) { + // Ensure that user-supplied inputs contain only allowed characters + if (!isModulePathLike(requiredPath)) { + throw jsi::JSError(rt, "Invalid characters in `requiredPath`. Only ASCII alphanumerics are allowed."); + } - // Check if this module has been loaded already, if not then load it... - if (inserted) { - if (!loadNodeAddon(addon, libraryNameStr)) { - return jsi::Value::undefined(); + // Check if this is a prefixed import (e.g. `node:fs/promises`) + const auto [pathPrefix, strippedPath] = rpartition(requiredPath, ':'); + if (!pathPrefix.empty()) { + // URL protocol or prefix detected, dispatch via custom resolver + std::string pathPrefixCopy(pathPrefix); // HACK: Need explicit cast to `std::string` + if (auto handler = prefixResolvers_.find(pathPrefixCopy); prefixResolvers_.end() != handler) { + // HACK: Smuggle the `pathPrefix` as new `requiredPackageName` + return (handler->second)(rt, strippedPath, pathPrefix, originalId); + } else { + throw jsi::JSError(rt, "Unsupported protocol or prefix \"" + pathPrefixCopy + "\". Have you registered it?"); } } - // Initialize the addon if it has not already been initialized - if (!rt.global().hasProperty(rt, addon.generatedName.data())) { - initializeNodeModule(rt, addon); + // Check, if this package has been overridden + if (auto handler = packageOverrides_.find(requiredPackageName); packageOverrides_.end() != handler) { + // This package has a custom resolver, invoke it + return (handler->second)(rt, strippedPath, requiredPackageName, originalId); } - // Look the exports up (using JSI) and return it... - return rt.global().getProperty(rt, addon.generatedName.data()); + // Otherwise, "requiredPath" must be a package-relative specifier + return resolveRelativePath(rt, strippedPath, requiredPackageName, originalId); } -bool CxxNodeApiHostModule::loadNodeAddon(NodeAddon &addon, - const std::string &libraryName) const { -#if defined(__APPLE__) - std::string libraryPath = - "@rpath/" + libraryName + ".framework/" + libraryName; -#elif defined(__ANDROID__) - std::string libraryPath = "lib" + libraryName + ".so"; -#else - abort() -#endif - - log_debug("[%s] Loading addon by '%s'", libraryName.c_str(), - libraryPath.c_str()); - - typename LoaderPolicy::Symbol initFn = NULL; - typename LoaderPolicy::Module library = - LoaderPolicy::loadLibrary(libraryPath.c_str()); - if (NULL != library) { - log_debug("[%s] Loaded addon", libraryName.c_str()); - addon.moduleHandle = library; - - // Generate a name allowing us to reference the exports object from JSI - // later Instead of using random numbers to avoid name clashes, we just use - // the pointer address of the loaded module - addon.generatedName.resize(32, '\0'); - snprintf(addon.generatedName.data(), addon.generatedName.size(), - "RN$NodeAddon_%p", addon.moduleHandle); - - initFn = LoaderPolicy::getSymbol(library, "napi_register_module_v1"); - if (NULL != initFn) { - log_debug("[%s] Found napi_register_module_v1 (%p)", libraryName.c_str(), - initFn); - addon.init = (napi_addon_register_func)initFn; - } else { - log_debug("[%s] Failed to find napi_register_module_v1. Expecting the " - "addon to call napi_module_register to register itself.", - libraryName.c_str()); - } - // TODO: Read "node_api_module_get_api_version_v1" to support the addon - // declaring its Node-API version - // @see - // https://github.com/callstackincubator/react-native-node-api/issues/4 - } else { - log_debug("[%s] Failed to load library", libraryName.c_str()); +jsi::Value +CxxNodeApiHostModule::resolveRelativePath(facebook::jsi::Runtime &rt, + const std::string_view &requiredPath, + const std::string_view &requiredPackageName, + const std::string_view &originalId) { + if (!startsWith(requiredPath, "./")) { + throw jsi::JSError(rt, "requiredPath must be relative and cannot leave its package root."); } - return NULL != initFn; + + // Check whether (`requiredPackageName`, `requiredPath`) is already cached + // NOTE: Cache must to be `jsi::Runtime`-local + auto [exports, isCached] = lookupRequireCache(rt, + requiredPackageName, + requiredPath); + + if (!isCached) { + // Ask the global addon registry to load given Node-API addon. + // If other runtime loaded it already, the OS will return the same pointer. + // NOTE: This method might try multiple platform-specific paths. + const std::string packageNameCopy(requiredPackageName); + const std::string requiredPathCopy(requiredPath); + auto &addon = g_platformAddonRegistry.loadAddon(packageNameCopy, requiredPathCopy); + + // Create a `napi_env` and initialize the addon + exports = g_platformAddonRegistry.instantiateAddonInRuntime(rt, addon); + updateRequireCache(rt, requiredPackageName, requiredPath, exports); + } + + return std::move(exports); +} + +std::pair +CxxNodeApiHostModule::lookupRequireCache(::jsi::Runtime &rt, + const std::string_view &packageName, + const std::string_view &subpath) { + // TODO: Implement me + return std::make_pair(jsi::Value(), false); +} + +void CxxNodeApiHostModule::updateRequireCache(jsi::Runtime &rt, + const std::string_view &packageName, + const std::string_view &subpath, + jsi::Value &value) { + // TODO: Implement me } -bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt, - NodeAddon &addon) { - // We should check if the module has already been initialized - assert(NULL != addon.moduleHandle); - assert(NULL != addon.init); - napi_status status = napi_ok; - // TODO: Read the version from the addon - // @see - // https://github.com/callstackincubator/react-native-node-api/issues/4 - napi_env env = reinterpret_cast(rt.createNodeApiEnv(8)); - - // Create the "exports" object - napi_value exports; - status = napi_create_object(env, &exports); - assert(status == napi_ok); - - // Call the addon init function to populate the "exports" object - // Allowing it to replace the value entirely by its return value - exports = addon.init(env, exports); - - napi_value global; - napi_get_global(env, &global); - assert(status == napi_ok); - - status = - napi_set_named_property(env, global, addon.generatedName.data(), exports); - assert(status == napi_ok); - - return true; +extern "C" { +NAPI_EXTERN void NAPI_CDECL napi_module_register(napi_module *mod) { + assert(NULL != mod && NULL != mod->nm_register_func); + g_platformAddonRegistry.handleOldNapiModuleRegister(mod->nm_register_func); } +} // extern "C" } // namespace callstack::nodeapihost diff --git a/packages/host/cpp/CxxNodeApiHostModule.hpp b/packages/host/cpp/CxxNodeApiHostModule.hpp index f77a89af..bc2e87e9 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.hpp +++ b/packages/host/cpp/CxxNodeApiHostModule.hpp @@ -1,5 +1,7 @@ #pragma once +#include // std::string_view + #include #include #include @@ -12,6 +14,12 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { public: static constexpr const char *kModuleName = "NodeApiHost"; + using ResolverFunc = std::function; + CxxNodeApiHostModule(std::shared_ptr jsInvoker); static facebook::jsi::Value @@ -19,20 +27,31 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { facebook::react::TurboModule &turboModule, const facebook::jsi::Value args[], size_t count); facebook::jsi::Value requireNodeAddon(facebook::jsi::Runtime &rt, - const facebook::jsi::String path); + const facebook::jsi::String &requiredPath, + const facebook::jsi::String &requiredPackageName, + const facebook::jsi::String &originalId); + facebook::jsi::Value requireNodeAddon(facebook::jsi::Runtime &rt, + const std::string &requiredPath, + const std::string &requiredPackageName, + const std::string &originalId); + + facebook::jsi::Value resolveRelativePath(facebook::jsi::Runtime &rt, + const std::string_view &requiredPath, + const std::string_view &requiredPackageName, + const std::string_view &originalId); + + std::pair + lookupRequireCache(facebook::jsi::Runtime &rt, + const std::string_view &packageName, + const std::string_view &subpath); + void updateRequireCache(facebook::jsi::Runtime &rt, + const std::string_view &packageName, + const std::string_view &subpath, + facebook::jsi::Value &value); protected: - struct NodeAddon { - void *moduleHandle; - napi_addon_register_func init; - std::string generatedName; - }; - std::unordered_map nodeAddons_; - using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround - // for my lazyness (work on iOS and Android) - - bool loadNodeAddon(NodeAddon &addon, const std::string &path) const; - bool initializeNodeModule(facebook::jsi::Runtime &rt, NodeAddon &addon); + std::unordered_map prefixResolvers_; + std::unordered_map packageOverrides_; }; } // namespace callstack::nodeapihost diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-weak-node-api-injector.ts index ff73b0ed..d9073edc 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-weak-node-api-injector.ts @@ -42,6 +42,7 @@ export function generateSource(functions: FunctionDecl[]) { log_debug("Injecting WeakNodeApiHost"); inject_weak_node_api_host(WeakNodeApiHost { + .napi_module_register = napi_module_register, ${functions .filter(({ kind }) => kind === "engine") .flatMap(({ name }) => `.${name} = ${name},`) diff --git a/packages/host/scripts/generate-weak-node-api.ts b/packages/host/scripts/generate-weak-node-api.ts index 23a3c89d..c33847b2 100644 --- a/packages/host/scripts/generate-weak-node-api.ts +++ b/packages/host/scripts/generate-weak-node-api.ts @@ -33,17 +33,43 @@ export function generateHeader(functions: FunctionDecl[]) { * Generates source code for a version script for the given Node API version. */ export function generateSource(functions: FunctionDecl[]) { + const interceptModuleRegisterCalls = true; return [ "// This file is generated by react-native-node-api", + `#define WITH_DEFERRED_NAPI_MODULE_REGISTER ${interceptModuleRegisterCalls ? 1 : 0}`, `#include "weak_node_api.hpp"`, // Generated header + "", + // Declare globals needed for bookkeeping intercepted calls + "#if WITH_DEFERRED_NAPI_MODULE_REGISTER", + "#include ", + "#include ", + "std::mutex g_internal_state_mutex;", + "std::vector g_pending_modules;", + "#endif // WITH_DEFERRED_NAPI_MODULE_REGISTER", + "", // Generate the struct of function pointers "WeakNodeApiHost g_host;", "void inject_weak_node_api_host(const WeakNodeApiHost& host) {", " g_host = host;", + "", + "#if WITH_DEFERRED_NAPI_MODULE_REGISTER", + " // Flush pending `napi_module_register()` calls", + " if (nullptr != host.napi_module_register) {", + " std::lock_guard lock(g_internal_state_mutex);", + " fprintf(stderr,", + ` "Flushing %zu intercepted calls to 'napi_module_register'...\\n",`, + " g_pending_modules.size());", + " for (napi_module *module : g_pending_modules) {", + " host.napi_module_register(module);", + " }", + " g_pending_modules.clear();", + " }", + "#endif // WITH_DEFERRED_NAPI_MODULE_REGISTER", "};", - ``, + "", // Generate function calling into the host ...functions.flatMap(({ returnType, noReturn, name, argumentTypes }) => { + const isDeferrable = name === "napi_module_register"; return [ `extern "C" ${returnType} ${ noReturn ? " __attribute__((noreturn))" : "" @@ -52,14 +78,26 @@ export function generateSource(functions: FunctionDecl[]) { .join(", ")}) {`, `if (g_host.${name} == nullptr) {`, ` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`, - " abort();", - "}", - (returnType === "void" ? "" : "return ") + + ...(isDeferrable ? [ + "#if WITH_DEFERRED_NAPI_MODULE_REGISTER", + " {", + " std::lock_guard guard(g_internal_state_mutex);", + " g_pending_modules.push_back(arg0);", + " }", + "#else", + " abort();", + "#endif // WITH_DEFERRED_NAPI_MODULE_REGISTER", + ] : [ + " abort();", + ]), + "} else {", + " " + (returnType === "void" ? "" : "return ") + "g_host." + name + "(" + argumentTypes.map((_, index) => `arg${index}`).join(", ") + ");", + "}", "};", ]; }), diff --git a/packages/host/src/node/babel-plugin/plugin.test.ts b/packages/host/src/node/babel-plugin/plugin.test.ts index 73e884aa..c95b15d0 100644 --- a/packages/host/src/node/babel-plugin/plugin.test.ts +++ b/packages/host/src/node/babel-plugin/plugin.test.ts @@ -6,110 +6,111 @@ import { transformFileSync } from "@babel/core"; import { plugin } from "./plugin.js"; import { setupTempDirectory } from "../test-utils.js"; -import { getLibraryName } from "../path-utils.js"; describe("plugin", () => { - it("transforms require calls, regardless", (context) => { - const tempDirectoryPath = setupTempDirectory(context, { - "package.json": `{ "name": "my-package" }`, - "addon-1.apple.node/addon-1.node": - "// This is supposed to be a binary file", - "addon-2.apple.node/addon-2.node": - "// This is supposed to be a binary file", - "addon-1.js": ` - const addon = require('./addon-1.node'); - console.log(addon); - `, - "addon-2.js": ` - const addon = require('./addon-2.node'); - console.log(addon); - `, - "sub-directory/addon-1.js": ` - const addon = require('../addon-1.node'); - console.log(addon); - `, - "addon-1-bindings.js": ` - const addon = require('bindings')('addon-1'); - console.log(addon); - `, - "require-js-file.js": ` - const addon = require('./addon-1.js'); - console.log(addon); - `, - }); + describe("transforms require calls, regardless", () => { + const EXPECTED_PKG_NAME = "my-package"; - const ADDON_1_REQUIRE_ARG = getLibraryName( - path.join(tempDirectoryPath, "addon-1"), - { stripPathSuffix: false } - ); - const ADDON_2_REQUIRE_ARG = getLibraryName( - path.join(tempDirectoryPath, "addon-2"), - { stripPathSuffix: false } - ); + type TestCaseParams = { + resolvedPath?: string; + originalPath: string; + inputFile: string; + }; - { - const result = transformFileSync( - path.join(tempDirectoryPath, "./addon-1.js"), - { plugins: [[plugin, { stripPathSuffix: false }]] } - ); - assert(result); - const { code } = result; - assert( - code && code.includes(`requireNodeAddon("${ADDON_1_REQUIRE_ARG}")`), - `Unexpected code: ${code}` - ); - } + const testCases: ReadonlyArray = [ + { resolvedPath: "./addon-1.node", originalPath: "./addon-1.node", inputFile: "./addon-1.js" }, + { resolvedPath: "./addon-2.node", originalPath: "./addon-2.node", inputFile: "./addon-2.js" }, + { resolvedPath: "./addon-1.node", originalPath: "../addon-1.node", inputFile: "./sub-directory/addon-1.js" }, + { resolvedPath: "./addon-2.node", originalPath: "../addon-2.node", inputFile: "./sub-directory-3/addon-outside.js" }, + { resolvedPath: "./addon-1.node", originalPath: "addon-1", inputFile: "./addon-1-bindings.js" }, + { resolvedPath: undefined, originalPath: "./addon-1.js", inputFile: "./require-js-file.js" }, + ]; + for (const { resolvedPath, originalPath, inputFile } of testCases) { + const expectedMessage = resolvedPath + ? `transform to requireNodeAddon() with "${resolvedPath}"` + : "NOT transform to requireNodeAddon()"; - { - const result = transformFileSync( - path.join(tempDirectoryPath, "./addon-2.js"), - { plugins: [[plugin, { naming: "hash" }]] } - ); - assert(result); - const { code } = result; - assert( - code && code.includes(`requireNodeAddon("${ADDON_2_REQUIRE_ARG}")`), - `Unexpected code: ${code}` - ); + it(`${inputFile} should ${expectedMessage}`, (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "${EXPECTED_PKG_NAME}" }`, + "addon-1.node": + "// This is supposed to be a binary file", + "addon-2.node": + "// This is supposed to be a binary file", + "addon-1.js": ` + const addon = require('./addon-1.node'); + console.log(addon); + `, + "addon-2.js": ` + const addon = require('./addon-2.node'); + console.log(addon); + `, + "sub-directory/addon-1.js": ` + const addon = require('../addon-1.node'); + console.log(addon); + `, + "sub-directory-3/package.json": `{ "name": "sub-package" }`, + "sub-directory-3/addon-outside.js": ` + const addon = require('../addon-2.node'); + console.log(addon); + `, + "addon-1-bindings.js": ` + const addon = require('bindings')('addon-1'); + console.log(addon); + `, + "require-js-file.js": ` + const addon = require('./addon-1.js'); + console.log(addon); + `, + }); + const result = transformFileSync( + path.join(tempDirectoryPath, inputFile), + { plugins: [[plugin, {}]] } + ); + assert(result); + const { code } = result; + if (!resolvedPath) { + assert( + code && !code.includes(`requireNodeAddon`), + `Unexpected code: ${code}` + ); + } else { + assert( + code && code.includes(`requireNodeAddon("${resolvedPath}", "${EXPECTED_PKG_NAME}", "${originalPath}")`), + `Unexpected code: ${code}` + ); + } + }); } + }); - { - const result = transformFileSync( - path.join(tempDirectoryPath, "./sub-directory/addon-1.js"), - { plugins: [[plugin, { naming: "hash" }]] } - ); - assert(result); - const { code } = result; - assert( - code && code.includes(`requireNodeAddon("${ADDON_1_REQUIRE_ARG}")`), - `Unexpected code: ${code}` - ); - } + it("transforms require calls to packages with native entry point", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "node_modules/@scope/my-package/package.json": + `{ "name": "@scope/my-package", "main": "./build/Release/addon-1.node" }`, + "node_modules/@scope/my-package/build/Release/addon-1.node": + "// This is supposed to be a binary file", + "package.json": `{ "name": "my-consumer" }`, + "test.js": ` + const addon = require('@scope/my-package'); + console.log(addon); + ` + }); - { - const result = transformFileSync( - path.join(tempDirectoryPath, "./addon-1-bindings.js"), - { plugins: [[plugin, { naming: "hash" }]] } - ); - assert(result); - const { code } = result; - assert( - code && code.includes(`requireNodeAddon("${ADDON_1_REQUIRE_ARG}")`), - `Unexpected code: ${code}` - ); - } + const EXPECTED_PKG_NAME = "@scope/my-package"; + const EXPECTED_PATH = "./build/Release/addon-1.node"; { const result = transformFileSync( - path.join(tempDirectoryPath, "./require-js-file.js"), - { plugins: [[plugin, { naming: "hash" }]] } + path.join(tempDirectoryPath, "test.js"), + { plugins: [[plugin]] } ); assert(result); const { code } = result; assert( - code && !code.includes(`requireNodeAddon`), + code && code.includes(`requireNodeAddon("${EXPECTED_PATH}", "${EXPECTED_PKG_NAME}", "${EXPECTED_PKG_NAME}")`), `Unexpected code: ${code}` ); - } + }; }); }); diff --git a/packages/host/src/node/babel-plugin/plugin.ts b/packages/host/src/node/babel-plugin/plugin.ts index 49733089..486b3821 100644 --- a/packages/host/src/node/babel-plugin/plugin.ts +++ b/packages/host/src/node/babel-plugin/plugin.ts @@ -4,9 +4,13 @@ import path from "node:path"; import type { PluginObj, NodePath } from "@babel/core"; import * as t from "@babel/types"; -import { getLibraryName, isNodeApiModule, NamingStrategy } from "../path-utils"; +import { + determineModuleContext, + isNodeApiModule, + findNodeAddonForBindings, +} from "../path-utils"; -type PluginOptions = { +export type PluginOptions = { stripPathSuffix?: boolean; }; @@ -20,12 +24,44 @@ function assertOptions(opts: unknown): asserts opts is PluginOptions { } } -export function replaceWithRequireNodeAddon( +// This function should work with both CommonJS and ECMAScript modules, +// (pretending that addons are supported with ES module imports), hence it +// must accept following import specifiers: +// - "Relative specifiers" (e.g. `./build/Release/addon.node`) +// - "Bare specifiers", in particular +// - to an entry point (e.g. `@callstack/example-addon`) +// - any specific exported feature within +// - "Absolute specifiers" like `node:fs/promise` and URLs. +// +// This function should also respect the Package entry points defined in the +// respective "package.json" file using "main" or "exports" and "imports" +// fields (including conditional exports and subpath imports). +// - https://nodejs.org/api/packages.html#package-entry-points +// - https://nodejs.org/api/packages.html#subpath-imports +function tryResolveModulePath(id: string, from: string): string | undefined { + if (id.includes(":")) { + // This must be a prefixed "Absolute specifier". We assume its a built-in + // module and pass it through without any changes. For security reasons, + // we don't support URLs to dynamic libraries (like Node-API addons). + return undefined; + } else { + // TODO: Stay compatible with https://nodejs.org/api/modules.html#all-together + try { + return require.resolve(id, { paths: [from] }); + } catch { + return undefined; + } + } +} + +export function replaceWithRequireNodeAddon3( p: NodePath, - modulePath: string, - naming: NamingStrategy + resolvedPath: string, + originalId: string ) { - const requireCallArgument = getLibraryName(modulePath, naming); + const { packageName, relativePath } = determineModuleContext(resolvedPath); + const dotRelativePath = relativePath.startsWith("./") ? relativePath : `./${relativePath}`; + p.replaceWith( t.callExpression( t.memberExpression( @@ -34,7 +70,8 @@ export function replaceWithRequireNodeAddon( ]), t.identifier("requireNodeAddon") ), - [t.stringLiteral(requireCallArgument)] + [dotRelativePath, packageName, originalId] + .map(t.stringLiteral), ) ); } @@ -44,7 +81,6 @@ export function plugin(): PluginObj { visitor: { CallExpression(p) { assertOptions(this.opts); - const { stripPathSuffix = false } = this.opts; if (typeof this.filename !== "string") { // This transformation only works when the filename is known return; @@ -64,20 +100,17 @@ export function plugin(): PluginObj { const [argument] = p.parent.arguments; if (argument.type === "StringLiteral") { const id = argument.value; - const relativePath = path.join(from, id); - // TODO: Support traversing the filesystem to find the Node-API module - if (isNodeApiModule(relativePath)) { - replaceWithRequireNodeAddon(p.parentPath, relativePath, { - stripPathSuffix, - }); + const resolvedPath = findNodeAddonForBindings(id, from); + if (resolvedPath !== undefined) { + replaceWithRequireNodeAddon3(p.parentPath, resolvedPath, id); } } - } else if ( - !path.isAbsolute(id) && - isNodeApiModule(path.join(from, id)) - ) { - const relativePath = path.join(from, id); - replaceWithRequireNodeAddon(p, relativePath, { stripPathSuffix }); + } else { + // This should handle "bare specifiers" and "private imports" that start with `#` + const resolvedPath = tryResolveModulePath(id, from); + if (!!resolvedPath && isNodeApiModule(resolvedPath)) { + replaceWithRequireNodeAddon3(p, resolvedPath, id); + } } } }, diff --git a/packages/host/src/node/path-utils.test.ts b/packages/host/src/node/path-utils.test.ts index 08a788ff..211e29c9 100644 --- a/packages/host/src/node/path-utils.test.ts +++ b/packages/host/src/node/path-utils.test.ts @@ -6,6 +6,8 @@ import fswin from "fswin"; import { determineModuleContext, + determineNormalizedModuleContext, + findNodeAddonForBindings, findNodeApiModulePaths, findPackageDependencyPaths, getLibraryName, @@ -135,14 +137,14 @@ describe("stripExtension", () => { }); }); -describe("determineModuleContext", () => { +describe("determineNormalizedModuleContext", () => { it("strips the file extension", (context) => { const tempDirectoryPath = setupTempDirectory(context, { "package.json": `{ "name": "my-package" }`, }); { - const { packageName, relativePath } = determineModuleContext( + const { packageName, relativePath } = determineNormalizedModuleContext( path.join(tempDirectoryPath, "some-dir/some-file.node") ); assert.equal(packageName, "my-package"); @@ -156,15 +158,17 @@ describe("determineModuleContext", () => { }); { - const { packageName, relativePath } = determineModuleContext( + const { packageName, relativePath } = determineNormalizedModuleContext( path.join(tempDirectoryPath, "some-dir/libsome-file.node") ); assert.equal(packageName, "my-package"); assert.equal(relativePath, "some-dir/some-file"); } }); +}); - it("resolves the correct package name", (context) => { +describe("determineModuleContext", () => { + it("resolves the correct unscoped package name", (context) => { const tempDirectoryPath = setupTempDirectory(context, { "package.json": `{ "name": "root-package" }`, // Two sub-packages with the same name @@ -177,7 +181,7 @@ describe("determineModuleContext", () => { path.join(tempDirectoryPath, "sub-package-a/some-file.node") ); assert.equal(packageName, "my-sub-package-a"); - assert.equal(relativePath, "some-file"); + assert.equal(relativePath, "some-file.node"); } { @@ -185,7 +189,32 @@ describe("determineModuleContext", () => { path.join(tempDirectoryPath, "sub-package-b/some-file.node") ); assert.equal(packageName, "my-sub-package-b"); - assert.equal(relativePath, "some-file"); + assert.equal(relativePath, "some-file.node"); + } + }); + + it("resolves the correct scoped package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "root-package" }`, + // Two sub-packages with the same name + "sub-package-a/package.json": `{ "name": "@root-package/my-sub-package-a" }`, + "sub-package-b/package.json": `{ "name": "@root-package/my-sub-package-b" }`, + }); + + { + const { packageName, relativePath } = determineModuleContext( + path.join(tempDirectoryPath, "sub-package-a/some-file.node") + ); + assert.equal(packageName, "@root-package/my-sub-package-a"); + assert.equal(relativePath, "some-file.node"); + } + + { + const { packageName, relativePath } = determineModuleContext( + path.join(tempDirectoryPath, "sub-package-b/some-file.node") + ); + assert.equal(packageName, "@root-package/my-sub-package-b"); + assert.equal(relativePath, "some-file.node"); } }); }); @@ -372,3 +401,31 @@ describe("determineModuleContext", () => { assert.equal(readCount, 1); }); }); + +describe("findNodeAddonForBindings()", () => { + it("should look for addons in common paths", (context) => { + // Arrange + const expectedPaths = { + "addon_1": "addon_1.node", + "addon_2": "build/Release/addon_2.node", + "addon_3": "build/Debug/addon_3.node", + "addon_4": "build/addon_4.node", + "addon_5": "out/Release/addon_5.node", + "addon_6": "out/Debug/addon_6.node", + "addon_7": "Release/addon_7.node", + "addon_8": "Debug/addon_8.node", + }; + const tempDirectoryPath = setupTempDirectory(context, + Object.fromEntries( + Object.values(expectedPaths) + .map((p) => [p, "// This is supposed to be a binary file"]) + ) + ); + // Act & Assert + Object.entries(expectedPaths).forEach(([name, relPath]) => { + const expectedPath = path.join(tempDirectoryPath, relPath); + const actualPath = findNodeAddonForBindings(name, tempDirectoryPath); + assert.equal(actualPath, expectedPath); + }); + }); +}); \ No newline at end of file diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index 179688ce..2770b826 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -31,6 +31,13 @@ const packageNameCache = new Map(); * TODO: Consider checking for a specific platform extension. */ export function isNodeApiModule(modulePath: string): boolean { + { + // HACK: Take a shortcut (if applicable): existing `.node` files are addons + try { + fs.accessSync(modulePath.endsWith(".node") ? modulePath : `${modulePath}.node`); + return true; + } catch { /* empty */ } + } const dir = path.dirname(modulePath); const baseName = path.basename(modulePath, ".node"); let entries: string[]; @@ -127,8 +134,22 @@ export function determineModuleContext( packageNameCache.set(pkgDir, pkgName); } // Compute module-relative path - const relPath = normalizeModulePath(path.relative(pkgDir, originalPath)); - return { packageName: pkgName, relativePath: relPath }; + const relativePath = path.relative(pkgDir, originalPath) + .replaceAll("\\", "/"); + return { packageName: pkgName, relativePath }; +} + +/** + * Traverse the filesystem upward to find a name for the package that which contains a file. + * This variant normalizes the module path. + */ +export function determineNormalizedModuleContext( + modulePath: string, + originalPath = modulePath +): ModuleContext { + const { packageName, relativePath } = determineModuleContext(modulePath, originalPath); + const relPath = normalizeModulePath(relativePath); + return { packageName, relativePath: relPath }; } export function normalizeModulePath(modulePath: string) { @@ -147,7 +168,7 @@ export function escapePath(modulePath: string) { * Get the name of the library which will be used when the module is linked in. */ export function getLibraryName(modulePath: string, naming: NamingStrategy) { - const { packageName, relativePath } = determineModuleContext(modulePath); + const { packageName, relativePath } = determineNormalizedModuleContext(modulePath); const escapedPackageName = escapePath(packageName); return naming.stripPathSuffix ? escapedPackageName @@ -385,3 +406,29 @@ export function getLatestMtime(fromPath: string): number { return latest; } + +// NOTE: List of paths influenced by `node-bindings` itself +// https://github.com/TooTallNate/node-bindings/blob/v1.3.0/bindings.js#L21 +const nodeBindingsSubdirs = [ + "./", + "./build/Release", + "./build/Debug", + "./build", + "./out/Release", + "./out/Debug", + "./Release", + "./Debug", +]; + +export function findNodeAddonForBindings(id: string, fromDir: string) { + const idWithExt = id.endsWith(".node") ? id : `${id}.node`; + // Support traversing the filesystem to find the Node-API module. + // Currently, we check the most common directories like `bindings` does. + for (const subdir of nodeBindingsSubdirs) { + const resolvedPath = path.join(fromDir, subdir, idWithExt); + if (isNodeApiModule(resolvedPath)) { + return resolvedPath; + } + } + return undefined; +} \ No newline at end of file diff --git a/packages/host/src/react-native/NativeNodeApiHost.ts b/packages/host/src/react-native/NativeNodeApiHost.ts index fdf04dc8..fb254a22 100644 --- a/packages/host/src/react-native/NativeNodeApiHost.ts +++ b/packages/host/src/react-native/NativeNodeApiHost.ts @@ -2,7 +2,7 @@ import type { TurboModule } from "react-native"; import { TurboModuleRegistry } from "react-native"; export interface Spec extends TurboModule { - requireNodeAddon(libraryName: string): void; + requireNodeAddon(requiredPath: string, packageName?: string, originalId?: string): void; } export default TurboModuleRegistry.getEnforcing("NodeApiHost"); diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/CMakeLists.txt b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/CMakeLists.txt new file mode 100644 index 00000000..404e3114 --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.15) +project(examples-1-getting-started-2_function_arguments_twisted-napi) + +add_compile_definitions(-DNAPI_VERSION=4) + +add_library(addon_twisted SHARED addon.c ${CMAKE_JS_SRC}) +set_target_properties(addon_twisted PROPERTIES PREFIX "" SUFFIX ".node") +target_include_directories(addon_twisted PRIVATE ${CMAKE_JS_INC}) +target_link_libraries(addon_twisted PRIVATE ${CMAKE_JS_LIB}) +target_compile_features(addon_twisted PRIVATE cxx_std_17) + +if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) + # Generate node.lib + execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) +endif() diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/addon.c b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/addon.c new file mode 100644 index 00000000..1ae258f3 --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/addon.c @@ -0,0 +1,57 @@ +#include +#include "node_api_deprecated.h" +#include + +static napi_value Add(napi_env env, napi_callback_info info) { + napi_status status; + + size_t argc = 2; + napi_value args[2]; + status = napi_get_cb_info(env, info, &argc, args, NULL, NULL); + assert(status == napi_ok); + + if (argc < 2) { + napi_throw_type_error(env, NULL, "Wrong number of arguments"); + return NULL; + } + + napi_valuetype valuetype0; + status = napi_typeof(env, args[0], &valuetype0); + assert(status == napi_ok); + + napi_valuetype valuetype1; + status = napi_typeof(env, args[1], &valuetype1); + assert(status == napi_ok); + + if (valuetype0 != napi_number || valuetype1 != napi_number) { + napi_throw_type_error(env, NULL, "Wrong arguments"); + return NULL; + } + + double value0; + status = napi_get_value_double(env, args[0], &value0); + assert(status == napi_ok); + + double value1; + status = napi_get_value_double(env, args[1], &value1); + assert(status == napi_ok); + + napi_value sum; + status = napi_create_double(env, value0 + value1, &sum); + assert(status == napi_ok); + + return sum; +} + +#define DECLARE_NAPI_METHOD(name, func) \ + { name, 0, func, 0, 0, 0, napi_default, 0 } + +napi_value Init(napi_env env, napi_value exports) { + napi_status status; + napi_property_descriptor addDescriptor = DECLARE_NAPI_METHOD("add", Add); + status = napi_define_properties(env, exports, 1, &addDescriptor); + assert(status == napi_ok); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/binding.gyp b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/binding.gyp new file mode 100644 index 00000000..7be488b5 --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "addon_twisted", + "sources": [ "addon.c" ], + "defines": [ + "NAPI_VERSION=4" + ], + } + ] +} diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/index.js b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/index.js new file mode 100644 index 00000000..bcc61cf6 --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/index.js @@ -0,0 +1,3 @@ +const addon = require('bindings')('addon_twisted.node') + +console.log('This should be eight:', addon.add(3, 5)) diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/node_api_deprecated.h b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/node_api_deprecated.h new file mode 100644 index 00000000..59d493bb --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/node_api_deprecated.h @@ -0,0 +1,269 @@ +// Taken from https://github.com/nodejs/node-api-headers/blob/2a5355a65e06b081154e584ca5b0574c9c07b61d/include/node_api.h +#ifndef SRC_NODE_API_H_ +#define SRC_NODE_API_H_ + +#ifdef BUILDING_NODE_EXTENSION + #ifdef _WIN32 + // Building native module against node + #define NAPI_EXTERN __declspec(dllimport) + #elif defined(__wasm32__) + #define NAPI_EXTERN __attribute__((__import_module__("napi"))) + #endif +#endif +#include +#include + +struct uv_loop_s; // Forward declaration. + +#ifdef _WIN32 +# define NAPI_MODULE_EXPORT __declspec(dllexport) +#else +# define NAPI_MODULE_EXPORT __attribute__((visibility("default"))) +#endif + +#if defined(__GNUC__) +# define NAPI_NO_RETURN __attribute__((noreturn)) +#elif defined(_WIN32) +# define NAPI_NO_RETURN __declspec(noreturn) +#else +# define NAPI_NO_RETURN +#endif + +typedef napi_value (*napi_addon_register_func)(napi_env env, + napi_value exports); + +typedef struct napi_module { + int nm_version; + unsigned int nm_flags; + const char* nm_filename; + napi_addon_register_func nm_register_func; + const char* nm_modname; + void* nm_priv; + void* reserved[4]; +} napi_module; + +#define NAPI_MODULE_VERSION 1 + +#if defined(_MSC_VER) +#pragma section(".CRT$XCU", read) +#define NAPI_C_CTOR(fn) \ + static void __cdecl fn(void); \ + __declspec(dllexport, allocate(".CRT$XCU")) void(__cdecl * fn##_)(void) = \ + fn; \ + static void __cdecl fn(void) +#else +#define NAPI_C_CTOR(fn) \ + static void fn(void) __attribute__((constructor)); \ + static void fn(void) +#endif + +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + EXTERN_C_START \ + static napi_module _module = \ + { \ + NAPI_MODULE_VERSION, \ + flags, \ + __FILE__, \ + regfunc, \ + #modname, \ + priv, \ + {0}, \ + }; \ + NAPI_C_CTOR(_register_ ## modname) { \ + napi_module_register(&_module); \ + } \ + EXTERN_C_END + +#define NAPI_MODULE_INITIALIZER_X(base, version) \ + NAPI_MODULE_INITIALIZER_X_HELPER(base, version) +#define NAPI_MODULE_INITIALIZER_X_HELPER(base, version) base##version + +#ifdef __wasm32__ +#define NAPI_WASM_INITIALIZER \ + NAPI_MODULE_INITIALIZER_X(napi_register_wasm_v, NAPI_MODULE_VERSION) +#define NAPI_MODULE(modname, regfunc) \ + EXTERN_C_START \ + NAPI_MODULE_EXPORT napi_value NAPI_WASM_INITIALIZER(napi_env env, \ + napi_value exports) { \ + return regfunc(env, exports); \ + } \ + EXTERN_C_END +#else +#define NAPI_MODULE(modname, regfunc) \ + NAPI_MODULE_X(modname, regfunc, NULL, 0) // NOLINT (readability/null_usage) +#endif + +#define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v + +#define NAPI_MODULE_INITIALIZER \ + NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, \ + NAPI_MODULE_VERSION) + +#define NAPI_MODULE_INIT() \ + EXTERN_C_START \ + NAPI_MODULE_EXPORT napi_value \ + NAPI_MODULE_INITIALIZER(napi_env env, napi_value exports); \ + EXTERN_C_END \ + NAPI_MODULE(NODE_GYP_MODULE_NAME, NAPI_MODULE_INITIALIZER) \ + napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ + napi_value exports) + +EXTERN_C_START + +NAPI_EXTERN void napi_module_register(napi_module* mod); + +NAPI_EXTERN NAPI_NO_RETURN void napi_fatal_error(const char* location, + size_t location_len, + const char* message, + size_t message_len); + +// Methods for custom handling of async operations +NAPI_EXTERN napi_status napi_async_init(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_context* result); + +NAPI_EXTERN napi_status napi_async_destroy(napi_env env, + napi_async_context async_context); + +NAPI_EXTERN napi_status napi_make_callback(napi_env env, + napi_async_context async_context, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result); + +// Methods to provide node::Buffer functionality with napi types +NAPI_EXTERN napi_status napi_create_buffer(napi_env env, + size_t length, + void** data, + napi_value* result); +NAPI_EXTERN napi_status napi_create_external_buffer(napi_env env, + size_t length, + void* data, + napi_finalize finalize_cb, + void* finalize_hint, + napi_value* result); +NAPI_EXTERN napi_status napi_create_buffer_copy(napi_env env, + size_t length, + const void* data, + void** result_data, + napi_value* result); +NAPI_EXTERN napi_status napi_is_buffer(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status napi_get_buffer_info(napi_env env, + napi_value value, + void** data, + size_t* length); + +// Methods to manage simple async operations +NAPI_EXTERN +napi_status napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result); +NAPI_EXTERN napi_status napi_delete_async_work(napi_env env, + napi_async_work work); +NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, + napi_async_work work); +NAPI_EXTERN napi_status napi_cancel_async_work(napi_env env, + napi_async_work work); + +// version management +NAPI_EXTERN +napi_status napi_get_node_version(napi_env env, + const napi_node_version** version); + +#if NAPI_VERSION >= 2 + +// Return the current libuv event loop for a given environment +NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env, + struct uv_loop_s** loop); + +#endif // NAPI_VERSION >= 2 + +#if NAPI_VERSION >= 3 + +NAPI_EXTERN napi_status napi_fatal_exception(napi_env env, napi_value err); + +NAPI_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env, + void (*fun)(void* arg), + void* arg); + +NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env, + void (*fun)(void* arg), + void* arg); + +NAPI_EXTERN napi_status napi_open_callback_scope(napi_env env, + napi_value resource_object, + napi_async_context context, + napi_callback_scope* result); + +NAPI_EXTERN napi_status napi_close_callback_scope(napi_env env, + napi_callback_scope scope); + +#endif // NAPI_VERSION >= 3 + +#if NAPI_VERSION >= 4 + +#ifndef __wasm32__ +// Calling into JS from other threads +NAPI_EXTERN napi_status +napi_create_threadsafe_function(napi_env env, + napi_value func, + napi_value async_resource, + napi_value async_resource_name, + size_t max_queue_size, + size_t initial_thread_count, + void* thread_finalize_data, + napi_finalize thread_finalize_cb, + void* context, + napi_threadsafe_function_call_js call_js_cb, + napi_threadsafe_function* result); + +NAPI_EXTERN napi_status +napi_get_threadsafe_function_context(napi_threadsafe_function func, + void** result); + +NAPI_EXTERN napi_status +napi_call_threadsafe_function(napi_threadsafe_function func, + void* data, + napi_threadsafe_function_call_mode is_blocking); + +NAPI_EXTERN napi_status +napi_acquire_threadsafe_function(napi_threadsafe_function func); + +NAPI_EXTERN napi_status +napi_release_threadsafe_function(napi_threadsafe_function func, + napi_threadsafe_function_release_mode mode); + +NAPI_EXTERN napi_status +napi_unref_threadsafe_function(napi_env env, napi_threadsafe_function func); + +NAPI_EXTERN napi_status +napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func); +#endif // __wasm32__ + +#endif // NAPI_VERSION >= 4 + +#ifdef NAPI_EXPERIMENTAL + +NAPI_EXTERN napi_status napi_add_async_cleanup_hook( + napi_env env, + napi_async_cleanup_hook hook, + void* arg, + napi_async_cleanup_hook_handle* remove_handle); + +NAPI_EXTERN napi_status napi_remove_async_cleanup_hook( + napi_async_cleanup_hook_handle remove_handle); + +#endif // NAPI_EXPERIMENTAL + +EXTERN_C_END + +#endif // SRC_NODE_API_H_ diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/package-lock.json b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/package-lock.json new file mode 100644 index 00000000..5e9323cd --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "@callstackincubator/example-3", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@callstackincubator/example-3", + "version": "0.0.0", + "dependencies": { + "bindings": "~1.5.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + } + } +} diff --git a/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/package.json b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/package.json new file mode 100644 index 00000000..89ee8750 --- /dev/null +++ b/packages/node-addon-examples/examples/1-getting-started/2_function_arguments_twisted/napi/package.json @@ -0,0 +1,19 @@ +{ + "name": "@callstackincubator/example-3", + "version": "0.0.0", + "description": "Node.js Addons Example #2", + "main": "./index.js", + "private": true, + "dependencies": { + "bindings": "~1.5.0" + }, + "scripts": { + "test": "node index.js" + }, + "binary": { + "napi_versions": [4] + }, + "gypfile": true, + "readme": "ERROR: No README data found!", + "_id": "function_arguments@0.0.0" +} \ No newline at end of file diff --git a/packages/node-addon-examples/index.js b/packages/node-addon-examples/index.js index 31ac0c3a..df7a5606 100644 --- a/packages/node-addon-examples/index.js +++ b/packages/node-addon-examples/index.js @@ -5,6 +5,8 @@ module.exports = { "1_hello_world/node-addon-api-addon-class": () => require("./examples/1-getting-started/1_hello_world/node-addon-api-addon-class/hello.js"), "2_function_arguments/napi": () => require("./examples/1-getting-started/2_function_arguments/napi/addon.js"), "2_function_arguments/node-addon-api": () => require("./examples/1-getting-started/2_function_arguments/node-addon-api/addon.js"), + // "2_function_arguments_twisted/napi": () => require("@callstackincubator/example-3"), + "2_function_arguments_twisted/napi": () => require("./examples/1-getting-started/2_function_arguments_twisted/napi/index.js"), "3_callbacks/napi": () => require("./examples/1-getting-started/3_callbacks/napi/addon.js"), "3_callbacks/node-addon-api": () => require("./examples/1-getting-started/3_callbacks/node-addon-api/addon.js"), "4_object_factory/napi": () => require("./examples/1-getting-started/4_object_factory/napi/addon.js"),