Skip to content

Commit 7d40f34

Browse files
committed
Implement WeakNodeApiMultiHost generator
1 parent a0163f5 commit 7d40f34

File tree

4 files changed

+277
-3
lines changed

4 files changed

+277
-3
lines changed

packages/weak-node-api/.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
/build-tests/
66
/*.xcframework
77
/*.android.node
8-
/generated/weak_node_api.cpp
9-
/generated/weak_node_api.hpp
8+
/generated/
109

1110
# Copied from node-api-headers by scripts/copy-node-api-headers.ts
1211
/include/

packages/weak-node-api/CMakeLists.txt

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

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,38 @@ import {
99
} from "../src/node-api-functions.js";
1010

1111
import * as weakNodeApiGenerator from "./generators/weak-node-api.js";
12+
import * as multiHostGenerator from "./generators/multi-host.js";
1213

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

1516
type GenerateFileOptions = {
1617
functions: FunctionDecl[];
1718
fileName: string;
1819
generator: (functions: FunctionDecl[]) => string;
20+
headingComment?: string;
1921
};
2022

2123
async function generateFile({
2224
functions,
2325
fileName,
2426
generator,
27+
headingComment = "",
2528
}: GenerateFileOptions) {
2629
const generated = generator(functions);
27-
const output = `// This file is generated - don't edit it directly\n\n${generated}`;
30+
const output = `
31+
/**
32+
* @file ${fileName}
33+
* ${headingComment
34+
.trim()
35+
.split("\n")
36+
.map((l) => l.trim())
37+
.join("\n* ")}
38+
*
39+
* @note This file is generated - don't edit it directly
40+
*/
41+
42+
${generated}
43+
`;
2844
const outputPath = path.join(OUTPUT_PATH, fileName);
2945
await fs.promises.writeFile(outputPath, output, "utf-8");
3046
const { status, stderr = "No error output" } = cp.spawnSync(
@@ -45,11 +61,41 @@ async function run() {
4561
functions,
4662
fileName: "weak_node_api.hpp",
4763
generator: weakNodeApiGenerator.generateHeader,
64+
headingComment: `
65+
@brief Weak Node-API host injection interface.
66+
67+
This header provides the struct and injection function for deferring Node-API function calls from addons into a Node-API host.
68+
`,
4869
});
4970
await generateFile({
5071
functions,
5172
fileName: "weak_node_api.cpp",
5273
generator: weakNodeApiGenerator.generateSource,
74+
headingComment: `
75+
@brief Weak Node-API host injection implementation.
76+
77+
Provides the implementation for deferring Node-API function calls from addons into a Node-API host.
78+
`,
79+
});
80+
await generateFile({
81+
functions,
82+
fileName: "weak_node_api_multi_host.hpp",
83+
generator: multiHostGenerator.generateHeader,
84+
headingComment: `
85+
@brief Weak Node-API multi-host injection interface.
86+
87+
This header provides the struct for deferring Node-API function calls from addons into multiple Node-API host implementations.
88+
`,
89+
});
90+
await generateFile({
91+
functions,
92+
fileName: "weak_node_api_multi_host.cpp",
93+
generator: multiHostGenerator.generateSource,
94+
headingComment: `
95+
@brief Weak Node-API multi-host injection implementation.
96+
97+
Provides the implementation for deferring Node-API function calls from addons into multiple Node-API host implementations.
98+
`,
5399
});
54100
}
55101

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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 <type_traits>
35+
#include <vector>
36+
#include <variant>
37+
38+
#include <node_api.h>
39+
#include "weak_node_api.hpp"
40+
41+
struct WeakNodeApiMultiHost : WeakNodeApiHost {
42+
template <typename T> struct Wrapped {
43+
static_assert(std::is_same<T, napi_env>::value ||
44+
std::is_same<T, napi_threadsafe_function>::value ||
45+
std::is_same<T, napi_async_cleanup_hook_handle>::value,
46+
"T must be either napi_env, node_api_basic_env, napi_threadsafe_function or napi_async_cleanup_hook_handle");
47+
T value;
48+
std::weak_ptr<WeakNodeApiHost> host;
49+
WeakNodeApiMultiHost *multi_host;
50+
};
51+
52+
napi_env wrap(napi_env value, std::weak_ptr<WeakNodeApiHost>);
53+
napi_threadsafe_function wrap(napi_threadsafe_function value, std::weak_ptr<WeakNodeApiHost>);
54+
napi_async_cleanup_hook_handle wrap(napi_async_cleanup_hook_handle value, std::weak_ptr<WeakNodeApiHost>);
55+
56+
WeakNodeApiMultiHost(
57+
void napi_module_register(napi_module *),
58+
void napi_fatal_error(const char *, size_t, const char *, size_t)
59+
);
60+
61+
private:
62+
std::vector<std::variant<std::unique_ptr<Wrapped<napi_env>>, std::unique_ptr<Wrapped<napi_threadsafe_function>>, std::unique_ptr<Wrapped<napi_async_cleanup_hook_handle>>>> wrapped_values;
63+
64+
public:
65+
66+
${functions.map(generateFunctionDecl).join("\n")}
67+
};
68+
`;
69+
}
70+
71+
function generateFunctionImpl(fn: FunctionDecl) {
72+
const { name, argumentTypes, returnType } = fn;
73+
const [firstArgument] = argumentTypes;
74+
const argumentNames =
75+
ARGUMENT_NAMES_PR_FUNCTION[name] ??
76+
argumentTypes.map((_, index) => `arg${index}`);
77+
if (name === "napi_fatal_error") {
78+
// Providing a default implementation
79+
return generateFunction({
80+
...fn,
81+
namespace: "WeakNodeApiMultiHost",
82+
argumentNames,
83+
body: `
84+
if (location && location_len) {
85+
fprintf(stderr, "Fatal Node-API error: %.*s %.*s",
86+
static_cast<int>(location_len),
87+
location,
88+
static_cast<int>(message_len),
89+
message
90+
);
91+
} else {
92+
fprintf(stderr, "Fatal Node-API error: %.*s", static_cast<int>(message_len), message);
93+
}
94+
abort();
95+
`,
96+
});
97+
} else if (name === "napi_module_register") {
98+
// Providing a default implementation
99+
return generateFunction({
100+
...fn,
101+
namespace: "WeakNodeApiMultiHost",
102+
argumentNames: [""],
103+
body: `
104+
fprintf(stderr, "napi_module_register is not implemented for this WeakNodeApiMultiHost");
105+
abort();
106+
`,
107+
});
108+
} else if (
109+
[
110+
"napi_env",
111+
"node_api_basic_env",
112+
// TODO: Wrap these on creation
113+
"napi_threadsafe_function",
114+
"napi_async_cleanup_hook_handle",
115+
].includes(firstArgument)
116+
) {
117+
const joinedArguments = argumentTypes
118+
.map((_, index) =>
119+
index === 0 ? "wrapped->value" : argumentNames[index],
120+
)
121+
.join(", ");
122+
123+
function generateCall() {
124+
if (name === "napi_create_threadsafe_function") {
125+
return `
126+
auto status = host->${name}(${joinedArguments});
127+
if (status == napi_status::napi_ok) {
128+
*${argumentNames[10]} = wrapped->multi_host->wrap(*${argumentNames[10]}, wrapped->host);
129+
}
130+
return status;
131+
`;
132+
} else if (name === "napi_add_async_cleanup_hook") {
133+
return `
134+
auto status = host->${name}(${joinedArguments});
135+
if (status == napi_status::napi_ok) {
136+
*${argumentNames[3]} = wrapped->multi_host->wrap(*${argumentNames[3]}, wrapped->host);
137+
}
138+
return status;
139+
`;
140+
} else {
141+
return `
142+
${returnType === "void" ? "" : "return"} host->${name}(${joinedArguments});
143+
`;
144+
}
145+
}
146+
147+
return generateFunction({
148+
...fn,
149+
namespace: "WeakNodeApiMultiHost",
150+
argumentNames,
151+
body: `
152+
auto wrapped = reinterpret_cast<Wrapped<${firstArgument}>*>(${argumentNames[0]});
153+
if (auto host = wrapped->host.lock()) {
154+
if (host->${name} == nullptr) {
155+
fprintf(stderr, "Node-API function '${name}' called on a host which doesn't provide an implementation\\n");
156+
return napi_status::napi_generic_failure;
157+
}
158+
${generateCall()}
159+
} else {
160+
fprintf(stderr, "Node-API function '${name}' called after host was destroyed.\\n");
161+
return napi_status::napi_generic_failure;
162+
}
163+
`,
164+
});
165+
} else {
166+
throw new Error(`Unexpected signature for '${name}' Node-API function`);
167+
}
168+
}
169+
170+
/**
171+
* Generates source code for a version script for the given Node API version.
172+
*/
173+
export function generateSource(functions: FunctionDecl[]) {
174+
return `
175+
#include "weak_node_api_multi_host.hpp"
176+
177+
WeakNodeApiMultiHost::WeakNodeApiMultiHost(
178+
void napi_module_register(napi_module *),
179+
void napi_fatal_error(const char *, size_t, const char *, size_t)
180+
)
181+
: WeakNodeApiHost({
182+
${functions
183+
.map(({ name }) => {
184+
if (
185+
name === "napi_module_register" ||
186+
name === "napi_fatal_error"
187+
) {
188+
// We take functions not taking a wrap-able argument via the constructor and call them directly
189+
return `.${name} = ${name} == nullptr ? WeakNodeApiMultiHost::${name} : ${name},`;
190+
} else {
191+
return `.${name} = WeakNodeApiMultiHost::${name},`;
192+
}
193+
})
194+
.join("\n")}
195+
}), wrapped_values{} {};
196+
197+
// TODO: Find a better way to delete these along the way
198+
199+
napi_env WeakNodeApiMultiHost::wrap(napi_env value,
200+
std::weak_ptr<WeakNodeApiHost> host) {
201+
auto ptr = std::make_unique<Wrapped<napi_env>>(value, host, this);
202+
auto raw_ptr = ptr.get();
203+
wrapped_values.push_back(std::move(ptr));
204+
return reinterpret_cast<napi_env>(raw_ptr);
205+
}
206+
207+
napi_threadsafe_function
208+
WeakNodeApiMultiHost::wrap(napi_threadsafe_function value,
209+
std::weak_ptr<WeakNodeApiHost> host) {
210+
auto ptr = std::make_unique<Wrapped<napi_threadsafe_function>>(value, host, this);
211+
auto raw_ptr = ptr.get();
212+
wrapped_values.push_back(std::move(ptr));
213+
return reinterpret_cast<napi_threadsafe_function>(raw_ptr);
214+
}
215+
216+
napi_async_cleanup_hook_handle
217+
WeakNodeApiMultiHost::wrap(napi_async_cleanup_hook_handle value,
218+
std::weak_ptr<WeakNodeApiHost> host) {
219+
auto ptr = std::make_unique<Wrapped<napi_async_cleanup_hook_handle>>(value, host, this);
220+
auto raw_ptr = ptr.get();
221+
wrapped_values.push_back(std::move(ptr));
222+
return reinterpret_cast<napi_async_cleanup_hook_handle>(raw_ptr);
223+
}
224+
225+
${functions.map(generateFunctionImpl).join("\n")}
226+
`;
227+
}

0 commit comments

Comments
 (0)