Skip to content

Commit 0aaf8d7

Browse files
author
Mariusz Pasinski
committed
feat: draft first implementation of AddonRegistry
This change implements the AddonRegistry by recycling as much code from CxxNodeApiHostModule as possible. It solves #4 and partially #30
1 parent a86efdf commit 0aaf8d7

File tree

2 files changed

+235
-0
lines changed

2 files changed

+235
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#include "AddonRegistry.hpp"
2+
#include <cctype> // for std::isalnum
3+
4+
#ifndef NODE_API_DEFAULT_MODULE_API_VERSION
5+
#define NODE_API_DEFAULT_MODULE_API_VERSION 8
6+
#endif
7+
8+
using namespace facebook;
9+
10+
namespace {
11+
napi_status napi_emplace_named_property_object(napi_env env,
12+
napi_value object,
13+
const char *utf8Name,
14+
napi_value *outObject) {
15+
bool propertyFound = false;
16+
napi_status status = napi_has_named_property(env, object, utf8Name, &propertyFound);
17+
assert(napi_ok == status);
18+
19+
assert(nullptr != outObject);
20+
if (propertyFound) {
21+
status = napi_get_named_property(env, object, utf8Name, outObject);
22+
} else {
23+
// Need to create it first
24+
status = napi_create_object(env, outObject);
25+
assert(napi_ok == status);
26+
27+
status = napi_set_named_property(env, object, utf8Name, *outObject);
28+
}
29+
30+
return status;
31+
}
32+
33+
void sanitizeLibraryNameInplace(std::string &name) {
34+
#if USING_PATCHED_BABEL_PLUGIN
35+
// Strip the extension (if present)
36+
// NOTE: This is needed when working with updated Babel plugin
37+
if (auto pos = name.find(".node"); std::string::npos != pos) {
38+
name = name.substr(0, pos);
39+
}
40+
#endif
41+
42+
for (char &c : name) {
43+
if (!std::isalnum(c)) {
44+
c = '-';
45+
}
46+
}
47+
}
48+
} // namespace
49+
50+
namespace callstack::nodeapihost {
51+
52+
AddonRegistry::NodeAddon& AddonRegistry::loadAddon(std::string packageName,
53+
std::string subpath) {
54+
const std::string fqan = packageName + subpath.substr(1);
55+
auto [it, inserted] =
56+
trackedAddons_.try_emplace(fqan, NodeAddon(packageName, subpath));
57+
NodeAddon &addon = it->second;
58+
59+
sanitizeLibraryNameInplace(packageName);
60+
sanitizeLibraryNameInplace(subpath);
61+
const std::string libraryName = packageName + subpath;
62+
63+
if (inserted || !it->second.isLoaded()) {
64+
#if defined(__APPLE__)
65+
const std::string libraryPath = "@rpath/" + libraryName + ".framework/" + libraryName;
66+
tryLoadAddonAsDynamicLib(addon, libraryPath);
67+
#elif defined(__ANDROID__)
68+
const std::string libraryPath = "lib" + libraryName + ".so";
69+
tryLoadAddonAsDynamicLib(addon, libraryPath);
70+
#else
71+
abort();
72+
#endif
73+
}
74+
75+
return addon;
76+
}
77+
78+
bool AddonRegistry::tryLoadAddonAsDynamicLib(NodeAddon &addon, const std::string &path) {
79+
// Load addon as dynamic library
80+
typename LoaderPolicy::Module library = LoaderPolicy::loadLibrary(path.c_str());
81+
if (nullptr != library) {
82+
// pending addon remains empty, we should look for the symbols...
83+
typename LoaderPolicy::Symbol initFn = LoaderPolicy::getSymbol(library, "napi_register_module_v1");
84+
if (nullptr != initFn) {
85+
addon.initFun_ = (napi_addon_register_func)initFn;
86+
addon.moduleApiVersion_ = NODE_API_DEFAULT_MODULE_API_VERSION;
87+
// This solves https://github.com/callstackincubator/react-native-node-api-modules/issues/4
88+
typename LoaderPolicy::Symbol getVersionFn = LoaderPolicy::getSymbol(library, "node_api_module_get_api_version_v1");
89+
if (nullptr != getVersionFn) {
90+
addon.moduleApiVersion_ = ((node_api_addon_get_api_version_func)getVersionFn)();
91+
}
92+
}
93+
94+
if (nullptr != addon.initFun_) {
95+
addon.moduleHandle_ = (void *)library;
96+
addon.loadedFilePath_ = path;
97+
}
98+
}
99+
}
100+
101+
jsi::Value AddonRegistry::instantiateAddonInRuntime(jsi::Runtime &rt, NodeAddon &addon) {
102+
// We should check if the module has already been initialized
103+
assert(true == addon.isLoaded());
104+
assert(addon.moduleApiVersion_ > 0 && addon.moduleApiVersion_ <= 10);
105+
106+
napi_status status = napi_ok;
107+
napi_env env = reinterpret_cast<napi_env>(rt.createNodeApiEnv(addon.moduleApiVersion_));
108+
109+
// Create the "exports" object
110+
napi_value exports;
111+
status = napi_create_object(env, &exports);
112+
assert(napi_ok == status);
113+
114+
// Call the addon init function to populate the "exports" object
115+
// Allowing it to replace the value entirely by its return value
116+
// TODO: Check the return value (see Node.js specs)
117+
exports = addon.initFun_(env, exports);
118+
119+
// "Compute" the Fully Qualified Addon Path
120+
const std::string fqap = addon.packageName_ + addon.subpath_.substr(1);
121+
122+
{
123+
napi_value descriptor;
124+
status = createAddonDescriptor(env, exports, &descriptor);
125+
assert(napi_ok == status);
126+
127+
napi_value global;
128+
napi_get_global(env, &global);
129+
assert(napi_ok == status);
130+
131+
status = storeAddonByFullPath(env, global, fqap, descriptor);
132+
assert(napi_ok == status);
133+
}
134+
135+
return lookupAddonByFullPath(rt, fqap);
136+
}
137+
138+
napi_status AddonRegistry::createAddonDescriptor(napi_env env, napi_value exports, napi_value *outDescriptor) {
139+
// Create the descriptor object
140+
assert(nullptr != outDescriptor);
141+
napi_status status = napi_create_object(env, outDescriptor);
142+
143+
// Point the `env` property to the current `napi_env`
144+
if (napi_ok == status) {
145+
napi_value env_value;
146+
status = napi_create_external(env, env, nullptr, nullptr, &env_value);
147+
if (napi_ok == status) {
148+
status = napi_set_named_property(env, *outDescriptor, "env", env_value);
149+
}
150+
}
151+
152+
// Cache the addons exports in descriptor's `exports` property
153+
if (napi_ok == status) {
154+
status = napi_set_named_property(env, *outDescriptor, "exports", exports);
155+
}
156+
157+
return status;
158+
}
159+
160+
napi_status AddonRegistry::storeAddonByFullPath(napi_env env, napi_value global, const std::string &fqap, napi_value descriptor) {
161+
// Get the internal registry object
162+
napi_value registryObject;
163+
napi_status status = napi_emplace_named_property_object(env, global, kInternalRegistryKey, &registryObject);
164+
assert(napi_ok == status);
165+
166+
status = napi_set_named_property(env, registryObject, fqap.c_str(), descriptor);
167+
return status;
168+
}
169+
170+
jsi::Value AddonRegistry::lookupAddonByFullPath(jsi::Runtime &rt, const std::string &fqap) {
171+
// Get the internal registry object
172+
jsi::Object global = rt.global();
173+
if (!global.hasProperty(rt, kInternalRegistryKey)) {
174+
// Create it first
175+
jsi::Object registryObject = jsi::Object(rt);
176+
global.setProperty(rt, kInternalRegistryKey, registryObject);
177+
}
178+
jsi::Value registryValue = global.getProperty(rt, kInternalRegistryKey);
179+
jsi::Object registryObject = registryValue.asObject(rt);
180+
181+
// Lookup by addon path
182+
jsi::Value addonValue(nullptr);
183+
if (registryObject.hasProperty(rt, fqap.c_str())) {
184+
addonValue = registryObject.getProperty(rt, fqap.c_str());
185+
}
186+
return addonValue;
187+
}
188+
189+
} // namespace callstack::nodeapihost
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
#include <unordered_map>
4+
#include <jsi/jsi.h>
5+
#include <node_api.h>
6+
#include "AddonLoaders.hpp"
7+
8+
// HACK: Feature flag that enables backwards-compatible code until PR is finished
9+
#define USING_PATCHED_BABEL_PLUGIN 1
10+
11+
namespace callstack::nodeapihost {
12+
13+
class AddonRegistry {
14+
public:
15+
struct NodeAddon {
16+
NodeAddon(std::string packageName, std::string subpath)
17+
: packageName_(packageName)
18+
, subpath_(subpath)
19+
{}
20+
21+
inline bool isLoaded() const { return nullptr != initFun_; }
22+
23+
std::string packageName_;
24+
std::string subpath_;
25+
std::string loadedFilePath_;
26+
void *moduleHandle_ = nullptr;
27+
napi_addon_register_func initFun_ = nullptr;
28+
int32_t moduleApiVersion_;
29+
};
30+
31+
NodeAddon& loadAddon(std::string packageName, std::string subpath);
32+
facebook::jsi::Value instantiateAddonInRuntime(facebook::jsi::Runtime &rt, NodeAddon &addon);
33+
34+
using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround
35+
// for my lazyness (works on iOS and Android)
36+
private:
37+
bool tryLoadAddonAsDynamicLib(NodeAddon &addon, const std::string &path);
38+
napi_status createAddonDescriptor(napi_env env, napi_value exports, napi_value *outDescriptor);
39+
napi_status storeAddonByFullPath(napi_env env, napi_value global, const std::string &fqap, napi_value descriptor);
40+
facebook::jsi::Value lookupAddonByFullPath(facebook::jsi::Runtime &rt, const std::string &fqap);
41+
42+
static constexpr const char *kInternalRegistryKey = "$NodeApiHost";
43+
std::unordered_map<std::string, NodeAddon> trackedAddons_;
44+
};
45+
46+
} // namespace callstack::nodeapihost

0 commit comments

Comments
 (0)