@lakutata/ffi 0.0.0-stage → 3.1.5

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