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