Skip to content

Commit 4d78b71

Browse files
committed
Implement WeakNodeApiMultiHost generator
1 parent b0fa9ea commit 4d78b71

File tree

5 files changed

+466
-0
lines changed

5 files changed

+466
-0
lines changed

packages/weak-node-api/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ set(GENERATED_SOURCE_DIR "generated")
1313
target_sources(${PROJECT_NAME}
1414
PUBLIC
1515
${GENERATED_SOURCE_DIR}/weak_node_api.cpp
16+
${GENERATED_SOURCE_DIR}/NodeApiMultiHost.cpp
1617
PUBLIC FILE_SET HEADERS
1718
BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES
1819
${GENERATED_SOURCE_DIR}/weak_node_api.hpp
1920
${GENERATED_SOURCE_DIR}/NodeApiHost.hpp
21+
${GENERATED_SOURCE_DIR}/NodeApiMultiHost.hpp
2022
${INCLUDE_DIR}/js_native_api_types.h
2123
${INCLUDE_DIR}/js_native_api.h
2224
${INCLUDE_DIR}/node_api_types.h

packages/weak-node-api/scripts/generate.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010

1111
import * as weakNodeApiGenerator from "./generators/weak-node-api.js";
1212
import * as hostGenerator from "./generators/NodeApiHost.js";
13+
import * as multiHostGenerator from "./generators/NodeApiMultiHost.js";
1314

1415
export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated");
1516

@@ -87,6 +88,26 @@ async function run() {
8788
Provides the implementation for deferring Node-API function calls from addons into a Node-API host.
8889
`,
8990
});
91+
await generateFile({
92+
functions,
93+
fileName: "NodeApiMultiHost.hpp",
94+
generator: multiHostGenerator.generateHeader,
95+
headingComment: `
96+
@brief Weak Node-API multi-host injection interface.
97+
98+
This header provides the struct for deferring Node-API function calls from addons into multiple Node-API host implementations.
99+
`,
100+
});
101+
await generateFile({
102+
functions,
103+
fileName: "NodeApiMultiHost.cpp",
104+
generator: multiHostGenerator.generateSource,
105+
headingComment: `
106+
@brief Weak Node-API multi-host injection implementation.
107+
108+
Provides the implementation for deferring Node-API function calls from addons into multiple Node-API host implementations.
109+
`,
110+
});
90111
}
91112

92113
run().catch((err) => {
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import type { FunctionDecl } from "../../src/node-api-functions.js";
2+
import { generateFunction } from "./shared.js";
3+
4+
const ARGUMENT_NAMES_PR_FUNCTION: Record<string, undefined | string[]> = {
5+
napi_create_threadsafe_function: [
6+
"env",
7+
"func",
8+
"async_resource",
9+
"async_resource_name",
10+
"max_queue_size",
11+
"initial_thread_count",
12+
"thread_finalize_data",
13+
"thread_finalize_cb",
14+
"context",
15+
"call_js_cb",
16+
"result",
17+
],
18+
napi_add_async_cleanup_hook: ["env", "hook", "arg", "remove_handle"],
19+
napi_fatal_error: ["location", "location_len", "message", "message_len"],
20+
};
21+
22+
export function generateFunctionDecl(fn: FunctionDecl) {
23+
return generateFunction({ ...fn, static: true });
24+
}
25+
26+
/**
27+
* Generates source code for a version script for the given Node API version.
28+
*/
29+
export function generateHeader(functions: FunctionDecl[]) {
30+
return `
31+
#pragma once
32+
33+
#include <memory>
34+
#include <vector>
35+
36+
#include <node_api.h>
37+
38+
#include "NodeApiHost.hpp"
39+
40+
struct NodeApiMultiHost : public NodeApiHost {
41+
42+
struct WrappedEnv;
43+
44+
struct WrappedThreadsafeFunction {
45+
napi_threadsafe_function value;
46+
WrappedEnv *env;
47+
std::weak_ptr<NodeApiHost> host;
48+
};
49+
50+
struct WrappedAsyncCleanupHookHandle {
51+
napi_async_cleanup_hook_handle value;
52+
WrappedEnv *env;
53+
std::weak_ptr<NodeApiHost> host;
54+
};
55+
56+
struct WrappedEnv {
57+
WrappedEnv(napi_env &&value, std::weak_ptr<NodeApiHost> &&host);
58+
59+
napi_env value;
60+
std::weak_ptr<NodeApiHost> host;
61+
62+
private:
63+
std::vector<std::unique_ptr<WrappedThreadsafeFunction>>
64+
threadsafe_functions;
65+
std::vector<std::unique_ptr<WrappedAsyncCleanupHookHandle>>
66+
async_cleanup_hook_handles;
67+
68+
public:
69+
napi_threadsafe_function wrap(napi_threadsafe_function value,
70+
WrappedEnv *env,
71+
std::weak_ptr<NodeApiHost>);
72+
napi_async_cleanup_hook_handle wrap(napi_async_cleanup_hook_handle value,
73+
WrappedEnv *env,
74+
std::weak_ptr<NodeApiHost>);
75+
};
76+
77+
napi_env wrap(napi_env value, std::weak_ptr<NodeApiHost>);
78+
79+
NodeApiMultiHost(
80+
void napi_module_register(napi_module *),
81+
void napi_fatal_error(const char *, size_t, const char *, size_t)
82+
);
83+
84+
private:
85+
std::vector<std::unique_ptr<WrappedEnv>> envs;
86+
87+
public:
88+
89+
${functions.map(generateFunctionDecl).join("\n")}
90+
};
91+
`;
92+
}
93+
94+
function generateFunctionImpl(fn: FunctionDecl) {
95+
const { name, argumentTypes, returnType } = fn;
96+
const [firstArgument] = argumentTypes;
97+
const argumentNames =
98+
ARGUMENT_NAMES_PR_FUNCTION[name] ??
99+
argumentTypes.map((_, index) => `arg${index}`);
100+
if (name === "napi_fatal_error") {
101+
// Providing a default implementation
102+
return generateFunction({
103+
...fn,
104+
namespace: "NodeApiMultiHost",
105+
argumentNames,
106+
body: `
107+
if (location && location_len) {
108+
fprintf(stderr, "Fatal Node-API error: %.*s %.*s",
109+
static_cast<int>(location_len),
110+
location,
111+
static_cast<int>(message_len),
112+
message
113+
);
114+
} else {
115+
fprintf(stderr, "Fatal Node-API error: %.*s", static_cast<int>(message_len), message);
116+
}
117+
abort();
118+
`,
119+
});
120+
} else if (name === "napi_module_register") {
121+
// Providing a default implementation
122+
return generateFunction({
123+
...fn,
124+
namespace: "NodeApiMultiHost",
125+
argumentNames: [""],
126+
body: `
127+
fprintf(stderr, "napi_module_register is not implemented for this NodeApiMultiHost");
128+
abort();
129+
`,
130+
});
131+
} else if (
132+
[
133+
"napi_env",
134+
"node_api_basic_env",
135+
"napi_threadsafe_function",
136+
"napi_async_cleanup_hook_handle",
137+
].includes(firstArgument)
138+
) {
139+
const joinedArguments = argumentTypes
140+
.map((_, index) =>
141+
index === 0 ? "wrapped->value" : argumentNames[index],
142+
)
143+
.join(", ");
144+
145+
function generateCall() {
146+
if (name === "napi_create_threadsafe_function") {
147+
return `
148+
auto status = host->${name}(${joinedArguments});
149+
if (status == napi_status::napi_ok) {
150+
*${argumentNames[10]} = wrapped->wrap(*${argumentNames[10]}, wrapped, wrapped->host);
151+
}
152+
return status;
153+
`;
154+
} else if (name === "napi_add_async_cleanup_hook") {
155+
return `
156+
auto status = host->${name}(${joinedArguments});
157+
if (status == napi_status::napi_ok) {
158+
*${argumentNames[3]} = wrapped->wrap(*${argumentNames[3]}, wrapped, wrapped->host);
159+
}
160+
return status;
161+
`;
162+
} else {
163+
return `
164+
${returnType === "void" ? "" : "return"} host->${name}(${joinedArguments});
165+
`;
166+
}
167+
}
168+
169+
function getWrappedType(nodeApiType: string) {
170+
if (nodeApiType === "napi_env" || nodeApiType === "node_api_basic_env") {
171+
return "WrappedEnv";
172+
} else if (nodeApiType === "napi_threadsafe_function") {
173+
return "WrappedThreadsafeFunction";
174+
} else if (nodeApiType === "napi_async_cleanup_hook_handle") {
175+
return "WrappedAsyncCleanupHookHandle";
176+
} else {
177+
throw new Error(`Unexpected Node-API type '${nodeApiType}'`);
178+
}
179+
}
180+
181+
return generateFunction({
182+
...fn,
183+
namespace: "NodeApiMultiHost",
184+
argumentNames,
185+
body: `
186+
auto wrapped = reinterpret_cast<${getWrappedType(firstArgument)}*>(${argumentNames[0]});
187+
if (auto host = wrapped->host.lock()) {
188+
if (host->${name} == nullptr) {
189+
fprintf(stderr, "Node-API function '${name}' called on a host which doesn't provide an implementation\\n");
190+
return napi_status::napi_generic_failure;
191+
}
192+
${generateCall()}
193+
} else {
194+
fprintf(stderr, "Node-API function '${name}' called after host was destroyed.\\n");
195+
return napi_status::napi_generic_failure;
196+
}
197+
`,
198+
});
199+
} else {
200+
throw new Error(`Unexpected signature for '${name}' Node-API function`);
201+
}
202+
}
203+
204+
/**
205+
* Generates source code for a version script for the given Node API version.
206+
*/
207+
export function generateSource(functions: FunctionDecl[]) {
208+
return `
209+
#include "NodeApiMultiHost.hpp"
210+
211+
NodeApiMultiHost::NodeApiMultiHost(
212+
void napi_module_register(napi_module *),
213+
void napi_fatal_error(const char *, size_t, const char *, size_t)
214+
)
215+
: NodeApiHost({
216+
${functions
217+
.map(({ name }) => {
218+
if (
219+
name === "napi_module_register" ||
220+
name === "napi_fatal_error"
221+
) {
222+
// We take functions not taking a wrap-able argument via the constructor and call them directly
223+
return `.${name} = ${name} == nullptr ? NodeApiMultiHost::${name} : ${name},`;
224+
} else {
225+
return `.${name} = NodeApiMultiHost::${name},`;
226+
}
227+
})
228+
.join("\n")}
229+
}), envs{} {};
230+
231+
// TODO: Ensure the Node-API functions aren't throwing (using NOEXCEPT)
232+
// TODO: Find a better way to delete these along the way
233+
234+
NodeApiMultiHost::WrappedEnv::WrappedEnv(napi_env &&value,
235+
std::weak_ptr<NodeApiHost> &&host)
236+
: value(value), host(host), threadsafe_functions{},
237+
async_cleanup_hook_handles{} {}
238+
239+
napi_env NodeApiMultiHost::wrap(napi_env value,
240+
std::weak_ptr<NodeApiHost> host) {
241+
auto ptr = std::make_unique<WrappedEnv>(std::move(value), std::move(host));
242+
auto raw_ptr = ptr.get();
243+
envs.push_back(std::move(ptr));
244+
return reinterpret_cast<napi_env>(raw_ptr);
245+
}
246+
247+
napi_threadsafe_function
248+
NodeApiMultiHost::WrappedEnv::wrap(napi_threadsafe_function value,
249+
WrappedEnv *env,
250+
std::weak_ptr<NodeApiHost> weak_host) {
251+
auto ptr = std::make_unique<WrappedThreadsafeFunction>(value, env, weak_host);
252+
auto raw_ptr = ptr.get();
253+
env->threadsafe_functions.push_back(std::move(ptr));
254+
return reinterpret_cast<napi_threadsafe_function>(raw_ptr);
255+
}
256+
257+
napi_async_cleanup_hook_handle
258+
NodeApiMultiHost::WrappedEnv::wrap(napi_async_cleanup_hook_handle value,
259+
WrappedEnv *env,
260+
std::weak_ptr<NodeApiHost> weak_host) {
261+
auto ptr = std::make_unique<WrappedAsyncCleanupHookHandle>(value, env, weak_host);
262+
auto raw_ptr = ptr.get();
263+
env->async_cleanup_hook_handles.push_back(std::move(ptr));
264+
return reinterpret_cast<napi_async_cleanup_hook_handle>(raw_ptr);
265+
}
266+
267+
${functions.map(generateFunctionImpl).join("\n")}
268+
`;
269+
}

packages/weak-node-api/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ FetchContent_MakeAvailable(Catch2)
1010

1111
add_executable(weak-node-api-tests
1212
test_inject.cpp
13+
test_multi_host.cpp
1314
)
1415
target_link_libraries(weak-node-api-tests
1516
PRIVATE

0 commit comments

Comments
 (0)