Skip to content

Commit daf6686

Browse files
committed
Implement WeakNodeApiMultiHost generator
1 parent cf5e056 commit daf6686

File tree

5 files changed

+460
-0
lines changed

5 files changed

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

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)