xbintsc 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/README.zh-CN.md +28 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +13 -1
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/driver/compiler.js +22 -4
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/extensions/native.d.ts +92 -0
- package/dist/src/extensions/native.js +217 -0
- package/dist/src/extensions/native.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +13 -0
- package/dist/src/extensions/registry.js +10 -0
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/main.test.js +15 -0
- package/dist/tests/cli/main.test.js.map +1 -1
- package/dist/tests/e2e/native-extensions.test.d.ts +13 -0
- package/dist/tests/e2e/native-extensions.test.js +175 -0
- package/dist/tests/e2e/native-extensions.test.js.map +1 -0
- package/dist/tests/extensions/native.test.d.ts +1 -0
- package/dist/tests/extensions/native.test.js +101 -0
- package/dist/tests/extensions/native.test.js.map +1 -0
- package/package.json +1 -1
- package/runtime/xt_ext.h +109 -0
- package/runtime/xt_ext.rs +173 -0
- package/src/cli/main.ts +13 -1
- package/src/driver/compiler.ts +25 -4
- package/src/extensions/native.ts +270 -0
- package/src/extensions/registry.ts +21 -0
- package/src/index.ts +10 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// xbintsc native-extension bindings for Rust.
|
|
2
|
+
//
|
|
3
|
+
// Include this file from a Rust `staticlib`/`cdylib` that xbintsc links into a
|
|
4
|
+
// generated binary:
|
|
5
|
+
//
|
|
6
|
+
// // src/lib.rs
|
|
7
|
+
// include!("../../runtime/xt_ext.rs");
|
|
8
|
+
//
|
|
9
|
+
// #[no_mangle]
|
|
10
|
+
// pub extern "C" fn mathx_add(argc: i32, argv: *const XtValue) -> XtValue {
|
|
11
|
+
// let a = arg_number(argc, argv, 0, 0.0);
|
|
12
|
+
// let b = arg_number(argc, argv, 1, 0.0);
|
|
13
|
+
// xt_number(a + b)
|
|
14
|
+
// }
|
|
15
|
+
//
|
|
16
|
+
// The exported functions use the runtime ABI, so a native manifest maps them
|
|
17
|
+
// exactly like C or C++ symbols: `xt_value name(int32_t argc, xt_value *argv)`.
|
|
18
|
+
//
|
|
19
|
+
// Build with the runtime directory reachable and produce a static library:
|
|
20
|
+
//
|
|
21
|
+
// cargo build --release # crate-type = ["staticlib"]
|
|
22
|
+
// # -> target/release/libmathx.a
|
|
23
|
+
|
|
24
|
+
/// A JavaScript value: a 64-bit NaN-boxed word (see `runtime/rt.h`).
|
|
25
|
+
pub type XtValue = u64;
|
|
26
|
+
|
|
27
|
+
pub const XT_TAG_MASK: XtValue = 0xFFFF_0000_0000_0000;
|
|
28
|
+
pub const XT_NUMBER_MASK: XtValue = 0xFFF8_0000_0000_0000;
|
|
29
|
+
|
|
30
|
+
pub const XT_UNDEFINED: XtValue = 0xFFF8_0000_0000_0000;
|
|
31
|
+
pub const XT_NULL: XtValue = 0xFFF9_0000_0000_0000;
|
|
32
|
+
pub const XT_FALSE: XtValue = 0xFFFA_0000_0000_0000;
|
|
33
|
+
pub const XT_TAG_STRING: XtValue = 0xFFFC_0000_0000_0000;
|
|
34
|
+
|
|
35
|
+
// Raw runtime entry points. They are wrapped in safe helpers below so that
|
|
36
|
+
// extension authors never have to spell `unsafe` just to build a value.
|
|
37
|
+
extern "C" {
|
|
38
|
+
#[link_name = "xt_number"]
|
|
39
|
+
fn raw_xt_number(d: f64) -> XtValue;
|
|
40
|
+
#[link_name = "xt_string_new"]
|
|
41
|
+
fn raw_xt_string_new(data: *const u8, len: usize) -> XtValue;
|
|
42
|
+
#[link_name = "xt_string_data"]
|
|
43
|
+
fn raw_xt_string_data(value: XtValue) -> *const u8;
|
|
44
|
+
#[link_name = "xt_string_length_value"]
|
|
45
|
+
fn raw_xt_string_length_value(value: XtValue) -> i32;
|
|
46
|
+
#[link_name = "xt_truthy"]
|
|
47
|
+
fn raw_xt_truthy(value: XtValue) -> i32;
|
|
48
|
+
#[link_name = "xt_to_number"]
|
|
49
|
+
fn raw_xt_to_number(value: XtValue) -> f64;
|
|
50
|
+
#[link_name = "xt_to_string"]
|
|
51
|
+
fn raw_xt_to_string(value: XtValue) -> XtValue;
|
|
52
|
+
#[link_name = "xt_array_new"]
|
|
53
|
+
fn raw_xt_array_new(count: i32, items: *mut XtValue) -> XtValue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Box an `f64` into a JavaScript number value.
|
|
57
|
+
#[inline]
|
|
58
|
+
pub fn xt_number(d: f64) -> XtValue {
|
|
59
|
+
unsafe { raw_xt_number(d) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Build a string value from a byte slice (assumed UTF-8).
|
|
63
|
+
#[inline]
|
|
64
|
+
pub fn xt_string_new(data: *const u8, len: usize) -> XtValue {
|
|
65
|
+
unsafe { raw_xt_string_new(data, len) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Pointer to the first byte of a string value's payload.
|
|
69
|
+
#[inline]
|
|
70
|
+
pub fn xt_string_data(value: XtValue) -> *const u8 {
|
|
71
|
+
unsafe { raw_xt_string_data(value) }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Byte length of a string value.
|
|
75
|
+
#[inline]
|
|
76
|
+
pub fn xt_string_length_value(value: XtValue) -> i32 {
|
|
77
|
+
unsafe { raw_xt_string_length_value(value) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// JavaScript truthiness of a value (`0`/`1`).
|
|
81
|
+
#[inline]
|
|
82
|
+
pub fn xt_truthy(value: XtValue) -> i32 {
|
|
83
|
+
unsafe { raw_xt_truthy(value) }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Convert a value to a number, coercing non-numbers like the runtime does.
|
|
87
|
+
#[inline]
|
|
88
|
+
pub fn xt_to_number(value: XtValue) -> f64 {
|
|
89
|
+
unsafe { raw_xt_to_number(value) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Convert a value to a string value.
|
|
93
|
+
#[inline]
|
|
94
|
+
pub fn xt_to_string(value: XtValue) -> XtValue {
|
|
95
|
+
unsafe { raw_xt_to_string(value) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Build an array from `count` items.
|
|
99
|
+
#[inline]
|
|
100
|
+
pub fn xt_array_new(count: i32, items: *mut XtValue) -> XtValue {
|
|
101
|
+
unsafe { raw_xt_array_new(count, items) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Whether a value carries the number (unboxed double) representation.
|
|
105
|
+
#[inline]
|
|
106
|
+
pub fn is_number(value: XtValue) -> bool {
|
|
107
|
+
(value & XT_NUMBER_MASK) != XT_NUMBER_MASK
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Whether a value is a string.
|
|
111
|
+
#[inline]
|
|
112
|
+
pub fn is_string(value: XtValue) -> bool {
|
|
113
|
+
(value & XT_TAG_MASK) == XT_TAG_STRING
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Reinterpret an unboxed number value as `f64`.
|
|
117
|
+
#[inline]
|
|
118
|
+
pub fn as_f64(value: XtValue) -> f64 {
|
|
119
|
+
f64::from_bits(value)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Read argument `index`, returning `undefined` when it was not supplied.
|
|
123
|
+
///
|
|
124
|
+
/// `argv` may be null when a function is called with no arguments.
|
|
125
|
+
#[inline]
|
|
126
|
+
pub fn arg(argc: i32, argv: *const XtValue, index: usize) -> XtValue {
|
|
127
|
+
if argv.is_null() || index >= argc as usize {
|
|
128
|
+
return XT_UNDEFINED;
|
|
129
|
+
}
|
|
130
|
+
unsafe { *argv.add(index) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Read argument `index` coerced to a number, or `fallback` when absent.
|
|
134
|
+
#[inline]
|
|
135
|
+
pub fn arg_number(argc: i32, argv: *const XtValue, index: usize, fallback: f64) -> f64 {
|
|
136
|
+
let value = arg(argc, argv, index);
|
|
137
|
+
if value == XT_UNDEFINED {
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
xt_to_number(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Read argument `index` coerced to a boolean.
|
|
144
|
+
#[inline]
|
|
145
|
+
pub fn arg_bool(argc: i32, argv: *const XtValue, index: usize, fallback: bool) -> bool {
|
|
146
|
+
let value = arg(argc, argv, index);
|
|
147
|
+
if value == XT_UNDEFINED {
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
xt_truthy(value) != 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Borrow argument `index` as a UTF-8 string slice, or `None` when it is not a
|
|
154
|
+
/// string. The bytes live in the runtime arena and never move.
|
|
155
|
+
#[inline]
|
|
156
|
+
pub fn arg_str<'a>(argc: i32, argv: *const XtValue, index: usize) -> Option<&'a str> {
|
|
157
|
+
let value = arg(argc, argv, index);
|
|
158
|
+
if !is_string(value) {
|
|
159
|
+
return None;
|
|
160
|
+
}
|
|
161
|
+
let length = xt_string_length_value(value) as usize;
|
|
162
|
+
let data = xt_string_data(value);
|
|
163
|
+
if data.is_null() || length == 0 {
|
|
164
|
+
return Some("");
|
|
165
|
+
}
|
|
166
|
+
unsafe { Some(core::str::from_utf8_unchecked(core::slice::from_raw_parts(data, length))) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Build a string value from a Rust string slice.
|
|
170
|
+
#[inline]
|
|
171
|
+
pub fn string_from(text: &str) -> XtValue {
|
|
172
|
+
xt_string_new(text.as_ptr(), text.len())
|
|
173
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* available programmatically through the driver API:
|
|
6
6
|
*
|
|
7
7
|
* xbintsc build <file> [-o out] [--emit ir|obj|exe] [-O0..3] [--ext node]
|
|
8
|
+
* [--ext-native manifest.json]
|
|
8
9
|
* xbintsc run <file> [-- args...]
|
|
9
10
|
* xbintsc emit <file> # print LLVM IR to stdout
|
|
10
11
|
* xbintsc doctor # report the resolved toolchain
|
|
@@ -23,6 +24,7 @@ import { resolveToolchain } from "../driver/toolchain-provider.js";
|
|
|
23
24
|
import { realRunner } from "../driver/toolchain.js";
|
|
24
25
|
import { createDefaultRegistry, type ExtensionRegistry } from "../extensions/registry.js";
|
|
25
26
|
import { nodeExtension } from "../extensions/node/index.js";
|
|
27
|
+
import { nativeExtensionFromManifest } from "../extensions/native.js";
|
|
26
28
|
|
|
27
29
|
export interface CliIo {
|
|
28
30
|
readonly stdout: (text: string) => void;
|
|
@@ -41,7 +43,7 @@ interface ParsedArgs {
|
|
|
41
43
|
readonly passthrough: string[];
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
const VALUE_FLAGS = new Set(["output", "out", "emit", "optimize", "ext"]);
|
|
46
|
+
const VALUE_FLAGS = new Set(["output", "out", "emit", "optimize", "ext", "ext-native"]);
|
|
45
47
|
|
|
46
48
|
function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
47
49
|
const positionals: string[] = [];
|
|
@@ -102,6 +104,14 @@ function buildRegistry(flags: Map<string, string | boolean>): ExtensionRegistry
|
|
|
102
104
|
else throw new Error(`Unknown extension '${name}'`);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
// Each `--ext-native` value is a manifest path describing a pre-built C++ or
|
|
108
|
+
// Rust library; several manifests may be comma separated.
|
|
109
|
+
const native = flags.get("ext-native");
|
|
110
|
+
if (typeof native === "string") {
|
|
111
|
+
for (const manifest of native.split(",").map((n) => n.trim()).filter(Boolean)) {
|
|
112
|
+
registry.register(nativeExtensionFromManifest(manifest));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
105
115
|
return registry;
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -133,6 +143,8 @@ Options:
|
|
|
133
143
|
--emit <kind> exe | obj | ir (default: exe)
|
|
134
144
|
-O0..-O3 Optimization level (default: -O2)
|
|
135
145
|
--ext <names> Comma separated extensions (e.g. node)
|
|
146
|
+
--ext-native <m> Register a C++/Rust extension from a JSON manifest
|
|
147
|
+
(comma separated for several)
|
|
136
148
|
--force Ignore the incremental cache
|
|
137
149
|
--verbose Print progress information
|
|
138
150
|
`;
|
package/src/driver/compiler.ts
CHANGED
|
@@ -160,7 +160,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
160
160
|
emit,
|
|
161
161
|
optimize,
|
|
162
162
|
process.platform,
|
|
163
|
-
registry
|
|
163
|
+
registryFingerprint(registry),
|
|
164
164
|
runtimeFingerprint(runtimeDir),
|
|
165
165
|
]);
|
|
166
166
|
|
|
@@ -209,7 +209,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
209
209
|
return { outputPath, irPath, cached: false, diagnostics: [], ir };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
const { runtimeObjects, extensionObjects } = ensureRuntimeObjects(
|
|
212
|
+
const { runtimeObjects, extensionObjects, nativeObjects } = ensureRuntimeObjects(
|
|
213
213
|
runner,
|
|
214
214
|
clang,
|
|
215
215
|
runtimeDir,
|
|
@@ -221,7 +221,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
221
221
|
|
|
222
222
|
link(runner, {
|
|
223
223
|
clang,
|
|
224
|
-
objectPaths: [objectPath, ...runtimeObjects, ...extensionObjects],
|
|
224
|
+
objectPaths: [objectPath, ...runtimeObjects, ...extensionObjects, ...nativeObjects],
|
|
225
225
|
outputPath,
|
|
226
226
|
linkerFlags: [
|
|
227
227
|
...toolchain.linkerArgs,
|
|
@@ -240,6 +240,8 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
240
240
|
interface RuntimeObjects {
|
|
241
241
|
readonly runtimeObjects: readonly string[];
|
|
242
242
|
readonly extensionObjects: readonly string[];
|
|
243
|
+
/** Pre-built C++/Rust objects and archives contributed by native extensions. */
|
|
244
|
+
readonly nativeObjects: readonly string[];
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
/** Core runtime translation units (each compiled and cached independently). */
|
|
@@ -285,6 +287,24 @@ function runtimeFingerprint(runtimeDir: string): string {
|
|
|
285
287
|
return hashParts(parts);
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Fingerprint everything an extension contributes that can change the linked
|
|
292
|
+
* executable but does not live under `runtime/`: its name, linker flags and the
|
|
293
|
+
* contents of any pre-built C++/Rust objects. Keeps a cached binary fresh when
|
|
294
|
+
* a native extension is rebuilt, or when `--ext-native` flags change.
|
|
295
|
+
*/
|
|
296
|
+
function registryFingerprint(registry: ExtensionRegistry): string {
|
|
297
|
+
const parts: string[] = [];
|
|
298
|
+
for (const extension of registry.all()) {
|
|
299
|
+
parts.push(extension.name);
|
|
300
|
+
parts.push(...(extension.linkerFlags?.() ?? []));
|
|
301
|
+
for (const object of extension.nativeObjects?.() ?? []) {
|
|
302
|
+
parts.push(object, existsSync(object) ? readFileSync(object).toString("base64") : "");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return hashParts(parts);
|
|
306
|
+
}
|
|
307
|
+
|
|
288
308
|
/**
|
|
289
309
|
* Compile the core runtime and every extension source, reusing cached object
|
|
290
310
|
* files keyed on the C source hash. Returns the object paths to link.
|
|
@@ -348,5 +368,6 @@ function ensureRuntimeObjects(
|
|
|
348
368
|
extensionObjects.push(compileOne(sourcePath, `ext_${extensionObjects.length}_${basename(sourcePath, ".c")}`));
|
|
349
369
|
}
|
|
350
370
|
}
|
|
351
|
-
|
|
371
|
+
// A native archive may be shared by several manifests; link each once.
|
|
372
|
+
return { runtimeObjects, extensionObjects, nativeObjects: [...new Set(registry.nativeObjects())] };
|
|
352
373
|
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native (C++ / Rust) extensions.
|
|
3
|
+
*
|
|
4
|
+
* xbintsc can consume code that was compiled outside the TypeScript pipeline,
|
|
5
|
+
* as long as it exposes `extern "C"` entry points with the runtime calling
|
|
6
|
+
* convention:
|
|
7
|
+
*
|
|
8
|
+
* xt_value my_fn(int32_t argc, xt_value *argv);
|
|
9
|
+
*
|
|
10
|
+
* A C++ or Rust project is built into an object file or static archive, and a
|
|
11
|
+
* small JSON manifest describes how those symbols map onto global functions
|
|
12
|
+
* (`builtins`) and importable modules (`modules`). The driver links the
|
|
13
|
+
* artifacts verbatim and the code generator wires the bindings exactly like it
|
|
14
|
+
* does for the built-in `node` extension — the core compiler never needs to
|
|
15
|
+
* know the extension was written in C++ or Rust.
|
|
16
|
+
*
|
|
17
|
+
* ```jsonc
|
|
18
|
+
* {
|
|
19
|
+
* "name": "mathx",
|
|
20
|
+
* "description": "C++ math helpers",
|
|
21
|
+
* "objects": ["build/libmathx.a"],
|
|
22
|
+
* "linkerFlags": ["-lm"],
|
|
23
|
+
* "linkerFlagsByPlatform": { "linux": ["-lstdc++"], "darwin": ["-lc++"] },
|
|
24
|
+
* "builtins": { "fastAdd": { "symbol": "mathx_add" } },
|
|
25
|
+
* "modules": {
|
|
26
|
+
* "mathx": {
|
|
27
|
+
* "exports": {
|
|
28
|
+
* "add": { "symbol": "mathx_add" },
|
|
29
|
+
* "reverse": { "symbol": "mathx_reverse" }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* `objects` paths are resolved relative to the manifest file, so a manifest can
|
|
37
|
+
* live next to the project it describes.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
41
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
42
|
+
import type { Extension, ExtensionModule, ModuleExport, ModuleExports } from "./registry.js";
|
|
43
|
+
|
|
44
|
+
/** A global function an extension provides (callable without an import). */
|
|
45
|
+
export interface NativeBuiltin {
|
|
46
|
+
/** Exported C symbol, called as `xt_value symbol(int32_t argc, xt_value *argv)`. */
|
|
47
|
+
readonly symbol: string;
|
|
48
|
+
readonly returnVoid?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One binding inside a `modules` entry; mirrors the core `ModuleExport`. */
|
|
52
|
+
export type NativeModuleExport = ModuleExport;
|
|
53
|
+
|
|
54
|
+
/** An importable module supplied by the native library. */
|
|
55
|
+
export interface NativeModule {
|
|
56
|
+
/** Named exports for `import { x } from "..."`. */
|
|
57
|
+
readonly exports?: Readonly<Record<string, NativeModuleExport>>;
|
|
58
|
+
/** Namespace name for `import * as ns` / `import ns from`. */
|
|
59
|
+
readonly namespace?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The on-disk schema of a native extension manifest. */
|
|
63
|
+
export interface NativeManifest {
|
|
64
|
+
/** Unique extension name (used by `--ext`/diagnostics). */
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly description?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Object files (`.o`) or static archives (`.a`/`.lib`) to link. Resolved
|
|
69
|
+
* relative to the manifest unless absolute.
|
|
70
|
+
*/
|
|
71
|
+
readonly objects?: readonly string[];
|
|
72
|
+
/** Linker flags appended verbatim, e.g. `["-lm"]`. */
|
|
73
|
+
readonly linkerFlags?: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* Linker flags added only on the given platform (`darwin`, `linux`, `win32`),
|
|
76
|
+
* for C++ standard libraries or Rust's native dependencies that differ per OS.
|
|
77
|
+
*/
|
|
78
|
+
readonly linkerFlagsByPlatform?: Readonly<Record<string, readonly string[]>>;
|
|
79
|
+
/** Global functions the extension binds. */
|
|
80
|
+
readonly builtins?: Readonly<Record<string, NativeBuiltin>>;
|
|
81
|
+
/** Modules importable as `import ... from "<specifier>"`. */
|
|
82
|
+
readonly modules?: Readonly<Record<string, NativeModule>>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Raised for a malformed manifest or a missing build artifact. */
|
|
86
|
+
export class NativeExtensionError extends Error {
|
|
87
|
+
constructor(message: string) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = "NativeExtensionError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stringArray(value: unknown, field: string): readonly string[] {
|
|
98
|
+
if (value === undefined) return [];
|
|
99
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
100
|
+
throw new NativeExtensionError(`Native manifest field '${field}' must be an array of strings`);
|
|
101
|
+
}
|
|
102
|
+
return value as readonly string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseBuiltins(value: unknown): Readonly<Record<string, NativeBuiltin>> | undefined {
|
|
106
|
+
if (value === undefined) return undefined;
|
|
107
|
+
if (!isRecord(value)) throw new NativeExtensionError("Native manifest field 'builtins' must be an object");
|
|
108
|
+
const builtins: Record<string, NativeBuiltin> = {};
|
|
109
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
110
|
+
if (!isRecord(entry) || typeof entry.symbol !== "string" || entry.symbol.length === 0) {
|
|
111
|
+
throw new NativeExtensionError(`Native manifest builtin '${name}' needs a non-empty 'symbol'`);
|
|
112
|
+
}
|
|
113
|
+
builtins[name] = {
|
|
114
|
+
symbol: entry.symbol,
|
|
115
|
+
...(entry.returnVoid === true ? { returnVoid: true } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return builtins;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseExports(value: unknown, moduleName: string): ModuleExports | undefined {
|
|
122
|
+
if (value === undefined) return undefined;
|
|
123
|
+
if (!isRecord(value)) {
|
|
124
|
+
throw new NativeExtensionError(`Native manifest module '${moduleName}'.exports must be an object`);
|
|
125
|
+
}
|
|
126
|
+
const exports: Record<string, ModuleExport> = {};
|
|
127
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
128
|
+
if (!isRecord(entry)) {
|
|
129
|
+
throw new NativeExtensionError(`Native manifest export '${moduleName}.${name}' must be an object`);
|
|
130
|
+
}
|
|
131
|
+
const symbol = entry.symbol;
|
|
132
|
+
const namespace = entry.namespace;
|
|
133
|
+
const method = entry.method;
|
|
134
|
+
if (symbol !== undefined && typeof symbol !== "string") {
|
|
135
|
+
throw new NativeExtensionError(`Native manifest export '${moduleName}.${name}'.symbol must be a string`);
|
|
136
|
+
}
|
|
137
|
+
if (namespace !== undefined && typeof namespace !== "string") {
|
|
138
|
+
throw new NativeExtensionError(`Native manifest export '${moduleName}.${name}'.namespace must be a string`);
|
|
139
|
+
}
|
|
140
|
+
if (method !== undefined && typeof method !== "string") {
|
|
141
|
+
throw new NativeExtensionError(`Native manifest export '${moduleName}.${name}'.method must be a string`);
|
|
142
|
+
}
|
|
143
|
+
if (symbol === undefined && namespace === undefined) {
|
|
144
|
+
throw new NativeExtensionError(
|
|
145
|
+
`Native manifest export '${moduleName}.${name}' needs a 'symbol' or a 'namespace'`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
exports[name] = {
|
|
149
|
+
...(symbol !== undefined ? { symbol } : {}),
|
|
150
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
151
|
+
...(method !== undefined ? { method } : {}),
|
|
152
|
+
...(entry.returnVoid === true ? { returnVoid: true } : {}),
|
|
153
|
+
...(entry.isConstructor === true ? { isConstructor: true } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return exports;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseModules(value: unknown): Readonly<Record<string, ExtensionModule>> | undefined {
|
|
160
|
+
if (value === undefined) return undefined;
|
|
161
|
+
if (!isRecord(value)) throw new NativeExtensionError("Native manifest field 'modules' must be an object");
|
|
162
|
+
const modules: Record<string, ExtensionModule> = {};
|
|
163
|
+
for (const [specifier, entry] of Object.entries(value)) {
|
|
164
|
+
if (!isRecord(entry)) {
|
|
165
|
+
throw new NativeExtensionError(`Native manifest module '${specifier}' must be an object`);
|
|
166
|
+
}
|
|
167
|
+
if (entry.namespace !== undefined && typeof entry.namespace !== "string") {
|
|
168
|
+
throw new NativeExtensionError(`Native manifest module '${specifier}'.namespace must be a string`);
|
|
169
|
+
}
|
|
170
|
+
modules[specifier] = {
|
|
171
|
+
...(entry.namespace !== undefined ? { namespace: entry.namespace } : {}),
|
|
172
|
+
...(entry.exports !== undefined ? { exports: parseExports(entry.exports, specifier) } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return modules;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Validate parsed JSON and normalise it into a {@link NativeManifest}. */
|
|
179
|
+
export function parseNativeManifest(value: unknown, source = "<manifest>"): NativeManifest {
|
|
180
|
+
if (!isRecord(value)) throw new NativeExtensionError(`Native manifest ${source} must contain a JSON object`);
|
|
181
|
+
if (typeof value.name !== "string" || value.name.trim().length === 0) {
|
|
182
|
+
throw new NativeExtensionError(`Native manifest ${source} needs a non-empty 'name'`);
|
|
183
|
+
}
|
|
184
|
+
if (value.description !== undefined && typeof value.description !== "string") {
|
|
185
|
+
throw new NativeExtensionError(`Native manifest '${value.name}'.description must be a string`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let linkerFlagsByPlatform: Record<string, readonly string[]> | undefined;
|
|
189
|
+
if (value.linkerFlagsByPlatform !== undefined) {
|
|
190
|
+
if (!isRecord(value.linkerFlagsByPlatform)) {
|
|
191
|
+
throw new NativeExtensionError("Native manifest field 'linkerFlagsByPlatform' must be an object");
|
|
192
|
+
}
|
|
193
|
+
linkerFlagsByPlatform = {};
|
|
194
|
+
for (const [platform, flags] of Object.entries(value.linkerFlagsByPlatform)) {
|
|
195
|
+
linkerFlagsByPlatform[platform] = stringArray(flags, `linkerFlagsByPlatform.${platform}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const builtins = parseBuiltins(value.builtins);
|
|
200
|
+
const modules = parseModules(value.modules);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
name: value.name,
|
|
204
|
+
...(value.description !== undefined ? { description: value.description } : {}),
|
|
205
|
+
objects: stringArray(value.objects, "objects"),
|
|
206
|
+
linkerFlags: stringArray(value.linkerFlags, "linkerFlags"),
|
|
207
|
+
...(linkerFlagsByPlatform ? { linkerFlagsByPlatform } : {}),
|
|
208
|
+
...(builtins ? { builtins } : {}),
|
|
209
|
+
...(modules ? { modules } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Read and validate a manifest from disk. */
|
|
214
|
+
export function loadNativeManifest(manifestPath: string): NativeManifest {
|
|
215
|
+
const absolute = resolve(manifestPath);
|
|
216
|
+
let text: string;
|
|
217
|
+
try {
|
|
218
|
+
text = readFileSync(absolute, "utf8");
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new NativeExtensionError(`Unable to read native extension manifest '${absolute}': ${String(error)}`);
|
|
221
|
+
}
|
|
222
|
+
let parsed: unknown;
|
|
223
|
+
try {
|
|
224
|
+
parsed = JSON.parse(text);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new NativeExtensionError(`Native extension manifest '${absolute}' is not valid JSON: ${String(error)}`);
|
|
227
|
+
}
|
|
228
|
+
return parseNativeManifest(parsed, absolute);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveArtifact(baseDir: string, path: string): string {
|
|
232
|
+
return isAbsolute(path) ? path : resolve(baseDir, path);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Turn a manifest into an {@link Extension}: resolve and verify every native
|
|
237
|
+
* object, then expose the manifest's builtins/modules to the code generator.
|
|
238
|
+
*
|
|
239
|
+
* The manifest is read eagerly so a typo surfaces at registration time rather
|
|
240
|
+
* than at link time.
|
|
241
|
+
*/
|
|
242
|
+
export function nativeExtensionFromManifest(manifestPath: string): Extension {
|
|
243
|
+
const absolute = resolve(manifestPath);
|
|
244
|
+
const manifest = loadNativeManifest(absolute);
|
|
245
|
+
const baseDir = dirname(absolute);
|
|
246
|
+
|
|
247
|
+
const objects = (manifest.objects ?? []).map((object) => resolveArtifact(baseDir, object));
|
|
248
|
+
for (const object of objects) {
|
|
249
|
+
if (!existsSync(object)) {
|
|
250
|
+
throw new NativeExtensionError(
|
|
251
|
+
`Native extension '${manifest.name}' references missing object '${object}'.\n` +
|
|
252
|
+
`Build the C++/Rust library first (see the project next to ${absolute}).`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const linkerFlags = () => [
|
|
258
|
+
...(manifest.linkerFlags ?? []),
|
|
259
|
+
...(manifest.linkerFlagsByPlatform?.[process.platform] ?? []),
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
name: manifest.name,
|
|
264
|
+
description: manifest.description ?? `Native extension from ${absolute}`,
|
|
265
|
+
nativeObjects: () => objects,
|
|
266
|
+
linkerFlags,
|
|
267
|
+
...(manifest.builtins ? { builtins: () => manifest.builtins! } : {}),
|
|
268
|
+
...(manifest.modules ? { modules: () => manifest.modules! } : {}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -52,6 +52,17 @@ export interface Extension {
|
|
|
52
52
|
readonly description?: string;
|
|
53
53
|
/** C/asm sources compiled and linked alongside the generated module. */
|
|
54
54
|
runtimeSources?(): readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* Pre-built object files or static archives linked alongside the generated
|
|
57
|
+
* module. This is the hook for native extensions written in C++ or Rust:
|
|
58
|
+
* compile them to `extern "C"` objects (or a static library) that use the
|
|
59
|
+
* `xt_value` ABI, then hand the artifacts to the driver through an
|
|
60
|
+
* `Extension` such as the one built by `src/extensions/native.ts`.
|
|
61
|
+
*
|
|
62
|
+
* The paths must already exist; the driver passes them straight to the
|
|
63
|
+
* linker and never recompiles them.
|
|
64
|
+
*/
|
|
65
|
+
nativeObjects?(): readonly string[];
|
|
55
66
|
/** Extra linker flags (e.g. `["-lm"]`, `["-framework", "CoreFoundation"]`). */
|
|
56
67
|
linkerFlags?(): readonly string[];
|
|
57
68
|
/** Global identifiers that resolve to runtime symbols when called. */
|
|
@@ -123,6 +134,16 @@ export class ExtensionRegistry {
|
|
|
123
134
|
return sources;
|
|
124
135
|
}
|
|
125
136
|
|
|
137
|
+
/** Flatten every registered extension's pre-built native objects. */
|
|
138
|
+
nativeObjects(): readonly string[] {
|
|
139
|
+
const objects: string[] = [];
|
|
140
|
+
for (const extension of this.extensions.values()) {
|
|
141
|
+
const extra = extension.nativeObjects?.();
|
|
142
|
+
if (extra) objects.push(...extra);
|
|
143
|
+
}
|
|
144
|
+
return objects;
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
linkerFlags(): readonly string[] {
|
|
127
148
|
const flags: string[] = [];
|
|
128
149
|
for (const extension of this.extensions.values()) {
|
package/src/index.ts
CHANGED
|
@@ -32,4 +32,14 @@ export {
|
|
|
32
32
|
export { ExtensionRegistry, createDefaultRegistry, coreExtension, type Extension, type ExtensionModule, type ModuleExport, type ModuleExports } from "./extensions/registry.js";
|
|
33
33
|
export { nodeExtension } from "./extensions/node/index.js";
|
|
34
34
|
export type { NodeModule } from "./extensions/node/module.js";
|
|
35
|
+
export {
|
|
36
|
+
NativeExtensionError,
|
|
37
|
+
loadNativeManifest,
|
|
38
|
+
nativeExtensionFromManifest,
|
|
39
|
+
parseNativeManifest,
|
|
40
|
+
type NativeManifest,
|
|
41
|
+
type NativeBuiltin,
|
|
42
|
+
type NativeModule,
|
|
43
|
+
type NativeModuleExport,
|
|
44
|
+
} from "./extensions/native.js";
|
|
35
45
|
export * from "./driver/index.js";
|