Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/weak-node-api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
/build-tests/
/*.xcframework
/*.android.node
/generated/weak_node_api.cpp
/generated/weak_node_api.hpp
/generated/

# Copied from node-api-headers by scripts/copy-node-api-headers.ts
/include/
Expand Down
2 changes: 2 additions & 0 deletions packages/weak-node-api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ set(GENERATED_SOURCE_DIR "generated")
target_sources(${PROJECT_NAME}
PUBLIC
${GENERATED_SOURCE_DIR}/weak_node_api.cpp
${GENERATED_SOURCE_DIR}/weak_node_api_multi_host.cpp
PUBLIC FILE_SET HEADERS
BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES
${GENERATED_SOURCE_DIR}/weak_node_api.hpp
${GENERATED_SOURCE_DIR}/weak_node_api_multi_host.hpp
${INCLUDE_DIR}/js_native_api_types.h
${INCLUDE_DIR}/js_native_api.h
${INCLUDE_DIR}/node_api_types.h
Expand Down
48 changes: 47 additions & 1 deletion packages/weak-node-api/scripts/generate-weak-node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,38 @@ import {
} from "../src/node-api-functions.js";

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

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

type GenerateFileOptions = {
functions: FunctionDecl[];
fileName: string;
generator: (functions: FunctionDecl[]) => string;
headingComment?: string;
};

async function generateFile({
functions,
fileName,
generator,
headingComment = "",
}: GenerateFileOptions) {
const generated = generator(functions);
const output = `// This file is generated - don't edit it directly\n\n${generated}`;
const output = `
/**
* @file ${fileName}
* ${headingComment
.trim()
.split("\n")
.map((l) => l.trim())
.join("\n* ")}
*
* @note This file is generated - don't edit it directly
*/

${generated}
`;
const outputPath = path.join(OUTPUT_PATH, fileName);
await fs.promises.writeFile(outputPath, output, "utf-8");
const { status, stderr = "No error output" } = cp.spawnSync(
Expand All @@ -45,11 +61,41 @@ async function run() {
functions,
fileName: "weak_node_api.hpp",
generator: weakNodeApiGenerator.generateHeader,
headingComment: `
@brief Weak Node-API host injection interface.

This header provides the struct and injection function for deferring Node-API function calls from addons into a Node-API host.
`,
});
await generateFile({
functions,
fileName: "weak_node_api.cpp",
generator: weakNodeApiGenerator.generateSource,
headingComment: `
@brief Weak Node-API host injection implementation.

Provides the implementation for deferring Node-API function calls from addons into a Node-API host.
`,
});
await generateFile({
functions,
fileName: "weak_node_api_multi_host.hpp",
generator: multiHostGenerator.generateHeader,
headingComment: `
@brief Weak Node-API multi-host injection interface.

This header provides the struct for deferring Node-API function calls from addons into multiple Node-API host implementations.
`,
});
await generateFile({
functions,
fileName: "weak_node_api_multi_host.cpp",
generator: multiHostGenerator.generateSource,
headingComment: `
@brief Weak Node-API multi-host injection implementation.

Provides the implementation for deferring Node-API function calls from addons into multiple Node-API host implementations.
`,
});
}

Expand Down
227 changes: 227 additions & 0 deletions packages/weak-node-api/scripts/generators/multi-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import type { FunctionDecl } from "../../src/node-api-functions.js";
import { generateFunction } from "./shared.js";

const ARGUMENT_NAMES_PR_FUNCTION: Record<string, undefined | string[]> = {
napi_create_threadsafe_function: [
"env",
"func",
"async_resource",
"async_resource_name",
"max_queue_size",
"initial_thread_count",
"thread_finalize_data",
"thread_finalize_cb",
"context",
"call_js_cb",
"result",
],
napi_add_async_cleanup_hook: ["env", "hook", "arg", "remove_handle"],
napi_fatal_error: ["location", "location_len", "message", "message_len"],
};

export function generateFunctionDecl(fn: FunctionDecl) {
return generateFunction({ ...fn, static: true });
}

/**
* Generates source code for a version script for the given Node API version.
*/
export function generateHeader(functions: FunctionDecl[]) {
return `
#pragma once

#include <memory>
#include <type_traits>
#include <vector>
#include <variant>

#include <node_api.h>
#include "weak_node_api.hpp"

struct WeakNodeApiMultiHost : WeakNodeApiHost {
template <typename T> struct Wrapped {
static_assert(std::is_same<T, napi_env>::value ||
std::is_same<T, napi_threadsafe_function>::value ||
std::is_same<T, napi_async_cleanup_hook_handle>::value,
"T must be either napi_env, node_api_basic_env, napi_threadsafe_function or napi_async_cleanup_hook_handle");
T value;
std::weak_ptr<WeakNodeApiHost> host;
WeakNodeApiMultiHost *multi_host;
};

napi_env wrap(napi_env value, std::weak_ptr<WeakNodeApiHost>);
napi_threadsafe_function wrap(napi_threadsafe_function value, std::weak_ptr<WeakNodeApiHost>);
napi_async_cleanup_hook_handle wrap(napi_async_cleanup_hook_handle value, std::weak_ptr<WeakNodeApiHost>);

WeakNodeApiMultiHost(
void napi_module_register(napi_module *),
void napi_fatal_error(const char *, size_t, const char *, size_t)
);

private:
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;

public:

${functions.map(generateFunctionDecl).join("\n")}
};
`;
}

function generateFunctionImpl(fn: FunctionDecl) {
const { name, argumentTypes, returnType } = fn;
const [firstArgument] = argumentTypes;
const argumentNames =
ARGUMENT_NAMES_PR_FUNCTION[name] ??
argumentTypes.map((_, index) => `arg${index}`);
if (name === "napi_fatal_error") {
// Providing a default implementation
return generateFunction({
...fn,
namespace: "WeakNodeApiMultiHost",
argumentNames,
body: `
if (location && location_len) {
fprintf(stderr, "Fatal Node-API error: %.*s %.*s",
static_cast<int>(location_len),
location,
static_cast<int>(message_len),
message
);
} else {
fprintf(stderr, "Fatal Node-API error: %.*s", static_cast<int>(message_len), message);
}
abort();
`,
});
} else if (name === "napi_module_register") {
// Providing a default implementation
return generateFunction({
...fn,
namespace: "WeakNodeApiMultiHost",
argumentNames: [""],
body: `
fprintf(stderr, "napi_module_register is not implemented for this WeakNodeApiMultiHost");
abort();
`,
});
} else if (
[
"napi_env",
"node_api_basic_env",
// TODO: Wrap these on creation
"napi_threadsafe_function",
"napi_async_cleanup_hook_handle",
].includes(firstArgument)
) {
const joinedArguments = argumentTypes
.map((_, index) =>
index === 0 ? "wrapped->value" : argumentNames[index],
)
.join(", ");

function generateCall() {
if (name === "napi_create_threadsafe_function") {
return `
auto status = host->${name}(${joinedArguments});
if (status == napi_status::napi_ok) {
*${argumentNames[10]} = wrapped->multi_host->wrap(*${argumentNames[10]}, wrapped->host);
}
return status;
`;
} else if (name === "napi_add_async_cleanup_hook") {
return `
auto status = host->${name}(${joinedArguments});
if (status == napi_status::napi_ok) {
*${argumentNames[3]} = wrapped->multi_host->wrap(*${argumentNames[3]}, wrapped->host);
}
return status;
`;
} else {
return `
${returnType === "void" ? "" : "return"} host->${name}(${joinedArguments});
`;
}
}

return generateFunction({
...fn,
namespace: "WeakNodeApiMultiHost",
argumentNames,
body: `
auto wrapped = reinterpret_cast<Wrapped<${firstArgument}>*>(${argumentNames[0]});
if (auto host = wrapped->host.lock()) {
if (host->${name} == nullptr) {
fprintf(stderr, "Node-API function '${name}' called on a host which doesn't provide an implementation\\n");
return napi_status::napi_generic_failure;
}
${generateCall()}
} else {
fprintf(stderr, "Node-API function '${name}' called after host was destroyed.\\n");
return napi_status::napi_generic_failure;
}
`,
});
} else {
throw new Error(`Unexpected signature for '${name}' Node-API function`);
}
}

/**
* Generates source code for a version script for the given Node API version.
*/
export function generateSource(functions: FunctionDecl[]) {
return `
#include "weak_node_api_multi_host.hpp"

WeakNodeApiMultiHost::WeakNodeApiMultiHost(
void napi_module_register(napi_module *),
void napi_fatal_error(const char *, size_t, const char *, size_t)
)
: WeakNodeApiHost({
${functions
.map(({ name }) => {
if (
name === "napi_module_register" ||
name === "napi_fatal_error"
) {
// We take functions not taking a wrap-able argument via the constructor and call them directly
return `.${name} = ${name} == nullptr ? WeakNodeApiMultiHost::${name} : ${name},`;
} else {
return `.${name} = WeakNodeApiMultiHost::${name},`;
}
})
.join("\n")}
}), wrapped_values{} {};

// TODO: Find a better way to delete these along the way

napi_env WeakNodeApiMultiHost::wrap(napi_env value,
std::weak_ptr<WeakNodeApiHost> host) {
auto ptr = std::make_unique<Wrapped<napi_env>>(value, host, this);
auto raw_ptr = ptr.get();
wrapped_values.push_back(std::move(ptr));
return reinterpret_cast<napi_env>(raw_ptr);
}

napi_threadsafe_function
WeakNodeApiMultiHost::wrap(napi_threadsafe_function value,
std::weak_ptr<WeakNodeApiHost> host) {
auto ptr = std::make_unique<Wrapped<napi_threadsafe_function>>(value, host, this);
auto raw_ptr = ptr.get();
wrapped_values.push_back(std::move(ptr));
return reinterpret_cast<napi_threadsafe_function>(raw_ptr);
}

napi_async_cleanup_hook_handle
WeakNodeApiMultiHost::wrap(napi_async_cleanup_hook_handle value,
std::weak_ptr<WeakNodeApiHost> host) {
auto ptr = std::make_unique<Wrapped<napi_async_cleanup_hook_handle>>(value, host, this);
auto raw_ptr = ptr.get();
wrapped_values.push_back(std::move(ptr));
return reinterpret_cast<napi_async_cleanup_hook_handle>(raw_ptr);
}

${functions.map(generateFunctionImpl).join("\n")}
`;
}
1 change: 1 addition & 0 deletions packages/weak-node-api/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ FetchContent_MakeAvailable(Catch2)

add_executable(weak-node-api-tests
test_inject.cpp
test_multi_host.cpp
)
target_link_libraries(weak-node-api-tests
PRIVATE
Expand Down
Loading
Loading