@lakutata/ffi 0.0.0-stage → 3.1.2
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/LICENSE +23 -0
- package/README.md +163 -2
- package/README.zh-CN.md +140 -0
- package/dist/cjs/components/NativeLibrary.d.ts +229 -0
- package/dist/cjs/components/NativeLibrary.js +346 -0
- package/dist/cjs/decorators/Library.d.ts +61 -0
- package/dist/cjs/decorators/Library.js +44 -0
- package/dist/cjs/decorators/Native.d.ts +69 -0
- package/dist/cjs/decorators/Native.js +90 -0
- package/dist/cjs/exceptions/NativeDeclarationException.d.ts +12 -0
- package/dist/cjs/exceptions/NativeDeclarationException.js +19 -0
- package/dist/cjs/exceptions/NativeLibraryClosedException.d.ts +12 -0
- package/dist/cjs/exceptions/NativeLibraryClosedException.js +19 -0
- package/dist/cjs/exceptions/NativeLibraryNotFoundException.d.ts +11 -0
- package/dist/cjs/exceptions/NativeLibraryNotFoundException.js +18 -0
- package/dist/cjs/exceptions/NativeMemoryException.d.ts +11 -0
- package/dist/cjs/exceptions/NativeMemoryException.js +18 -0
- package/dist/cjs/exports/FFI.d.ts +12 -0
- package/dist/cjs/exports/FFI.js +40 -0
- package/dist/cjs/exports/Generator.d.ts +2 -0
- package/dist/cjs/exports/Generator.js +10 -0
- package/dist/cjs/lib/Memory.d.ts +188 -0
- package/dist/cjs/lib/Memory.js +227 -0
- package/dist/cjs/lib/Runtime.d.ts +98 -0
- package/dist/cjs/lib/Runtime.js +209 -0
- package/dist/cjs/lib/backend/Functions.d.ts +65 -0
- package/dist/cjs/lib/backend/Functions.js +214 -0
- package/dist/cjs/lib/backend/KoffiTypes.d.ts +53 -0
- package/dist/cjs/lib/backend/KoffiTypes.js +426 -0
- package/dist/cjs/lib/declarations/Builtins.d.ts +33 -0
- package/dist/cjs/lib/declarations/Builtins.js +179 -0
- package/dist/cjs/lib/declarations/Layout.d.ts +41 -0
- package/dist/cjs/lib/declarations/Layout.js +180 -0
- package/dist/cjs/lib/declarations/Lexer.d.ts +40 -0
- package/dist/cjs/lib/declarations/Lexer.js +325 -0
- package/dist/cjs/lib/declarations/Parser.d.ts +90 -0
- package/dist/cjs/lib/declarations/Parser.js +1425 -0
- package/dist/cjs/lib/declarations/Platform.d.ts +50 -0
- package/dist/cjs/lib/declarations/Platform.js +27 -0
- package/dist/cjs/lib/declarations/Types.d.ts +173 -0
- package/dist/cjs/lib/declarations/Types.js +179 -0
- package/dist/cjs/lib/generator/CText.d.ts +14 -0
- package/dist/cjs/lib/generator/CText.js +133 -0
- package/dist/cjs/lib/generator/Generator.d.ts +136 -0
- package/dist/cjs/lib/generator/Generator.js +395 -0
- package/dist/cjs/lib/generator/Preprocessor.d.ts +60 -0
- package/dist/cjs/lib/generator/Preprocessor.js +94 -0
- package/dist/cjs/lib/generator/TypeScriptTypes.d.ts +35 -0
- package/dist/cjs/lib/generator/TypeScriptTypes.js +153 -0
- package/dist/cjs/lib/library/NativeCallback.d.ts +44 -0
- package/dist/cjs/lib/library/NativeCallback.js +66 -0
- package/dist/cjs/lib/library/NativeLibraryHandle.d.ts +268 -0
- package/dist/cjs/lib/library/NativeLibraryHandle.js +540 -0
- package/dist/cjs/lib/library/Resolve.d.ts +49 -0
- package/dist/cjs/lib/library/Resolve.js +130 -0
- package/dist/cjs/lib/thread/NativeThreads.d.ts +80 -0
- package/dist/cjs/lib/thread/NativeThreads.js +384 -0
- package/dist/cjs/lib/thread/NativeWorker.d.ts +6 -0
- package/dist/cjs/lib/thread/NativeWorker.js +96 -0
- package/dist/cjs/lib/thread/NativeWorkerCode.d.ts +5 -0
- package/dist/cjs/lib/thread/NativeWorkerCode.js +4 -0
- package/dist/cjs/lib/thread/PackageRequire.d.ts +6 -0
- package/dist/cjs/lib/thread/PackageRequire.js +4 -0
- package/dist/cjs/lib/thread/ThreadProtocol.d.ts +77 -0
- package/dist/cjs/lib/thread/ThreadProtocol.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/components/NativeLibrary.js +338 -0
- package/dist/esm/decorators/Library.js +40 -0
- package/dist/esm/decorators/Native.js +85 -0
- package/dist/esm/exceptions/NativeDeclarationException.js +15 -0
- package/dist/esm/exceptions/NativeLibraryClosedException.js +15 -0
- package/dist/esm/exceptions/NativeLibraryNotFoundException.js +14 -0
- package/dist/esm/exceptions/NativeMemoryException.js +14 -0
- package/dist/esm/exports/FFI.js +10 -0
- package/dist/esm/exports/Generator.js +2 -0
- package/dist/esm/lib/Memory.js +210 -0
- package/dist/esm/lib/Runtime.js +192 -0
- package/dist/esm/lib/backend/Functions.js +206 -0
- package/dist/esm/lib/backend/KoffiTypes.js +418 -0
- package/dist/esm/lib/declarations/Builtins.js +174 -0
- package/dist/esm/lib/declarations/Layout.js +175 -0
- package/dist/esm/lib/declarations/Lexer.js +321 -0
- package/dist/esm/lib/declarations/Parser.js +1421 -0
- package/dist/esm/lib/declarations/Platform.js +23 -0
- package/dist/esm/lib/declarations/Types.js +170 -0
- package/dist/esm/lib/generator/CText.js +129 -0
- package/dist/esm/lib/generator/Generator.js +388 -0
- package/dist/esm/lib/generator/Preprocessor.js +87 -0
- package/dist/esm/lib/generator/TypeScriptTypes.js +149 -0
- package/dist/esm/lib/library/NativeCallback.js +59 -0
- package/dist/esm/lib/library/NativeLibraryHandle.js +532 -0
- package/dist/esm/lib/library/Resolve.js +122 -0
- package/dist/esm/lib/thread/NativeThreads.js +377 -0
- package/dist/esm/lib/thread/NativeWorker.js +93 -0
- package/dist/esm/lib/thread/NativeWorkerCode.js +1 -0
- package/dist/esm/lib/thread/PackageRequire.js +2 -0
- package/dist/esm/lib/thread/ThreadProtocol.js +1 -0
- package/dist/types/components/NativeLibrary.d.ts +229 -0
- package/dist/types/decorators/Library.d.ts +61 -0
- package/dist/types/decorators/Native.d.ts +69 -0
- package/dist/types/exceptions/NativeDeclarationException.d.ts +12 -0
- package/dist/types/exceptions/NativeLibraryClosedException.d.ts +12 -0
- package/dist/types/exceptions/NativeLibraryNotFoundException.d.ts +11 -0
- package/dist/types/exceptions/NativeMemoryException.d.ts +11 -0
- package/dist/types/exports/FFI.d.ts +12 -0
- package/dist/types/exports/Generator.d.ts +2 -0
- package/dist/types/lib/Memory.d.ts +188 -0
- package/dist/types/lib/Runtime.d.ts +98 -0
- package/dist/types/lib/backend/Functions.d.ts +65 -0
- package/dist/types/lib/backend/KoffiTypes.d.ts +53 -0
- package/dist/types/lib/declarations/Builtins.d.ts +33 -0
- package/dist/types/lib/declarations/Layout.d.ts +41 -0
- package/dist/types/lib/declarations/Lexer.d.ts +40 -0
- package/dist/types/lib/declarations/Parser.d.ts +90 -0
- package/dist/types/lib/declarations/Platform.d.ts +50 -0
- package/dist/types/lib/declarations/Types.d.ts +173 -0
- package/dist/types/lib/generator/CText.d.ts +14 -0
- package/dist/types/lib/generator/Generator.d.ts +136 -0
- package/dist/types/lib/generator/Preprocessor.d.ts +60 -0
- package/dist/types/lib/generator/TypeScriptTypes.d.ts +35 -0
- package/dist/types/lib/library/NativeCallback.d.ts +44 -0
- package/dist/types/lib/library/NativeLibraryHandle.d.ts +268 -0
- package/dist/types/lib/library/Resolve.d.ts +49 -0
- package/dist/types/lib/thread/NativeThreads.d.ts +80 -0
- package/dist/types/lib/thread/NativeWorker.d.ts +6 -0
- package/dist/types/lib/thread/NativeWorkerCode.d.ts +5 -0
- package/dist/types/lib/thread/PackageRequire.d.ts +6 -0
- package/dist/types/lib/thread/ThreadProtocol.d.ts +77 -0
- package/package.json +85 -4
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fileNames = fileNames;
|
|
7
|
+
exports.libraryNames = libraryNames;
|
|
8
|
+
exports.loadLibrary = loadLibrary;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const koffi_1 = __importDefault(require("koffi"));
|
|
12
|
+
const NativeLibraryNotFoundException_js_1 = require("../../exceptions/NativeLibraryNotFoundException.js");
|
|
13
|
+
/**
|
|
14
|
+
* The file of the dynamic loader of musl, which holds its C library (Alpine has no libc.so.6)
|
|
15
|
+
*/
|
|
16
|
+
function muslLoader() {
|
|
17
|
+
try {
|
|
18
|
+
const loader = (0, node_fs_1.readdirSync)('/lib').find((name) => /^ld-musl-.*\.so\.1$/.test(name));
|
|
19
|
+
return loader ? `/lib/${loader}` : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The file of the C library (c) or of its mathematical functions (m) on a system
|
|
27
|
+
* @param name
|
|
28
|
+
* @param os
|
|
29
|
+
*/
|
|
30
|
+
function systemLibrary(name, os) {
|
|
31
|
+
switch (os) {
|
|
32
|
+
case 'darwin':
|
|
33
|
+
return '/usr/lib/libSystem.B.dylib';
|
|
34
|
+
case 'win32':
|
|
35
|
+
return 'ucrtbase.dll';
|
|
36
|
+
case 'linux':
|
|
37
|
+
return muslLoader() ?? (name === 'c' ? 'libc.so.6' : 'libm.so.6');
|
|
38
|
+
case 'freebsd':
|
|
39
|
+
return name === 'c' ? 'libc.so.7' : 'libm.so.5';
|
|
40
|
+
default:
|
|
41
|
+
return name === 'c' ? 'libc.so' : 'libm.so';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The file names a name gives on a system: the name itself when it has a directory or an extension, else the names the
|
|
46
|
+
* system gives libraries
|
|
47
|
+
* @param name
|
|
48
|
+
* @param os
|
|
49
|
+
*/
|
|
50
|
+
function fileNames(name, os = process.platform) {
|
|
51
|
+
if (name === 'c' || name === 'm')
|
|
52
|
+
return [systemLibrary(name, os)];
|
|
53
|
+
if (/[/\\]/.test(name) || /\.(dylib|dll|so)(\.\d+)*$/i.test(name))
|
|
54
|
+
return [name];
|
|
55
|
+
switch (os) {
|
|
56
|
+
case 'darwin':
|
|
57
|
+
return [`lib${name}.dylib`, `${name}.dylib`];
|
|
58
|
+
case 'win32':
|
|
59
|
+
return [`${name}.dll`, `lib${name}.dll`];
|
|
60
|
+
default:
|
|
61
|
+
return [`lib${name}.so`, `${name}.so`];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The names of a library on a system
|
|
66
|
+
* @param names
|
|
67
|
+
* @param os
|
|
68
|
+
*/
|
|
69
|
+
function libraryNames(names, os = process.platform) {
|
|
70
|
+
if (typeof names === 'string')
|
|
71
|
+
return fileNames(names, os);
|
|
72
|
+
const given = names[os] ?? names.default;
|
|
73
|
+
if (given === undefined)
|
|
74
|
+
throw new NativeLibraryNotFoundException_js_1.NativeLibraryNotFoundException(`The library has no name for ${os} (give one for ${os}, or a default)`);
|
|
75
|
+
return (typeof given === 'string' ? [given] : given).flatMap((name) => fileNames(name, os));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load a library: the file given; else each name in the search directories, in the directory of a packaged
|
|
79
|
+
* application, then where the system looks for libraries (dlopen, LoadLibrary)
|
|
80
|
+
* @param names
|
|
81
|
+
* @param options
|
|
82
|
+
* @returns The library and the file it was loaded from.
|
|
83
|
+
*/
|
|
84
|
+
function loadLibrary(names, options = {}) {
|
|
85
|
+
const attempts = [];
|
|
86
|
+
const attempt = (file) => {
|
|
87
|
+
try {
|
|
88
|
+
return koffi_1.default.load(file);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
attempts.push(`${file}: ${error.message}`);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
if (options.path) {
|
|
96
|
+
const file = node_path_1.default.resolve(options.path);
|
|
97
|
+
const library = attempt(file);
|
|
98
|
+
if (library)
|
|
99
|
+
return { library: library, file: file };
|
|
100
|
+
throw new NativeLibraryNotFoundException_js_1.NativeLibraryNotFoundException(`The library cannot be loaded:\n${attempts.join('\n')}`);
|
|
101
|
+
}
|
|
102
|
+
const directories = [...options.search ?? [], ...options.packaged ? [options.packaged] : []].map((directory) => node_path_1.default.resolve(directory));
|
|
103
|
+
const candidates = libraryNames(names);
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
//A name with a directory: that file only
|
|
106
|
+
if (/[/\\]/.test(candidate)) {
|
|
107
|
+
const library = attempt(candidate);
|
|
108
|
+
if (library)
|
|
109
|
+
return { library: library, file: candidate };
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const directory of directories) {
|
|
113
|
+
const file = node_path_1.default.join(directory, candidate);
|
|
114
|
+
if (!(0, node_fs_1.existsSync)(file)) {
|
|
115
|
+
attempts.push(`${file}: not found`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const library = attempt(file);
|
|
119
|
+
if (library)
|
|
120
|
+
return { library: library, file: file };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const candidate of candidates.filter((name) => !/[/\\]/.test(name))) {
|
|
124
|
+
const library = attempt(candidate);
|
|
125
|
+
if (library)
|
|
126
|
+
return { library: library, file: candidate };
|
|
127
|
+
}
|
|
128
|
+
const versioned = process.platform === 'linux' && candidates.some((name) => name.endsWith('.so')) ? '\nOn Linux, the libraries of the system are often installed with their version only (libsqlite3.so.0): give that name.' : '';
|
|
129
|
+
throw new NativeLibraryNotFoundException_js_1.NativeLibraryNotFoundException(`The library ${candidates.join(' or ')} cannot be loaded:\n${attempts.join('\n')}${versioned}`);
|
|
130
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FunctionDeclaration } from '../declarations/Types.js';
|
|
2
|
+
import type { ThreadFunctionOptions } from './ThreadProtocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* A function called in the threads: how a thread binds it
|
|
5
|
+
*/
|
|
6
|
+
export interface ThreadFunction {
|
|
7
|
+
/**
|
|
8
|
+
* Its prototype, or the name of a declared function
|
|
9
|
+
*/
|
|
10
|
+
readonly declaration: string;
|
|
11
|
+
readonly named: boolean;
|
|
12
|
+
readonly options: ThreadFunctionOptions;
|
|
13
|
+
/**
|
|
14
|
+
* Its declaration, read by the thread of the library: which arguments are written by the function, which are
|
|
15
|
+
* callbacks
|
|
16
|
+
*/
|
|
17
|
+
readonly type: FunctionDeclaration;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The result of a call in a thread: its value, and the errno of the thread after it
|
|
21
|
+
*/
|
|
22
|
+
export interface ThreadResult {
|
|
23
|
+
readonly value: unknown;
|
|
24
|
+
readonly errno: number;
|
|
25
|
+
readonly windowsError?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The worker threads of a library: they load it, and run its asynchronous calls (synchronously, each in its thread);
|
|
29
|
+
* with one thread, in the order they are made. The arguments are copied to the thread and the ones the function writes
|
|
30
|
+
* copied back; a JS function given as a callback is registered for the call in the thread of the library, which runs it
|
|
31
|
+
* when the library calls it
|
|
32
|
+
*/
|
|
33
|
+
export declare class NativeThreads {
|
|
34
|
+
#private;
|
|
35
|
+
/**
|
|
36
|
+
* Start the threads of a library.
|
|
37
|
+
* @param file The file of the library, as loaded by this thread.
|
|
38
|
+
* @param declarations Its C declarations.
|
|
39
|
+
* @param count The number of threads.
|
|
40
|
+
* @param crt On Windows, the C library whose errno is read after each call.
|
|
41
|
+
*/
|
|
42
|
+
constructor(file: string, declarations: string, count: number, crt?: string);
|
|
43
|
+
/**
|
|
44
|
+
* Settled once every thread has loaded the library: rejected with the error of a thread that could not.
|
|
45
|
+
*/
|
|
46
|
+
get ready(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* The number of threads.
|
|
49
|
+
*/
|
|
50
|
+
get count(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Add declarations to the ones of the threads (and of the threads started later).
|
|
53
|
+
* @param declarations The declarations.
|
|
54
|
+
*/
|
|
55
|
+
declare(declarations: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Bind a function in the threads now rather than at its first call: when the library is made, while the threads
|
|
58
|
+
* start (a thread started later binds it at its first call in it).
|
|
59
|
+
* @param fn The function.
|
|
60
|
+
*/
|
|
61
|
+
bind(fn: ThreadFunction): void;
|
|
62
|
+
/**
|
|
63
|
+
* Call a function in the least busy thread (with one thread, after the calls made before).
|
|
64
|
+
* @param fn The function.
|
|
65
|
+
* @param args Its arguments.
|
|
66
|
+
* @returns Its value and the errno of the thread after it.
|
|
67
|
+
*/
|
|
68
|
+
call(fn: ThreadFunction, args: unknown[]): Promise<ThreadResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Stop a thread as if it had ended on its own (for the tests): its calls fail, the next call starts a new one.
|
|
71
|
+
* @param index The index of the thread.
|
|
72
|
+
*/
|
|
73
|
+
stopThread(index: number): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Close the threads: each unloads the library and ends; the ones still running after a while are stopped. A thread
|
|
76
|
+
* still running a call (its native code did not return) is stopped at once, the call rejected: the thread ends
|
|
77
|
+
* once the native code returns.
|
|
78
|
+
*/
|
|
79
|
+
close(): Promise<void>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NativeThreads = void 0;
|
|
7
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
8
|
+
const koffi_1 = __importDefault(require("koffi"));
|
|
9
|
+
const NativeDeclarationException_js_1 = require("../../exceptions/NativeDeclarationException.js");
|
|
10
|
+
const NativeLibraryClosedException_js_1 = require("../../exceptions/NativeLibraryClosedException.js");
|
|
11
|
+
const NativeLibraryNotFoundException_js_1 = require("../../exceptions/NativeLibraryNotFoundException.js");
|
|
12
|
+
const NativeMemoryException_js_1 = require("../../exceptions/NativeMemoryException.js");
|
|
13
|
+
const Functions_js_1 = require("../backend/Functions.js");
|
|
14
|
+
const Runtime_js_1 = require("../Runtime.js");
|
|
15
|
+
const NativeWorkerCode_js_1 = require("./NativeWorkerCode.js");
|
|
16
|
+
const PackageRequire_js_1 = require("./PackageRequire.js");
|
|
17
|
+
/**
|
|
18
|
+
* How long the threads have to close once asked, in milliseconds
|
|
19
|
+
*/
|
|
20
|
+
const CLOSE_TIMEOUT = 5000;
|
|
21
|
+
/**
|
|
22
|
+
* An error of a thread, made again with its class
|
|
23
|
+
* @param error
|
|
24
|
+
*/
|
|
25
|
+
function errorOf(error) {
|
|
26
|
+
const classes = {
|
|
27
|
+
NativeDeclarationException: NativeDeclarationException_js_1.NativeDeclarationException,
|
|
28
|
+
NativeLibraryClosedException: NativeLibraryClosedException_js_1.NativeLibraryClosedException,
|
|
29
|
+
NativeLibraryNotFoundException: NativeLibraryNotFoundException_js_1.NativeLibraryNotFoundException,
|
|
30
|
+
NativeMemoryException: NativeMemoryException_js_1.NativeMemoryException,
|
|
31
|
+
TypeError: TypeError,
|
|
32
|
+
RangeError: RangeError
|
|
33
|
+
};
|
|
34
|
+
const made = new (classes[error.name] ?? Error)(error.message);
|
|
35
|
+
if (!classes[error.name])
|
|
36
|
+
made.name = error.name;
|
|
37
|
+
return made;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The value a callback returns to the library when it throws: zero, false, a null pointer
|
|
41
|
+
* @param type
|
|
42
|
+
*/
|
|
43
|
+
function zeroOf(type) {
|
|
44
|
+
if (type.kind === 'primitive')
|
|
45
|
+
return type.name === 'void' ? undefined : type.name === 'bool' ? false : 0;
|
|
46
|
+
if (type.kind === 'enum')
|
|
47
|
+
return 0;
|
|
48
|
+
if (type.kind === 'pointer')
|
|
49
|
+
return null;
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether a parameter points to memory the function may write: not const, or marked _Out_ or _Inout_
|
|
54
|
+
* @param parameter
|
|
55
|
+
*/
|
|
56
|
+
function written(parameter) {
|
|
57
|
+
if (!parameter)
|
|
58
|
+
return true;
|
|
59
|
+
if (parameter.direction !== 'in')
|
|
60
|
+
return true;
|
|
61
|
+
return parameter.type.kind === 'pointer' && !parameter.type.constTarget;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The worker threads of a library: they load it, and run its asynchronous calls (synchronously, each in its thread);
|
|
65
|
+
* with one thread, in the order they are made. The arguments are copied to the thread and the ones the function writes
|
|
66
|
+
* copied back; a JS function given as a callback is registered for the call in the thread of the library, which runs it
|
|
67
|
+
* when the library calls it
|
|
68
|
+
*/
|
|
69
|
+
class NativeThreads {
|
|
70
|
+
#data;
|
|
71
|
+
#count;
|
|
72
|
+
#slots;
|
|
73
|
+
#declarations = [];
|
|
74
|
+
#nextFunction = 1;
|
|
75
|
+
#nextCall = 1;
|
|
76
|
+
#closed = false;
|
|
77
|
+
/**
|
|
78
|
+
* Start the threads of a library.
|
|
79
|
+
* @param file The file of the library, as loaded by this thread.
|
|
80
|
+
* @param declarations Its C declarations.
|
|
81
|
+
* @param count The number of threads.
|
|
82
|
+
* @param crt On Windows, the C library whose errno is read after each call.
|
|
83
|
+
*/
|
|
84
|
+
constructor(file, declarations, count, crt) {
|
|
85
|
+
if (!NativeWorkerCode_js_1.nativeWorkerCode || !PackageRequire_js_1.packageRequire)
|
|
86
|
+
throw new Error('The code of the worker threads of the libraries is not built (scripts/worker.mjs)');
|
|
87
|
+
this.#data = { koffi: PackageRequire_js_1.packageRequire.resolve('koffi'), file: file, declarations: declarations, ...crt ? { crt: crt } : {} };
|
|
88
|
+
this.#count = count;
|
|
89
|
+
this.#slots = Array.from({ length: count }, () => this.#start());
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Settled once every thread has loaded the library: rejected with the error of a thread that could not.
|
|
93
|
+
*/
|
|
94
|
+
get ready() {
|
|
95
|
+
return Promise.all(this.#slots.map((slot) => slot?.ready ?? Promise.resolve())).then(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The number of threads.
|
|
99
|
+
*/
|
|
100
|
+
get count() {
|
|
101
|
+
return this.#count;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Add declarations to the ones of the threads (and of the threads started later).
|
|
105
|
+
* @param declarations The declarations.
|
|
106
|
+
*/
|
|
107
|
+
declare(declarations) {
|
|
108
|
+
this.#declarations.push(declarations);
|
|
109
|
+
for (const slot of this.#slots)
|
|
110
|
+
if (slot && !slot.exited)
|
|
111
|
+
this.#post(slot, { type: 'declare', declarations: declarations });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Bind a function in the threads now rather than at its first call: when the library is made, while the threads
|
|
115
|
+
* start (a thread started later binds it at its first call in it).
|
|
116
|
+
* @param fn The function.
|
|
117
|
+
*/
|
|
118
|
+
bind(fn) {
|
|
119
|
+
if (this.#closed)
|
|
120
|
+
return;
|
|
121
|
+
for (const slot of this.#slots)
|
|
122
|
+
if (slot && !slot.exited)
|
|
123
|
+
this.#bound(slot, fn);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Call a function in the least busy thread (with one thread, after the calls made before).
|
|
127
|
+
* @param fn The function.
|
|
128
|
+
* @param args Its arguments.
|
|
129
|
+
* @returns Its value and the errno of the thread after it.
|
|
130
|
+
*/
|
|
131
|
+
call(fn, args) {
|
|
132
|
+
if (this.#closed)
|
|
133
|
+
return Promise.reject(new NativeLibraryClosedException_js_1.NativeLibraryClosedException(`The library ${this.#data.file} is closed`));
|
|
134
|
+
const slot = this.#leastBusy();
|
|
135
|
+
const id = this.#bound(slot, fn);
|
|
136
|
+
const type = fn.type.type;
|
|
137
|
+
const passed = args.slice();
|
|
138
|
+
const back = [];
|
|
139
|
+
const views = new Map();
|
|
140
|
+
const transfer = [];
|
|
141
|
+
const callbacks = [];
|
|
142
|
+
let callbackError;
|
|
143
|
+
for (let index = 0; index < passed.length; index++) {
|
|
144
|
+
const value = passed[index];
|
|
145
|
+
const parameter = type.parameters[index];
|
|
146
|
+
//A JS function given as a callback: registered for the call, run in this thread when the library calls it
|
|
147
|
+
if (typeof value === 'function') {
|
|
148
|
+
const target = parameter?.type.kind === 'pointer' ? parameter.type.to : undefined;
|
|
149
|
+
if (!target || target.kind !== 'function')
|
|
150
|
+
return this.#refuse(callbacks, new TypeError(`The argument ${index} of ${fn.type.name} is a function, but its parameter is not a pointer to a function`));
|
|
151
|
+
const prototype = target;
|
|
152
|
+
const zero = zeroOf(prototype.result);
|
|
153
|
+
const safe = (...callbackArgs) => {
|
|
154
|
+
try {
|
|
155
|
+
return value(...callbackArgs);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
callbackError ??= { error: error };
|
|
159
|
+
return zero;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const pointer = koffi_1.default.register((0, Functions_js_1.convertedCallback)(prototype, Runtime_js_1.TYPES, safe), koffi_1.default.pointer(Runtime_js_1.TYPES.prototype(prototype)));
|
|
163
|
+
callbacks.push(pointer);
|
|
164
|
+
passed[index] = pointer;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
//A buffer: its own bytes only, copied and transferred (a Buffer of the pool would copy all its pool); copied
|
|
168
|
+
//back after the call when the function may write it
|
|
169
|
+
if (ArrayBuffer.isView(value)) {
|
|
170
|
+
const bytes = new Uint8Array(value.byteLength);
|
|
171
|
+
bytes.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
172
|
+
//A Buffer as its bytes (koffi takes them as it takes the Buffer)
|
|
173
|
+
const copy = Buffer.isBuffer(value) || value instanceof Uint8Array ? bytes : value instanceof DataView ? new DataView(bytes.buffer) : new value.constructor(bytes.buffer);
|
|
174
|
+
passed[index] = copy;
|
|
175
|
+
transfer.push(bytes.buffer);
|
|
176
|
+
if (written(parameter)) {
|
|
177
|
+
back.push(index);
|
|
178
|
+
views.set(index, value);
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
//An output parameter: an array of one element, an object filled by the function
|
|
183
|
+
if (parameter && parameter.direction !== 'in' && value && typeof value === 'object')
|
|
184
|
+
back.push(index);
|
|
185
|
+
}
|
|
186
|
+
const callId = this.#nextCall++;
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const release = () => {
|
|
189
|
+
for (const pointer of callbacks)
|
|
190
|
+
koffi_1.default.unregister(pointer);
|
|
191
|
+
};
|
|
192
|
+
slot.calls.set(callId, {
|
|
193
|
+
resolve: resolve,
|
|
194
|
+
reject: (error) => {
|
|
195
|
+
release();
|
|
196
|
+
reject(error);
|
|
197
|
+
},
|
|
198
|
+
abandon: reject,
|
|
199
|
+
settle: (message) => {
|
|
200
|
+
release();
|
|
201
|
+
//The arguments the function wrote, given back to the caller
|
|
202
|
+
back.forEach((index, at) => {
|
|
203
|
+
const given = args[index];
|
|
204
|
+
const returned = message.back[at];
|
|
205
|
+
const view = views.get(index);
|
|
206
|
+
if (view && ArrayBuffer.isView(returned))
|
|
207
|
+
new Uint8Array(view.buffer, view.byteOffset, view.byteLength).set(new Uint8Array(returned.buffer, returned.byteOffset, returned.byteLength));
|
|
208
|
+
else if (Array.isArray(given) && Array.isArray(returned))
|
|
209
|
+
given.splice(0, given.length, ...returned);
|
|
210
|
+
else if (given && typeof given === 'object' && returned && typeof returned === 'object')
|
|
211
|
+
Object.assign(given, returned);
|
|
212
|
+
});
|
|
213
|
+
if (callbackError)
|
|
214
|
+
return reject(callbackError.error);
|
|
215
|
+
if (message.error)
|
|
216
|
+
return reject(errorOf(message.error));
|
|
217
|
+
resolve({ value: message.value, errno: message.errno, ...message.windowsError !== undefined ? { windowsError: message.windowsError } : {} });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
this.#post(slot, { type: 'call', id: callId, function: id, args: passed, back: back }, transfer);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
slot.calls.delete(callId);
|
|
225
|
+
release();
|
|
226
|
+
reject(error);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Stop a thread as if it had ended on its own (for the tests): its calls fail, the next call starts a new one.
|
|
232
|
+
* @param index The index of the thread.
|
|
233
|
+
*/
|
|
234
|
+
async stopThread(index) {
|
|
235
|
+
await this.#slots[index]?.worker.terminate();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Close the threads: each unloads the library and ends; the ones still running after a while are stopped. A thread
|
|
239
|
+
* still running a call (its native code did not return) is stopped at once, the call rejected: the thread ends
|
|
240
|
+
* once the native code returns.
|
|
241
|
+
*/
|
|
242
|
+
async close() {
|
|
243
|
+
if (this.#closed)
|
|
244
|
+
return;
|
|
245
|
+
this.#closed = true;
|
|
246
|
+
await Promise.all(this.#slots.map(async (slot) => {
|
|
247
|
+
if (!slot || slot.exited)
|
|
248
|
+
return;
|
|
249
|
+
if (slot.calls.size) {
|
|
250
|
+
const error = new NativeLibraryClosedException_js_1.NativeLibraryClosedException(`The library ${this.#data.file} is closed while a call runs in its thread`);
|
|
251
|
+
for (const call of slot.calls.values())
|
|
252
|
+
call.abandon(error);
|
|
253
|
+
slot.calls.clear();
|
|
254
|
+
this.#stop(slot);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const exited = new Promise((resolve) => {
|
|
258
|
+
slot.worker.once('exit', () => resolve());
|
|
259
|
+
});
|
|
260
|
+
//A thread still loading the library gets it once loaded: its port keeps the messages until it listens
|
|
261
|
+
this.#post(slot, { type: 'close' });
|
|
262
|
+
let timer;
|
|
263
|
+
const timeout = new Promise((resolve) => {
|
|
264
|
+
timer = setTimeout(() => resolve(true), CLOSE_TIMEOUT);
|
|
265
|
+
});
|
|
266
|
+
const late = await Promise.race([exited, timeout]);
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
if (late)
|
|
269
|
+
this.#stop(slot);
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A thread stopped, not awaited: a thread running native code ends only once it returns
|
|
274
|
+
* @param slot
|
|
275
|
+
*/
|
|
276
|
+
#stop(slot) {
|
|
277
|
+
slot.worker.terminate().catch(() => undefined);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A call refused before it is sent: its callbacks released
|
|
281
|
+
* @param callbacks
|
|
282
|
+
* @param error
|
|
283
|
+
*/
|
|
284
|
+
#refuse(callbacks, error) {
|
|
285
|
+
for (const pointer of callbacks)
|
|
286
|
+
koffi_1.default.unregister(pointer);
|
|
287
|
+
return Promise.reject(error);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The identifier of a function in a thread, which binds it once
|
|
291
|
+
* @param slot
|
|
292
|
+
* @param fn
|
|
293
|
+
*/
|
|
294
|
+
#bound(slot, fn) {
|
|
295
|
+
let id = slot.bound.get(fn);
|
|
296
|
+
if (id === undefined) {
|
|
297
|
+
id = this.#nextFunction++;
|
|
298
|
+
slot.bound.set(fn, id);
|
|
299
|
+
this.#post(slot, { type: 'bind', id: id, declaration: fn.declaration, named: fn.named, options: fn.options });
|
|
300
|
+
}
|
|
301
|
+
return id;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The thread with the fewest calls waiting, started again if it ended
|
|
305
|
+
*/
|
|
306
|
+
#leastBusy() {
|
|
307
|
+
for (let index = 0; index < this.#slots.length; index++) {
|
|
308
|
+
if (!this.#slots[index] || this.#slots[index].exited)
|
|
309
|
+
this.#slots[index] = this.#start();
|
|
310
|
+
}
|
|
311
|
+
let chosen = this.#slots[0];
|
|
312
|
+
for (const slot of this.#slots)
|
|
313
|
+
if (slot.calls.size < chosen.calls.size)
|
|
314
|
+
chosen = slot;
|
|
315
|
+
return chosen;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A message to a thread
|
|
319
|
+
* @param slot
|
|
320
|
+
* @param message
|
|
321
|
+
* @param transfer
|
|
322
|
+
*/
|
|
323
|
+
#post(slot, message, transfer = []) {
|
|
324
|
+
slot.worker.postMessage(message, transfer);
|
|
325
|
+
//Kept alive while calls wait in it
|
|
326
|
+
if (message.type === 'call')
|
|
327
|
+
slot.worker.ref();
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* A thread started: it loads the library, then serves its calls
|
|
331
|
+
*/
|
|
332
|
+
#start() {
|
|
333
|
+
const worker = new node_worker_threads_1.Worker(NativeWorkerCode_js_1.nativeWorkerCode, { eval: true, workerData: { ...this.#data, declarations: [this.#data.declarations, ...this.#declarations].join('\n') } });
|
|
334
|
+
let readied = () => undefined;
|
|
335
|
+
let failed = () => undefined;
|
|
336
|
+
const ready = new Promise((resolve, reject) => {
|
|
337
|
+
readied = resolve;
|
|
338
|
+
failed = reject;
|
|
339
|
+
});
|
|
340
|
+
//Not unhandled when nothing awaits it: the calls report the failure
|
|
341
|
+
ready.catch(() => undefined);
|
|
342
|
+
const slot = { worker: worker, ready: ready, calls: new Map(), bound: new Map(), exited: false };
|
|
343
|
+
//A thread keeps the process alive while it starts and while calls wait in it, not once idle
|
|
344
|
+
const refresh = () => {
|
|
345
|
+
if (!slot.calls.size)
|
|
346
|
+
worker.unref();
|
|
347
|
+
};
|
|
348
|
+
worker.on('message', (message) => {
|
|
349
|
+
switch (message.type) {
|
|
350
|
+
case 'ready':
|
|
351
|
+
readied();
|
|
352
|
+
refresh();
|
|
353
|
+
return;
|
|
354
|
+
case 'failed':
|
|
355
|
+
slot.error = errorOf(message.error);
|
|
356
|
+
failed(slot.error);
|
|
357
|
+
refresh();
|
|
358
|
+
return;
|
|
359
|
+
case 'result': {
|
|
360
|
+
const call = slot.calls.get(message.id);
|
|
361
|
+
slot.calls.delete(message.id);
|
|
362
|
+
refresh();
|
|
363
|
+
call?.settle(message);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
case 'closed':
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
const end = (error) => {
|
|
371
|
+
if (slot.exited)
|
|
372
|
+
return;
|
|
373
|
+
slot.exited = true;
|
|
374
|
+
failed(error);
|
|
375
|
+
for (const call of slot.calls.values())
|
|
376
|
+
call.reject(slot.error ?? error);
|
|
377
|
+
slot.calls.clear();
|
|
378
|
+
};
|
|
379
|
+
worker.on('error', (error) => end(error));
|
|
380
|
+
worker.on('exit', (code) => end(new Error(`The worker thread of the library ${this.#data.file} ended (exit code ${code})`)));
|
|
381
|
+
return slot;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
exports.NativeThreads = NativeThreads;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve the calls of a library in this worker thread: it loads the library, binds the functions it is asked to bind
|
|
3
|
+
* (synchronous: the thread is the one waiting), calls them in the order of the messages, and sends back their results,
|
|
4
|
+
* the arguments asked back (the output parameters, the buffers written) and its errno after each call
|
|
5
|
+
*/
|
|
6
|
+
export declare function runNativeWorker(): void;
|