@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,388 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { NativeLibrary } from '../../components/NativeLibrary.js';
|
|
3
|
+
import { builtinTypes } from '../declarations/Builtins.js';
|
|
4
|
+
import { DeclarationParser } from '../declarations/Parser.js';
|
|
5
|
+
import { CURRENT_PLATFORM } from '../declarations/Platform.js';
|
|
6
|
+
import { loadNativeLibrary } from '../library/NativeLibraryHandle.js';
|
|
7
|
+
import { declarationText, prunedTokens } from './CText.js';
|
|
8
|
+
import { preprocessHeader } from './Preprocessor.js';
|
|
9
|
+
import { TypeScriptTypes } from './TypeScriptTypes.js';
|
|
10
|
+
/**
|
|
11
|
+
* The tokens that are not names of types or constants
|
|
12
|
+
*/
|
|
13
|
+
const KEYWORDS = new Set(['struct', 'union', 'enum', 'typedef', 'extern', 'static', 'const', 'volatile', 'signed', 'unsigned', 'inline']);
|
|
14
|
+
/**
|
|
15
|
+
* The names the generated file imports or uses: not names of interfaces
|
|
16
|
+
*/
|
|
17
|
+
const TAKEN = new Set(['Library', 'Native', 'NativeLibrary', 'NativeVariable', 'Pointer', 'Out', 'Buffer', 'ArrayBufferView', 'Array', 'Object', 'Function', 'String', 'Number', 'Date', 'Error', 'Map', 'Set', 'Promise']);
|
|
18
|
+
/**
|
|
19
|
+
* The options of the configuration of the component of a library: not names of properties
|
|
20
|
+
*/
|
|
21
|
+
const CONFIGURATION = ['path', 'search', 'keepLoaded', 'drainTimeout', 'threads'];
|
|
22
|
+
/**
|
|
23
|
+
* The names of the members of a NativeLibrary: a function named as one of them gets another property
|
|
24
|
+
*/
|
|
25
|
+
function memberNames() {
|
|
26
|
+
const names = new Set(CONFIGURATION);
|
|
27
|
+
for (let prototype = NativeLibrary.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)) {
|
|
28
|
+
for (const name of Object.getOwnPropertyNames(prototype))
|
|
29
|
+
names.add(name);
|
|
30
|
+
}
|
|
31
|
+
return names;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A path as the paths of the line markers are compared (case-insensitive on Windows)
|
|
35
|
+
* @param file
|
|
36
|
+
*/
|
|
37
|
+
function comparable(file) {
|
|
38
|
+
const resolved = path.resolve(file);
|
|
39
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether a file is inside a directory
|
|
43
|
+
* @param file
|
|
44
|
+
* @param directory
|
|
45
|
+
*/
|
|
46
|
+
function inside(file, directory) {
|
|
47
|
+
const relative = path.relative(directory, file);
|
|
48
|
+
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Which files are the ones of the library: the header, the headers of its directory when it is not a directory of the
|
|
52
|
+
* system, the ones of the directories given
|
|
53
|
+
* @param header
|
|
54
|
+
* @param systemDirectories
|
|
55
|
+
* @param own
|
|
56
|
+
*/
|
|
57
|
+
function ownFiles(header, systemDirectories, own) {
|
|
58
|
+
const main = comparable(header);
|
|
59
|
+
const system = systemDirectories.map(comparable);
|
|
60
|
+
const directory = path.dirname(main);
|
|
61
|
+
const directories = [...system.includes(directory) ? [] : [directory], ...own.map(comparable)];
|
|
62
|
+
const known = new Map();
|
|
63
|
+
return (file) => {
|
|
64
|
+
if (!file || file.startsWith('<'))
|
|
65
|
+
return false;
|
|
66
|
+
let result = known.get(file);
|
|
67
|
+
if (result === undefined) {
|
|
68
|
+
const candidate = comparable(file);
|
|
69
|
+
result = candidate === main || directories.some((dir) => inside(candidate, dir) && !system.some((sys) => sys !== dir && inside(candidate, sys) && inside(sys, dir)));
|
|
70
|
+
known.set(file, result);
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The names a declaration uses: of types (\`type x\`, \`struct x\`) and of constants
|
|
77
|
+
* @param tokens
|
|
78
|
+
*/
|
|
79
|
+
function referencesOf(tokens) {
|
|
80
|
+
const references = [];
|
|
81
|
+
tokens.forEach((token, index) => {
|
|
82
|
+
if (token.kind !== 'identifier' || KEYWORDS.has(token.text))
|
|
83
|
+
return;
|
|
84
|
+
const before = tokens[index - 1];
|
|
85
|
+
if (before?.kind === 'identifier' && (before.text === 'struct' || before.text === 'union' || before.text === 'enum'))
|
|
86
|
+
references.push(`${before.text} ${token.text}`);
|
|
87
|
+
else
|
|
88
|
+
references.push(`type ${token.text}`, `constant ${token.text}`);
|
|
89
|
+
});
|
|
90
|
+
return references;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A name in PascalCase (\`sqlite3\`: \`Sqlite3\`, \`vendor_sdk\`: \`VendorSdk\`)
|
|
94
|
+
* @param name
|
|
95
|
+
*/
|
|
96
|
+
function pascalCase(name) {
|
|
97
|
+
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
98
|
+
const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join('') || 'Library';
|
|
99
|
+
return /^[0-9]/.test(joined) ? `Library${joined}` : joined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The name \`@Library\` gets for a library given as a name or a file: \`libsqlite3.dylib\`, \`libsqlite3.so\`, \`sqlite3.dll\`
|
|
103
|
+
* give \`sqlite3\` (completed on each platform); a file with a version is kept as it is
|
|
104
|
+
* @param library
|
|
105
|
+
*/
|
|
106
|
+
function libraryName(library) {
|
|
107
|
+
const file = path.basename(library);
|
|
108
|
+
const plain = file.match(/^(?:lib)?(.+?)\.(?:dylib|so|dll)$/i);
|
|
109
|
+
if (plain && !/\.(?:dylib|so|dll)/i.test(plain[1]))
|
|
110
|
+
return plain[1];
|
|
111
|
+
return file;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A text as the lines of a comment of TypeScript, wrapped at 120 characters
|
|
115
|
+
* @param text
|
|
116
|
+
*/
|
|
117
|
+
function commentLines(text) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
let line = '//';
|
|
120
|
+
for (const word of text.split(' ')) {
|
|
121
|
+
if (line.length > 2 && line.length + 1 + word.length > 120) {
|
|
122
|
+
lines.push(line);
|
|
123
|
+
line = '//';
|
|
124
|
+
}
|
|
125
|
+
line += line.length > 2 ? ` ${word}` : word;
|
|
126
|
+
}
|
|
127
|
+
lines.push(line);
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Text put in a template literal of TypeScript
|
|
132
|
+
* @param text
|
|
133
|
+
*/
|
|
134
|
+
function templateText(text) {
|
|
135
|
+
return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The first line of the message of an error
|
|
139
|
+
* @param error
|
|
140
|
+
*/
|
|
141
|
+
function reasonOf(error) {
|
|
142
|
+
return (error instanceof Error ? error.message : String(error)).split('\n')[0].replace(/ \(line \d+, column \d+\)$/, '');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Generate the class of a library from a header already run through the preprocessor (see generateNativeLibrary).
|
|
146
|
+
* @param preprocessed The output of the preprocessor, the header, the directories of the system.
|
|
147
|
+
* @param options The library, the class, the directories of the library.
|
|
148
|
+
* @param platform The platform the header was preprocessed for.
|
|
149
|
+
* @returns The code of the class and the report.
|
|
150
|
+
*/
|
|
151
|
+
export async function generateFromPreprocessed(preprocessed, options = {}, platform = CURRENT_PLATFORM) {
|
|
152
|
+
const header = preprocessed.header;
|
|
153
|
+
const headerName = path.basename(header).replace(/\.[^.]+$/, '');
|
|
154
|
+
const source = new DeclarationParser(platform).readSource(preprocessed.text);
|
|
155
|
+
const items = source.items;
|
|
156
|
+
const isOwn = ownFiles(header, preprocessed.systemDirectories, options.own ?? []);
|
|
157
|
+
const builtins = builtinTypes(platform);
|
|
158
|
+
const skipped = [];
|
|
159
|
+
const where = (item) => ({ ...item.file !== undefined ? { file: item.file } : {}, ...item.line !== undefined ? { line: item.line } : {} });
|
|
160
|
+
//The declarations of the library, and the ones of the system they use
|
|
161
|
+
const definers = new Map();
|
|
162
|
+
items.forEach((item, index) => {
|
|
163
|
+
if (item.error)
|
|
164
|
+
return;
|
|
165
|
+
for (const name of item.names)
|
|
166
|
+
definers.set(name, [...definers.get(name) ?? [], index]);
|
|
167
|
+
});
|
|
168
|
+
const selected = new Set();
|
|
169
|
+
const queue = [];
|
|
170
|
+
items.forEach((item, index) => {
|
|
171
|
+
if (!isOwn(item.file))
|
|
172
|
+
return;
|
|
173
|
+
if (item.error) {
|
|
174
|
+
skipped.push({ name: declarationText(prunedTokens(item.tokens.slice(0, 16)), '').join(' ').slice(0, 80), ...where(item), reason: item.error });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!item.names.length)
|
|
178
|
+
return;
|
|
179
|
+
selected.add(index);
|
|
180
|
+
queue.push(index);
|
|
181
|
+
});
|
|
182
|
+
while (queue.length) {
|
|
183
|
+
const index = queue.pop();
|
|
184
|
+
const item = items[index];
|
|
185
|
+
for (const reference of referencesOf(item.tokens)) {
|
|
186
|
+
if (item.names.includes(reference))
|
|
187
|
+
continue;
|
|
188
|
+
if (reference.startsWith('type ') && builtins.has(reference.slice(5)))
|
|
189
|
+
continue;
|
|
190
|
+
//The definition seen at that point: the last one before it
|
|
191
|
+
const candidates = definers.get(reference) ?? [];
|
|
192
|
+
const definer = candidates.filter((candidate) => candidate < index).pop() ?? candidates[0];
|
|
193
|
+
if (definer === undefined || selected.has(definer))
|
|
194
|
+
continue;
|
|
195
|
+
//A typedef of the system redefining a type the package knows is not needed
|
|
196
|
+
if (!isOwn(items[definer].file) && items[definer].names.every((name) => name.startsWith('type ') && builtins.has(name.slice(5))))
|
|
197
|
+
continue;
|
|
198
|
+
selected.add(definer);
|
|
199
|
+
queue.push(definer);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
//The macros of the library whose value is an integer, written with their values
|
|
203
|
+
const itemConstants = new Set(items.flatMap((item) => [...item.names]).filter((name) => name.startsWith('constant ')).map((name) => name.slice(9)));
|
|
204
|
+
const macroConstants = [...source.macros]
|
|
205
|
+
.filter(([name, origin]) => isOwn(origin.file) && source.declarations.constants.has(name) && !itemConstants.has(name))
|
|
206
|
+
.map(([name]) => [name, source.declarations.constants.get(name)]);
|
|
207
|
+
//The declarations written again, each one checked: read alone, after the ones before it
|
|
208
|
+
const checker = new DeclarationParser(platform);
|
|
209
|
+
const chunks = [];
|
|
210
|
+
for (const index of [...selected].sort((a, b) => a - b)) {
|
|
211
|
+
const item = items[index];
|
|
212
|
+
const body = declarationText(prunedTokens(item.tokens));
|
|
213
|
+
const lines = item.pack !== undefined && item.names.some((name) => name.startsWith('struct ') || name.startsWith('union ') || name.startsWith('type '))
|
|
214
|
+
? [`#pragma pack(push, ${item.pack})`, ...body, '#pragma pack(pop)']
|
|
215
|
+
: body;
|
|
216
|
+
try {
|
|
217
|
+
checker.parse(lines.join('\n'));
|
|
218
|
+
chunks.push({ index: index, lines: lines });
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
selected.delete(index);
|
|
222
|
+
if (isOwn(item.file))
|
|
223
|
+
skipped.push({ name: item.names.join(', '), ...where(item), reason: `its declaration cannot be read again: ${reasonOf(error)}` });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const constantLines = macroConstants.map(([name, value]) => `#define ${name} ${value}`);
|
|
227
|
+
const declarations = [...chunks.flatMap((chunk) => chunk.lines), ...constantLines].join('\n');
|
|
228
|
+
//The functions and the variables of the library, as the declarations written are read (the types the package knows
|
|
229
|
+
//replace the ones of the system of the same name: DWORD, LPCWSTR)
|
|
230
|
+
const emitted = checker.declarations;
|
|
231
|
+
let functions = [];
|
|
232
|
+
let variables = [];
|
|
233
|
+
const prototypes = new Map();
|
|
234
|
+
for (const chunk of chunks) {
|
|
235
|
+
const item = items[chunk.index];
|
|
236
|
+
if (!isOwn(item.file))
|
|
237
|
+
continue;
|
|
238
|
+
for (const name of item.names) {
|
|
239
|
+
if (name.startsWith('function ')) {
|
|
240
|
+
const fn = emitted.functions.get(name.slice(9));
|
|
241
|
+
if (fn)
|
|
242
|
+
functions.push(fn);
|
|
243
|
+
prototypes.set(name.slice(9), declarationText(prunedTokens(item.tokens), '').join(' '));
|
|
244
|
+
}
|
|
245
|
+
else if (name.startsWith('variable ')) {
|
|
246
|
+
const variable = emitted.variables.get(name.slice(9));
|
|
247
|
+
if (variable)
|
|
248
|
+
variables.push(variable);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const ownConstants = macroConstants.length + chunks.filter((chunk) => isOwn(items[chunk.index].file)).flatMap((chunk) => items[chunk.index].names).filter((name) => name.startsWith('constant ')).length;
|
|
253
|
+
//Checked against the library: the functions and the variables it cannot give left out
|
|
254
|
+
let libraryFile;
|
|
255
|
+
let unchecked;
|
|
256
|
+
const library = options.library ?? headerName;
|
|
257
|
+
if (options.check === false) {
|
|
258
|
+
unchecked = 'not asked';
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
let handle;
|
|
262
|
+
try {
|
|
263
|
+
handle = loadNativeLibrary(library, declarations);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
unchecked = reasonOf(error).replace(/:$/, '').replace(/^The /, 'the ');
|
|
267
|
+
}
|
|
268
|
+
if (handle) {
|
|
269
|
+
libraryFile = handle.file;
|
|
270
|
+
const usable = (declared, bind) => declared.filter((declaration) => {
|
|
271
|
+
try {
|
|
272
|
+
bind(declaration.name);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
skipped.push({ name: declaration.name, reason: reasonOf(error) });
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
functions = usable(functions, (name) => handle.function(name));
|
|
281
|
+
variables = usable(variables, (name) => handle.variable(name));
|
|
282
|
+
await handle.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
//The interfaces of the structs and unions of the library
|
|
286
|
+
const records = [];
|
|
287
|
+
const names = new Map();
|
|
288
|
+
const used = new Set([...TAKEN]);
|
|
289
|
+
const className = options.className ?? pascalCase(headerName);
|
|
290
|
+
used.add(className);
|
|
291
|
+
const nameRecord = (record, preferred) => {
|
|
292
|
+
if (names.has(record))
|
|
293
|
+
return;
|
|
294
|
+
let name = preferred;
|
|
295
|
+
while (used.has(name))
|
|
296
|
+
name = `${name}_`;
|
|
297
|
+
used.add(name);
|
|
298
|
+
names.set(record, name);
|
|
299
|
+
records.push({ name: name, record: record });
|
|
300
|
+
};
|
|
301
|
+
for (const chunk of chunks) {
|
|
302
|
+
const item = items[chunk.index];
|
|
303
|
+
if (!isOwn(item.file))
|
|
304
|
+
continue;
|
|
305
|
+
for (const name of item.names) {
|
|
306
|
+
const type = name.startsWith('type ') ? emitted.types.get(name.slice(5)) : name.startsWith('struct ') || name.startsWith('union ') ? emitted.tags.get(name) : undefined;
|
|
307
|
+
if ((type?.kind === 'struct' || type?.kind === 'union') && type.members)
|
|
308
|
+
nameRecord(type, name.startsWith('type ') ? name.slice(5) : name.split(' ')[1]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const types = new TypeScriptTypes(names);
|
|
312
|
+
//The class: a property by function and variable, named as it (another name when the component has a member of that name)
|
|
313
|
+
const reserved = memberNames();
|
|
314
|
+
const property = (name) => {
|
|
315
|
+
if (!reserved.has(name))
|
|
316
|
+
return { name: name, argument: '' };
|
|
317
|
+
let renamed = `${name}_`;
|
|
318
|
+
while (reserved.has(renamed))
|
|
319
|
+
renamed = `${renamed}_`;
|
|
320
|
+
return { name: renamed, argument: `'${name}'` };
|
|
321
|
+
};
|
|
322
|
+
const members = [];
|
|
323
|
+
for (const fn of functions) {
|
|
324
|
+
const prototype = prototypes.get(fn.name);
|
|
325
|
+
const { name, argument } = property(fn.name);
|
|
326
|
+
members.push([...prototype ? [` //${prototype}`] : [], ` @Native(${argument})`, ` public readonly ${name}: ${types.functionType(fn.type)}`].join('\n'));
|
|
327
|
+
}
|
|
328
|
+
for (const variable of variables) {
|
|
329
|
+
const { name, argument } = property(variable.name);
|
|
330
|
+
members.push([` @NativeVariable(${argument})`, ` public readonly ${name}: ${types.type(variable.type, 'received')}`].join('\n'));
|
|
331
|
+
}
|
|
332
|
+
const interfaces = records.map(({ name, record }) => [`export interface ${name} {`, ...types.members(record, ' '), '}'].join('\n'));
|
|
333
|
+
const body = [...interfaces, ...members].join('\n');
|
|
334
|
+
const imports = ['Library', 'Native', 'NativeLibrary', ...variables.length ? ['NativeVariable'] : [], ...body.includes('Out<') ? ['type Out'] : [], ...body.includes('Pointer') ? ['type Pointer'] : []];
|
|
335
|
+
const platformName = `${platform.os}-${platform.arch}`;
|
|
336
|
+
const code = [
|
|
337
|
+
...commentLines(`Generated from ${path.basename(header)} by the generator of @lakutata/ffi (lakutata ffi generate), for ${platformName}${libraryFile ? `; checked against ${libraryFile}` : ''}: ${functions.length} functions, ${variables.length} variables, ${ownConstants} constants.`),
|
|
338
|
+
...commentLines('The functions are synchronous, and their pointer parameters are inputs: where needed, mark the output parameters _Out_ in the declarations and the functions that block {async: true} (see the guide of @lakutata/ffi). Another build of the library may lack some of these functions: the component then refuses to start, naming the function.'),
|
|
339
|
+
`import {${imports.join(', ')}} from '@lakutata/ffi'`,
|
|
340
|
+
'',
|
|
341
|
+
...interfaces.flatMap((text) => [text, '']),
|
|
342
|
+
'@Library({',
|
|
343
|
+
` names: ${JSON.stringify(libraryName(library)).replace(/^"|"$/g, '\'')},`,
|
|
344
|
+
' declarations: `',
|
|
345
|
+
...templateText(declarations).split('\n').map((line) => ` ${line}`),
|
|
346
|
+
' `',
|
|
347
|
+
'})',
|
|
348
|
+
`export class ${className} extends NativeLibrary {`,
|
|
349
|
+
members.join('\n\n'),
|
|
350
|
+
'}',
|
|
351
|
+
''
|
|
352
|
+
].join('\n');
|
|
353
|
+
return {
|
|
354
|
+
className: className,
|
|
355
|
+
code: code,
|
|
356
|
+
declarations: declarations,
|
|
357
|
+
report: {
|
|
358
|
+
command: preprocessed.command,
|
|
359
|
+
platform: platformName,
|
|
360
|
+
...libraryFile ? { library: libraryFile } : {},
|
|
361
|
+
...unchecked ? { unchecked: unchecked } : {},
|
|
362
|
+
functions: functions.length,
|
|
363
|
+
variables: variables.length,
|
|
364
|
+
interfaces: records.length,
|
|
365
|
+
constants: ownConstants,
|
|
366
|
+
skipped: skipped
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Generate the class of a native library from its header: the header is run through the preprocessor of the C compiler,
|
|
372
|
+
* its declarations are read, the functions and the variables the library cannot give are left out, and the class gets a
|
|
373
|
+
* property by function (named as the function) with its TypeScript type.
|
|
374
|
+
* @param options The header, the library, the class, the compiler and its options.
|
|
375
|
+
* @returns The code of the class, and what was generated and left out.
|
|
376
|
+
* @example
|
|
377
|
+
* ```typescript
|
|
378
|
+
* import {writeFileSync} from 'node:fs'
|
|
379
|
+
* import {generateNativeLibrary} from '@lakutata/ffi/generator'
|
|
380
|
+
*
|
|
381
|
+
* const {code, report} = await generateNativeLibrary({header: 'vendor/include/vendor.h', library: 'vendor/lib/libvendor.so'})
|
|
382
|
+
* writeFileSync('src/native/Vendor.ts', code)
|
|
383
|
+
* for (const skipped of report.skipped) console.log(`${skipped.name}: ${skipped.reason}`)
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
export async function generateNativeLibrary(options) {
|
|
387
|
+
return await generateFromPreprocessed(preprocessHeader(options), options);
|
|
388
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { devNull, tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { NativeDeclarationException } from '../../exceptions/NativeDeclarationException.js';
|
|
6
|
+
/**
|
|
7
|
+
* The C compiler whose preprocessor reads the headers by default: the one of the CC variable, else cl on Windows (run
|
|
8
|
+
* in the Developer Command Prompt of Visual Studio), cc elsewhere.
|
|
9
|
+
* @returns The command of the compiler.
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import {defaultCompiler} from '@lakutata/ffi/generator'
|
|
13
|
+
*
|
|
14
|
+
* console.log(defaultCompiler())
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function defaultCompiler() {
|
|
18
|
+
return process.env.CC?.trim() || (process.platform === 'win32' ? 'cl' : 'cc');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether a compiler takes the options of Microsoft (cl, clang-cl)
|
|
22
|
+
* @param compiler
|
|
23
|
+
*/
|
|
24
|
+
function microsoft(compiler) {
|
|
25
|
+
return /^(?:cl|clang-cl)(?:\.exe)?$/i.test(path.basename(compiler));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run a compiler: its output, its errors
|
|
29
|
+
* @param compiler
|
|
30
|
+
* @param args
|
|
31
|
+
*/
|
|
32
|
+
function run(compiler, args) {
|
|
33
|
+
const result = spawnSync(compiler, args, { encoding: 'latin1', maxBuffer: 1 << 30, windowsHide: true });
|
|
34
|
+
if (result.error?.code === 'ENOENT') {
|
|
35
|
+
throw new NativeDeclarationException(microsoft(compiler)
|
|
36
|
+
? `The compiler ${compiler} is not found: run in the Developer Command Prompt (or PowerShell) of Visual Studio, or give another compiler`
|
|
37
|
+
: `The compiler ${compiler} is not found: install a C compiler (the Command Line Tools of Xcode, gcc, clang), or give another one`);
|
|
38
|
+
}
|
|
39
|
+
if (result.error)
|
|
40
|
+
throw result.error;
|
|
41
|
+
return { stdout: result.stdout, stderr: result.stderr, status: result.status };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The directories of the headers of the system of GCC and Clang, as -v lists them (without the ones given by -I)
|
|
45
|
+
* @param compiler
|
|
46
|
+
*/
|
|
47
|
+
function systemDirectoriesOf(compiler) {
|
|
48
|
+
const { stdout, stderr } = run(compiler, ['-E', '-v', '-x', 'c', devNull]);
|
|
49
|
+
const lines = `${stdout}\n${stderr}`.split(/\r?\n/);
|
|
50
|
+
const start = lines.findIndex((line) => line.startsWith('#include <...> search starts here'));
|
|
51
|
+
const end = lines.findIndex((line, index) => index > start && line.startsWith('End of search list'));
|
|
52
|
+
if (start < 0 || end < 0)
|
|
53
|
+
return [];
|
|
54
|
+
return lines.slice(start + 1, end).map((line) => path.resolve(line.replace(/\(framework directory\)$/, '').trim()));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run a header through the preprocessor of a compiler, keeping its #define and the line markers naming the files.
|
|
58
|
+
* @param options The header, the compiler, the directories and the macros.
|
|
59
|
+
* @returns The output, the header as the line markers name it, the directories of the system.
|
|
60
|
+
*/
|
|
61
|
+
export function preprocessHeader(options) {
|
|
62
|
+
const header = path.resolve(options.header);
|
|
63
|
+
const compiler = options.compiler ?? defaultCompiler();
|
|
64
|
+
const include = (options.include ?? []).map((directory) => path.resolve(directory));
|
|
65
|
+
const define = options.define ?? [];
|
|
66
|
+
if (microsoft(compiler)) {
|
|
67
|
+
//cl /P writes a file; /d1PP keeps the #define in it
|
|
68
|
+
const directory = mkdtempSync(path.join(tmpdir(), 'lakutata-ffi-preprocess-'));
|
|
69
|
+
const output = path.join(directory, 'header.i');
|
|
70
|
+
const args = ['/nologo', '/P', '/TC', '/d1PP', `/Fi${output}`, ...include.map((dir) => `/I${dir}`), ...define.map((macro) => `/D${macro}`), header];
|
|
71
|
+
try {
|
|
72
|
+
const { stderr, status } = run(compiler, args);
|
|
73
|
+
if (status !== 0)
|
|
74
|
+
throw new NativeDeclarationException(`The preprocessor failed (${compiler} ${args.join(' ')}): ${stderr.trim()}`);
|
|
75
|
+
const systemDirectories = (process.env.INCLUDE ?? '').split(';').filter(Boolean).map((dir) => path.resolve(dir));
|
|
76
|
+
return { text: readFileSync(output, 'latin1'), header: header, systemDirectories: systemDirectories, command: [compiler, ...args.filter((arg) => !arg.startsWith('/Fi'))].join(' ') };
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
rmSync(directory, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const args = ['-E', '-dD', '-x', 'c', ...include.map((dir) => `-I${dir}`), ...define.map((macro) => `-D${macro}`), header];
|
|
83
|
+
const { stdout, stderr, status } = run(compiler, args);
|
|
84
|
+
if (status !== 0)
|
|
85
|
+
throw new NativeDeclarationException(`The preprocessor failed (${compiler} ${args.join(' ')}): ${stderr.trim()}`);
|
|
86
|
+
return { text: stdout, header: header, systemDirectories: systemDirectoriesOf(compiler), command: [compiler, ...args].join(' ') };
|
|
87
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { primitiveBig } from '../declarations/Types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The words TypeScript reserves: not names of parameters
|
|
4
|
+
*/
|
|
5
|
+
const RESERVED = new Set([
|
|
6
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false',
|
|
7
|
+
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try',
|
|
8
|
+
'typeof', 'var', 'void', 'while', 'with', 'as', 'implements', 'interface', 'let', 'package', 'private', 'protected', 'public', 'static',
|
|
9
|
+
'yield', 'await', 'arguments', 'eval'
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* The typed arrays of the arrays of numbers of the structs
|
|
13
|
+
*/
|
|
14
|
+
const TYPED_ARRAYS = {
|
|
15
|
+
schar: 'Int8Array', int8: 'Int8Array', uchar: 'Uint8Array', uint8: 'Uint8Array', short: 'Int16Array', int16: 'Int16Array',
|
|
16
|
+
ushort: 'Uint16Array', uint16: 'Uint16Array', char16: 'Uint16Array', int: 'Int32Array', int32: 'Int32Array', uint: 'Uint32Array',
|
|
17
|
+
uint32: 'Uint32Array', char32: 'Uint32Array', float: 'Float32Array', double: 'Float64Array'
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The name of a parameter in TypeScript: its C name, not a reserved word, not one of the others
|
|
21
|
+
* @param parameter
|
|
22
|
+
* @param index
|
|
23
|
+
* @param taken
|
|
24
|
+
*/
|
|
25
|
+
function parameterName(parameter, index, taken) {
|
|
26
|
+
let name = parameter.name ?? `arg${index}`;
|
|
27
|
+
if (RESERVED.has(name))
|
|
28
|
+
name = `${name}_`;
|
|
29
|
+
while (taken.has(name))
|
|
30
|
+
name = `${name}_`;
|
|
31
|
+
taken.add(name);
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The TypeScript types of C types, as the package converts their values
|
|
36
|
+
*/
|
|
37
|
+
export class TypeScriptTypes {
|
|
38
|
+
#names;
|
|
39
|
+
/**
|
|
40
|
+
* Types for the declarations of a library.
|
|
41
|
+
* @param names The TypeScript names of its structs and unions.
|
|
42
|
+
*/
|
|
43
|
+
constructor(names) {
|
|
44
|
+
this.#names = names;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The TypeScript type of a value of a C type.
|
|
48
|
+
* @param type The C type.
|
|
49
|
+
* @param flow Whether the value is given to the native code or received from it.
|
|
50
|
+
*/
|
|
51
|
+
type(type, flow) {
|
|
52
|
+
switch (type.kind) {
|
|
53
|
+
case 'primitive':
|
|
54
|
+
return this.#primitive(type.name, flow);
|
|
55
|
+
case 'enum':
|
|
56
|
+
return this.#primitive(type.base, flow);
|
|
57
|
+
case 'pointer':
|
|
58
|
+
return this.#pointer(type.to, type.constTarget, flow);
|
|
59
|
+
case 'array':
|
|
60
|
+
return this.#array(type.of);
|
|
61
|
+
case 'struct':
|
|
62
|
+
case 'union':
|
|
63
|
+
return this.#names.get(type) ?? this.#inline(type);
|
|
64
|
+
case 'function':
|
|
65
|
+
return this.#pointer(type, false, flow);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The TypeScript type of a function: of a native function, or of a JS callback the native code calls.
|
|
70
|
+
* @param type The function type.
|
|
71
|
+
* @param callback Whether it is a JS callback (its arguments received, its result given).
|
|
72
|
+
*/
|
|
73
|
+
functionType(type, callback = false) {
|
|
74
|
+
const taken = new Set();
|
|
75
|
+
const parameters = type.parameters.map((parameter, index) => {
|
|
76
|
+
return `${parameterName(parameter, index, taken)}: ${callback ? this.type(parameter.type, 'received') : this.#parameter(parameter)}`;
|
|
77
|
+
});
|
|
78
|
+
if (type.variadic)
|
|
79
|
+
parameters.push('...args: unknown[]');
|
|
80
|
+
return `(${parameters.join(', ')}) => ${this.type(type.result, callback ? 'given' : 'received')}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The members of a struct or a union, as the lines of an interface (or of an object type).
|
|
84
|
+
* @param record The struct or the union.
|
|
85
|
+
* @param indent The indentation of the lines.
|
|
86
|
+
*/
|
|
87
|
+
members(record, indent) {
|
|
88
|
+
return (record.members ?? []).map((member) => `${indent}${member.name}: ${this.type(member.type, 'received')}`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A parameter of a native function: an output parameter as the array or the object filled
|
|
92
|
+
* @param parameter
|
|
93
|
+
*/
|
|
94
|
+
#parameter(parameter) {
|
|
95
|
+
if (parameter.direction !== 'in' && parameter.type.kind === 'pointer') {
|
|
96
|
+
const to = parameter.type.to;
|
|
97
|
+
if ((to.kind === 'struct' || to.kind === 'union') && to.members)
|
|
98
|
+
return this.type(to, 'received');
|
|
99
|
+
return `Out<${this.type(to, 'received')}>`;
|
|
100
|
+
}
|
|
101
|
+
return this.type(parameter.type, 'given');
|
|
102
|
+
}
|
|
103
|
+
#primitive(name, flow) {
|
|
104
|
+
if (name === 'void')
|
|
105
|
+
return 'void';
|
|
106
|
+
if (name === 'bool')
|
|
107
|
+
return 'boolean';
|
|
108
|
+
if (primitiveBig(name))
|
|
109
|
+
return flow === 'given' ? 'bigint | number' : 'bigint';
|
|
110
|
+
return 'number';
|
|
111
|
+
}
|
|
112
|
+
#pointer(to, constTarget, flow) {
|
|
113
|
+
//The strings: char, char16_t, wchar_t
|
|
114
|
+
if (to.kind === 'primitive' && (to.name === 'char' || to.name === 'char16' || to.name === 'wchar')) {
|
|
115
|
+
return flow === 'given' && !constTarget ? 'string | Buffer | null' : 'string | null';
|
|
116
|
+
}
|
|
117
|
+
if (to.kind === 'function' && flow === 'given')
|
|
118
|
+
return `(${this.functionType(to, true)}) | Pointer | null`;
|
|
119
|
+
if (flow !== 'given' || to.kind === 'function')
|
|
120
|
+
return 'Pointer | null';
|
|
121
|
+
//A struct or a union given to the function: its object too
|
|
122
|
+
if ((to.kind === 'struct' || to.kind === 'union') && to.members)
|
|
123
|
+
return `${this.type(to, 'received')} | Pointer | Buffer | ArrayBufferView | null`;
|
|
124
|
+
return 'Pointer | Buffer | ArrayBufferView | null';
|
|
125
|
+
}
|
|
126
|
+
#array(of) {
|
|
127
|
+
if (of.kind === 'primitive' || of.kind === 'enum') {
|
|
128
|
+
const name = of.kind === 'enum' ? of.base : of.name;
|
|
129
|
+
if (name === 'char' || name === 'wchar')
|
|
130
|
+
return 'string';
|
|
131
|
+
if (primitiveBig(name))
|
|
132
|
+
return 'bigint[]';
|
|
133
|
+
if (name === 'bool')
|
|
134
|
+
return 'boolean[]';
|
|
135
|
+
return TYPED_ARRAYS[name] ?? 'number[]';
|
|
136
|
+
}
|
|
137
|
+
const element = this.type(of, 'received');
|
|
138
|
+
return /^[\w.]+$/.test(element) ? `${element}[]` : `Array<${element}>`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A struct or a union without a name (an anonymous member): an object type
|
|
142
|
+
* @param record
|
|
143
|
+
*/
|
|
144
|
+
#inline(record) {
|
|
145
|
+
if (!record.members)
|
|
146
|
+
return 'Pointer';
|
|
147
|
+
return `{${record.members.map((member) => `${member.name}: ${this.type(member.type, 'received')}`).join(', ')}}`;
|
|
148
|
+
}
|
|
149
|
+
}
|