Skip to content
Merged
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
96 changes: 15 additions & 81 deletions packages/weak-node-api/scripts/generate-weak-node-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import cp from "node:child_process";
Expand All @@ -7,6 +8,8 @@ import {
getNodeApiFunctions,
} from "../src/node-api-functions.js";

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

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

type GenerateFileOptions = {
Expand All @@ -20,10 +23,18 @@ async function generateFile({
fileName,
generator,
}: GenerateFileOptions) {
const output = generator(functions);
const generated = generator(functions);
const output = `// This file is generated - don't edit it directly\n\n${generated}`;
const outputPath = path.join(OUTPUT_PATH, fileName);
await fs.promises.writeFile(outputPath, output, "utf-8");
cp.spawnSync("clang-format", ["-i", outputPath], { stdio: "inherit" });
const { status, stderr = "No error output" } = cp.spawnSync(
"clang-format",
["-i", outputPath],
{
encoding: "utf8",
},
);
assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`);
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message will display 'undefined' when stderr is empty. The spawnSync options specify encoding: 'utf8', but when status is non-zero and stderr is empty, this will show an unhelpful message. Consider using stderr || 'no error output' or checking if stderr exists before interpolating it.

Suggested change
assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`);
assert.equal(status, 0, `Failed to format ${fileName}: ${stderr || "no error output"}`);

Copilot uses AI. Check for mistakes.
}

async function run() {
Expand All @@ -33,92 +44,15 @@ async function run() {
await generateFile({
functions,
fileName: "weak_node_api.hpp",
generator: generateHeader,
generator: weakNodeApiGenerator.generateHeader,
});
await generateFile({
functions,
fileName: "weak_node_api.cpp",
generator: generateSource,
generator: weakNodeApiGenerator.generateSource,
});
}

export function generateFunctionDecl({
returnType,
name,
argumentTypes,
}: FunctionDecl) {
return `${returnType} (*${name})(${argumentTypes.join(", ")});`;
}

/**
* Generates source code for a version script for the given Node API version.
*/
export function generateHeader(functions: FunctionDecl[]) {
return `
// This file is generated by react-native-node-api
#include <node_api.h> // Node-API
#include <stdio.h> // fprintf()
#include <stdlib.h> // abort()

// Ideally we would have just used NAPI_NO_RETURN, but
// __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct
// TODO: If we targeted C++23 we could use std::unreachable()

#if defined(__GNUC__)
#define WEAK_NODE_API_UNREACHABLE __builtin_unreachable();
#else
#define WEAK_NODE_API_UNREACHABLE __assume(0);
#endif

// Generate the struct of function pointers
struct WeakNodeApiHost {
${functions.map(generateFunctionDecl).join("\n")}
};
typedef void(*InjectHostFunction)(const WeakNodeApiHost&);
extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host);
`;
}

function generateFunctionImpl({
returnType,
name,
argumentTypes,
noReturn,
}: FunctionDecl) {
return `
extern "C" ${returnType} ${name}(
${argumentTypes.map((type, index) => `${type} arg${index}`).join(", ")}
) {
if (g_host.${name} == nullptr) {
fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");
abort();
}
${returnType === "void" ? "" : "return "} g_host.${name}(
${argumentTypes.map((_, index) => `arg${index}`).join(", ")}
);
${noReturn ? "WEAK_NODE_API_UNREACHABLE" : ""}
};
`;
}

/**
* Generates source code for a version script for the given Node API version.
*/
export function generateSource(functions: FunctionDecl[]) {
return `
// This file is generated by react-native-node-api
#include "weak_node_api.hpp" // Generated header

WeakNodeApiHost g_host;
void inject_weak_node_api_host(const WeakNodeApiHost& host) {
g_host = host;
};

// Generate function calling into the host
${functions.map(generateFunctionImpl).join("\n")}
`;
}

run().catch((err) => {
console.error(err);
process.exitCode = 1;
Expand Down
28 changes: 28 additions & 0 deletions packages/weak-node-api/scripts/generators/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { FunctionDecl } from "../../src/node-api-functions.js";

type FunctionOptions = FunctionDecl & {
extern?: true;
static?: true;
namespace?: string;
body?: string;
argumentNames?: string[];
};

export function generateFunction({
extern,
static: staticMember,
returnType,
namespace,
name,
argumentTypes,
argumentNames = [],
noReturn,
body,
}: FunctionOptions) {
return `
${staticMember ? "static " : ""}${extern ? 'extern "C" ' : ""}${returnType} ${namespace ? namespace + "::" : ""}${name}(
${argumentTypes.map((type, index) => `${type} ` + (argumentNames[index] ?? `arg${index}`)).join(", ")}
) ${body ? `{ ${body} ${noReturn ? "WEAK_NODE_API_UNREACHABLE;" : ""}\n}` : ""}
;
`;
}
74 changes: 74 additions & 0 deletions packages/weak-node-api/scripts/generators/weak-node-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { FunctionDecl } from "../../src/node-api-functions.js";
import { generateFunction } from "./shared.js";

export function generateFunctionDecl({
returnType,
name,
argumentTypes,
}: FunctionDecl) {
return `${returnType} (*${name})(${argumentTypes.join(", ")});`;
}

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

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The header lacks a #pragma once include guard comment explaining its purpose. The generated header should include a brief description of what this file provides (e.g., 'Weak Node-API host injection interface').

Suggested change
return `
return `
/**
* @file weak_node_api.hpp
* @brief Weak Node-API host injection interface.
*
* This header provides the struct and injection function for
* dynamically supplying Node-API function pointers to the host.
*
* #pragma once is used as an include guard to prevent multiple inclusion.
*/

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be adding this in a followup PR.

#pragma once
#include <node_api.h> // Node-API
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Corrected comment: 'Node-API' should be 'Node-API headers' for consistency with other include comments.

Suggested change
#include <node_api.h> // Node-API
#include <node_api.h> // Node-API headers

Copilot uses AI. Check for mistakes.
#include <stdio.h> // fprintf()
#include <stdlib.h> // abort()
// Ideally we would have just used NAPI_NO_RETURN, but
// __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct
// TODO: If we targeted C++23 we could use std::unreachable()
#if defined(__GNUC__)
#define WEAK_NODE_API_UNREACHABLE __builtin_unreachable()
#else
#define WEAK_NODE_API_UNREACHABLE __assume(0)
#endif
// Generate the struct of function pointers
struct WeakNodeApiHost {
${functions.map(generateFunctionDecl).join("\n")}
};
typedef void(*InjectHostFunction)(const WeakNodeApiHost&);
extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host);
`;
}

function generateFunctionImpl(fn: FunctionDecl) {
const { name, returnType, argumentTypes } = fn;
return generateFunction({
...fn,
extern: true,
body: `
if (g_host.${name} == nullptr) {
fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");
abort();
}
${returnType === "void" ? "" : "return "} g_host.${name}(
${argumentTypes.map((_, index) => `arg${index}`).join(", ")}
);
`,
});
}

/**
* Generates source code for a version script for the given Node API version.
*/
export function generateSource(functions: FunctionDecl[]) {
return `
#include "weak_node_api.hpp"
WeakNodeApiHost g_host;
void inject_weak_node_api_host(const WeakNodeApiHost& host) {
g_host = host;
};
// Generate function calling into the host
${functions.map(generateFunctionImpl).join("\n")}
`;
}
Loading