react-nitro-ffigen 0.0.0-stage → 0.0.1-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +236 -2
- package/package.json +28 -4
- package/src/adapters/enum.ts +48 -0
- package/src/binding-types.ts +18 -0
- package/src/build.ts +98 -0
- package/src/clang-ast-types.ts +16 -0
- package/src/generate.ts +346 -0
- package/src/hermes.d.ts +33 -0
- package/src/tsc.ts +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Margelo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,237 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Static Hermes FFI generator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```mermaid
|
|
4
|
+
flowchart TD
|
|
5
|
+
A["Yoga C headers"] -->|ffigen| B["native.ts — FFI wrappers"]
|
|
6
|
+
B -->|"shermes -emit-c"| C["yoga.c — wrappers translated into C"]
|
|
7
|
+
C -->|"C compiler"| D["Compiled bridge"]
|
|
8
|
+
|
|
9
|
+
E["Yoga C++ implementation"] -->|"C++ compiler"| F["Compiled Yoga"]
|
|
10
|
+
|
|
11
|
+
D --> G["Link into the app with the Hermes runtime"]
|
|
12
|
+
F --> G
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A small Bun/TypeScript generator for selected functions in public C headers.
|
|
16
|
+
Clang parses the header with the library's include paths, defines, and target
|
|
17
|
+
flags. The generator emits TypeScript native bindings and public declarations.
|
|
18
|
+
No Python, CastXML, or runtime FFI dependency is needed by the generator.
|
|
19
|
+
|
|
20
|
+
V1 supports C scalar values, typedefs, named enums whose constants fit `int32_t`,
|
|
21
|
+
and opaque pointers to structs/unions. Struct values, buffers, callbacks,
|
|
22
|
+
variadics, and other unsupported selections fail generation with the function
|
|
23
|
+
and type that need support. Only explicitly selected functions are generated.
|
|
24
|
+
|
|
25
|
+
## Generate bindings
|
|
26
|
+
|
|
27
|
+
Call `generateBindings` from Bun. Paths in `clangArgs` are relative to the working
|
|
28
|
+
directory passed as its second argument:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { generateBindings } from "react-nitro-ffigen";
|
|
32
|
+
import type { BindingOptions } from "react-nitro-ffigen";
|
|
33
|
+
|
|
34
|
+
const options = {
|
|
35
|
+
name: "layout",
|
|
36
|
+
header: "yoga/Yoga.h",
|
|
37
|
+
clangArgs: ["-I", "/absolute/path/to/yoga"],
|
|
38
|
+
functions: [
|
|
39
|
+
"YGNodeNew",
|
|
40
|
+
"YGNodeStyleSetWidth",
|
|
41
|
+
"YGNodeLayoutGetWidth",
|
|
42
|
+
"YGNodeFreeRecursive",
|
|
43
|
+
],
|
|
44
|
+
} satisfies BindingOptions;
|
|
45
|
+
|
|
46
|
+
const files = await generateBindings(options, import.meta.dir);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The returned file map contains:
|
|
50
|
+
|
|
51
|
+
| File | Purpose |
|
|
52
|
+
| -------------------------- | -------------------------------------------------------------- |
|
|
53
|
+
| `native.ts` | Compile with Static Hermes; installs `globalThis.layout` |
|
|
54
|
+
| `native.d.ts` | Target-specific global declaration for checking `native.ts` |
|
|
55
|
+
| `api.d.ts` | Exported application types, including distinct pointer types |
|
|
56
|
+
| `ffigen_layout_adapters.h` | C adapters that normalize enum parameters/results to `int32_t` |
|
|
57
|
+
|
|
58
|
+
Shared Hermes declarations (`c_ptr`, the C scalar types, and `$SHBuiltin`) live
|
|
59
|
+
once in `react-nitro-ffigen/hermes`. Generated `native.d.ts` files import those
|
|
60
|
+
types automatically. The project containing generated bindings must have
|
|
61
|
+
`react-nitro-ffigen` installed; targets do not copy or maintain Hermes types.
|
|
62
|
+
|
|
63
|
+
Scalar and pointer calls use the original C prototypes through their public
|
|
64
|
+
header. Enum adapters let the C compiler handle the actual enum ABI, including
|
|
65
|
+
short enums, without assuming that all enums are C `int`. Constants come from
|
|
66
|
+
Clang's evaluated AST, including declarations produced by macros. Typedef chains
|
|
67
|
+
are resolved; mutable pointers can be used as const pointers, but not vice versa.
|
|
68
|
+
Different struct/union handles are incompatible in the public TypeScript API.
|
|
69
|
+
Boolean wrappers convert between JS booleans and Hermes' numeric `c_bool`.
|
|
70
|
+
|
|
71
|
+
Generation completes before any output files are written. A missing function,
|
|
72
|
+
unsupported selected type, or Clang failure does not silently omit a binding.
|
|
73
|
+
`generateBindings(options, cwd)` returns the
|
|
74
|
+
four files as a `Record<string, string>` without writing them. The required `cwd`
|
|
75
|
+
sets Clang's working directory for relative paths; the generator leaves the
|
|
76
|
+
parent process's working directory unchanged.
|
|
77
|
+
|
|
78
|
+
Import public types from the generated `api.d.ts` module:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import type { YGNodeRef } from "./generated/api";
|
|
82
|
+
|
|
83
|
+
const node: YGNodeRef = globalThis.layout.YGNodeNew();
|
|
84
|
+
globalThis.layout.YGNodeFreeRecursive(node);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The type import also supplies the declaration for the runtime global. It does not
|
|
88
|
+
load the compiled library; native initialization must evaluate the Hermes unit
|
|
89
|
+
before application code runs. Public C types are module exports and require no
|
|
90
|
+
global type namespace or triple-slash reference.
|
|
91
|
+
|
|
92
|
+
## Native C adapters
|
|
93
|
+
|
|
94
|
+
An adapter is a generated C wrapper that exposes a signature Hermes can call
|
|
95
|
+
and invokes the original library function using its declared C types. Adapter
|
|
96
|
+
implementations live in `src/adapters/`; they use the resolved binding descriptions
|
|
97
|
+
in `src/binding-types.ts`, while `src/clang-ast-types.ts` describes Clang's input AST.
|
|
98
|
+
|
|
99
|
+
The [enum adapter](src/adapters/enum.ts) handles functions with enum parameters
|
|
100
|
+
or return values. Hermes can represent the numeric values, but mapping every C
|
|
101
|
+
enum directly to `c_int` would assume its underlying integer type. Compiler flags
|
|
102
|
+
such as `-fshort-enums` can change that type. Instead, the generated wrapper uses
|
|
103
|
+
`int32_t` at the Hermes boundary (`c_i32` in the binding) and casts to/from the
|
|
104
|
+
original enum. For example:
|
|
105
|
+
|
|
106
|
+
```c
|
|
107
|
+
static inline void ffigen_yoga_YGNodeStyleSetFlexDirection(
|
|
108
|
+
YGNodeRef node, int32_t direction) {
|
|
109
|
+
YGNodeStyleSetFlexDirection(node, (YGFlexDirection)direction);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The C compiler handles the enum's actual calling convention. Compile the adapter
|
|
114
|
+
with the same target and enum-related flags as the library. Generation rejects
|
|
115
|
+
enum constants outside the signed 32-bit range so the adapter can preserve them.
|
|
116
|
+
|
|
117
|
+
Future APIs may need other adapters, such as for structs passed or returned by
|
|
118
|
+
value. Those require a concrete design for how JS supplies or reads the fields
|
|
119
|
+
and how native storage is managed. Struct values currently remain unsupported;
|
|
120
|
+
opaque struct pointers already work. Add a separate adapter when an API requires
|
|
121
|
+
it, using the enum module as the first concrete implementation.
|
|
122
|
+
|
|
123
|
+
## Compile and consume
|
|
124
|
+
|
|
125
|
+
Check the native source separately from the application declarations:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
bun packages/react-nitro-ffigen/src/tsc.ts generated/native.ts
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The check extends `config/tsconfig.json`, selects the supplied files, and disables
|
|
132
|
+
emission and composite builds. The build pipeline and generator tests use this
|
|
133
|
+
same check.
|
|
134
|
+
|
|
135
|
+
Compile it using the pinned Hermes compiler:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
packages/react-nitro-hermes/.build/hermes-cmake/bin/shermes \
|
|
139
|
+
-typed -parse-flow -O -emit-c -exported-unit=layout \
|
|
140
|
+
-o generated/layout.c generated/native.ts
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Compile that C with the generated directory, library headers, and matching Hermes
|
|
144
|
+
headers on the include path. Link it with the native library and Hermes, then
|
|
145
|
+
load it with `evaluateSHUnit(sh_export_layout)`, as the Yoga integration does.
|
|
146
|
+
|
|
147
|
+
Import public types from `api.d.ts` in application code. The loaded unit supplies
|
|
148
|
+
the runtime object:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import type { YGNodeRef } from "./generated/api";
|
|
152
|
+
const node: YGNodeRef = globalThis.layout.YGNodeNew();
|
|
153
|
+
globalThis.layout.YGNodeStyleSetWidth(node, 50);
|
|
154
|
+
globalThis.layout.YGNodeFreeRecursive(node);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Do not include `native.ts` or `native.d.ts` in that app project, or send the native
|
|
158
|
+
source through a bundler that erases its annotations. The generated source uses
|
|
159
|
+
the shared TypeScript/Flow syntax supported by the pinned compiler. Ambient
|
|
160
|
+
declarations are separate because `shermes` does not execute them.
|
|
161
|
+
|
|
162
|
+
Use the same library version, target, SDK, and preprocessor flags for generation
|
|
163
|
+
and native compilation. Generate separately for different target ABIs; typedefs
|
|
164
|
+
such as `size_t` are resolved for the target passed to Clang. `clang` can be
|
|
165
|
+
overridden in the config.
|
|
166
|
+
|
|
167
|
+
This is a low-level API: ownership and freeing remain explicit. Handles are opaque
|
|
168
|
+
native values, not JS objects with fields or numeric addresses to manipulate.
|
|
169
|
+
No nullability, error-code, lifetime, or garbage-collection policy is inferred.
|
|
170
|
+
Integer values use JS numbers and Hermes' conversions, so full-range 64-bit
|
|
171
|
+
integers are not supported. Enums accept their declared values; arbitrary bitmask
|
|
172
|
+
combinations need a future API-specific policy. No struct layout offsets or
|
|
173
|
+
general macro constants are generated.
|
|
174
|
+
|
|
175
|
+
## Yoga package (macOS)
|
|
176
|
+
|
|
177
|
+
The shared builder in `src/build.ts` generates and checks bindings, builds a
|
|
178
|
+
native CMake target, and emits an exported Static Hermes C unit. A target's
|
|
179
|
+
`ffi.config.ts` uses the `TargetConfig` type from `react-nitro-ffigen/build` and
|
|
180
|
+
extends the binding config with its native build settings:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
native: {
|
|
184
|
+
sourceDir: 'yoga/yoga',
|
|
185
|
+
target: 'yogacore',
|
|
186
|
+
},
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Paths are relative to the config directory. Invoke the builder with any config:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
bun packages/react-nitro-ffigen/src/build.ts packages/react-nitro-yoga/ffi.config.ts
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The [React Nitro Yoga package](../react-nitro-yoga/README.md) owns its pinned
|
|
196
|
+
`yoga` submodule, `ffi.config.ts`, Swift package, and generated bindings. New
|
|
197
|
+
libraries can use this generator from their own packages.
|
|
198
|
+
|
|
199
|
+
Initialize the submodules and build from the repository root:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
git submodule update --init --recursive
|
|
203
|
+
bun install
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The root postinstall runs workspace builds in dependency order, building the
|
|
207
|
+
pinned compiler in `react-nitro-hermes` before generating and compiling Yoga
|
|
208
|
+
in `react-nitro-yoga`. To rebuild only Yoga, run
|
|
209
|
+
`bun run --cwd packages/react-nitro-yoga build`. Native builds are incremental;
|
|
210
|
+
standalone target builds require an existing compiler.
|
|
211
|
+
Yoga outputs are under `packages/react-nitro-yoga/Generated/`, with the host
|
|
212
|
+
native library at `packages/react-nitro-yoga/.build/yoga/libyogacore.a`.
|
|
213
|
+
|
|
214
|
+
Import Yoga's public types from `react-nitro-yoga`:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import type { YGNodeRef } from "react-nitro-yoga";
|
|
218
|
+
|
|
219
|
+
const node: YGNodeRef = globalThis.yoga.YGNodeNew();
|
|
220
|
+
globalThis.yoga.YGNodeFreeRecursive(node);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Declare `react-nitro-yoga` as a dependency of consumers. Its Swift package
|
|
224
|
+
exposes native `Yoga` separately from `ReactNitroYoga`, which initializes the
|
|
225
|
+
compiled unit in each runtime. The renderer consumes native Yoga and the public
|
|
226
|
+
types; runtime creation calls the initializer in `react-nitro-yoga`.
|
|
227
|
+
|
|
228
|
+
## Checks
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
bun run --cwd packages/react-nitro-ffigen typecheck
|
|
232
|
+
bun run --cwd packages/react-nitro-ffigen test
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Tests check public pointer/const/enum types, compile multiple targets using the
|
|
236
|
+
shared Hermes declarations, execute C enum adapters under `-fshort-enums`, and
|
|
237
|
+
verify that unsupported APIs and Clang errors are reported.
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-nitro-ffigen",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1-alpha.7",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"react-nitro-hermes": "0.0.1-alpha.7",
|
|
6
|
+
"typescript": "7.0.2"
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"tag": "latest",
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/generate.ts",
|
|
15
|
+
"./hermes": {
|
|
16
|
+
"types": "./src/hermes.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./build": "./src/build.ts",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/margelo/react-nitro.git",
|
|
25
|
+
"directory": "packages/react-nitro-ffigen"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FunctionBinding } from '../binding-types';
|
|
2
|
+
|
|
3
|
+
interface EnumAdapter {
|
|
4
|
+
symbol: string;
|
|
5
|
+
lines: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a C wrapper when a function takes or returns an enum.
|
|
10
|
+
*
|
|
11
|
+
* Hermes represents our enum values as c_i32, but a C enum's underlying integer
|
|
12
|
+
* type depends on the target and compiler flags (for example, -fshort-enums).
|
|
13
|
+
* The wrapper exposes int32_t to Hermes and casts to/from the declared C types.
|
|
14
|
+
* Compiling it with the library's headers and flags lets the C compiler handle
|
|
15
|
+
* the actual enum ABI. Type resolution rejects constants outside int32_t range.
|
|
16
|
+
* Functions without enum parameters or results need no enum adapter.
|
|
17
|
+
*/
|
|
18
|
+
export function generateEnumAdapter(
|
|
19
|
+
binding: FunctionBinding,
|
|
20
|
+
targetName: string,
|
|
21
|
+
): EnumAdapter | undefined {
|
|
22
|
+
const hasEnumParameter = binding.parameters.some(parameter => parameter.type.kind === 'enum');
|
|
23
|
+
const needsAdapter = binding.result.kind === 'enum' || hasEnumParameter;
|
|
24
|
+
if (needsAdapter === false) return undefined;
|
|
25
|
+
|
|
26
|
+
const symbol = `ffigen_${targetName}_${binding.name}`;
|
|
27
|
+
const cParameters: string[] = [];
|
|
28
|
+
const cArguments: string[] = [];
|
|
29
|
+
for (const { name, type } of binding.parameters) {
|
|
30
|
+
const cType = type.kind === 'enum' ? 'int32_t' : type.c;
|
|
31
|
+
cParameters.push(`${cType} ${name}`);
|
|
32
|
+
cArguments.push(`(${type.c})${name}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { result } = binding;
|
|
36
|
+
const cResult = result.kind === 'enum' ? 'int32_t' : result.c;
|
|
37
|
+
const cParams = cParameters.length === 0 ? 'void' : cParameters.join(', ');
|
|
38
|
+
const cArgs = cArguments.join(', ');
|
|
39
|
+
const cCall = `${binding.name}(${cArgs})`;
|
|
40
|
+
const statement = result.kind === 'void' ? `${cCall};` : `return (${cResult})${cCall};`;
|
|
41
|
+
const lines = [
|
|
42
|
+
`static inline ${cResult} ${symbol}(${cParams}) {`,
|
|
43
|
+
` ${statement}`,
|
|
44
|
+
'}',
|
|
45
|
+
'',
|
|
46
|
+
];
|
|
47
|
+
return { symbol, lines };
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Resolved binding descriptions shared by the generator and C adapters. */
|
|
2
|
+
export interface Type {
|
|
3
|
+
kind: 'scalar' | 'boolean' | 'pointer' | 'enum' | 'void';
|
|
4
|
+
native: string;
|
|
5
|
+
public: string;
|
|
6
|
+
c: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Parameter {
|
|
10
|
+
name: string;
|
|
11
|
+
type: Type;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FunctionBinding {
|
|
15
|
+
name: string;
|
|
16
|
+
parameters: Parameter[];
|
|
17
|
+
result: Type;
|
|
18
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { hermesBuildDirectory as hermesBuild, shermesPath as compiler } from 'react-nitro-hermes';
|
|
5
|
+
import { generateBindings } from './generate';
|
|
6
|
+
import type { BindingConfig } from './generate';
|
|
7
|
+
import { tscPath as tsc, typecheck } from './tsc';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This build script takes a target's ffigen configuration and builds the corresponding native library and TypeScript bindings.
|
|
11
|
+
* It's the build step for "turning a native library expsed by a C header into TypeScript bindings, using shermes ffi".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface TargetConfig extends BindingConfig {
|
|
15
|
+
native: {
|
|
16
|
+
/** CMake source directory, relative to the config file. */
|
|
17
|
+
sourceDir: string;
|
|
18
|
+
/** CMake target that builds the native library. */
|
|
19
|
+
target: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function run(command: string[], cwd?: string): Promise<void> {
|
|
24
|
+
const child = Bun.spawn(command, { cwd, stdout: 'inherit', stderr: 'inherit' });
|
|
25
|
+
const code = await child.exited;
|
|
26
|
+
if (code !== 0) throw new Error(`${command[0]} exited with status ${code}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function buildTarget(configPath: string) {
|
|
30
|
+
// Prepare all paths:
|
|
31
|
+
const path = resolve(configPath);
|
|
32
|
+
const url = pathToFileURL(path);
|
|
33
|
+
const module = await import(url.href);
|
|
34
|
+
const config: TargetConfig = module.default;
|
|
35
|
+
const targetDirectory = dirname(path);
|
|
36
|
+
const nativeSource = resolve(targetDirectory, config.native.sourceDir);
|
|
37
|
+
const cmakeFile = join(nativeSource, 'CMakeLists.txt');
|
|
38
|
+
try {
|
|
39
|
+
await access(cmakeFile);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`Cannot read ${cmakeFile}. Initialize target submodules with git submodule update --init --recursive`);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await access(compiler);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`Shermes compiler not found at ${compiler}. Run bun run --cwd packages/react-nitro-hermes build from the repository root`);
|
|
47
|
+
}
|
|
48
|
+
const directory = resolve(targetDirectory, '.build');
|
|
49
|
+
const generated = resolve(targetDirectory, config.outDir);
|
|
50
|
+
const nativeBuild = join(directory, config.name);
|
|
51
|
+
|
|
52
|
+
// Step 1: compile the actual native library (for example yoga):
|
|
53
|
+
// Configure a Release build of the target's native library using Ninja.
|
|
54
|
+
await run(['cmake', '-S', nativeSource, '-B', nativeBuild, '-G', 'Ninja', '-DCMAKE_BUILD_TYPE=Release']);
|
|
55
|
+
// Compile the configured native library
|
|
56
|
+
await run(['cmake', '--build', nativeBuild, '--target', config.native.target, '--parallel', '8']);
|
|
57
|
+
|
|
58
|
+
// Step 2: Generate the TypeScript bindings from the C header:
|
|
59
|
+
const files = await generateBindings(config, targetDirectory);
|
|
60
|
+
await mkdir(generated, { recursive: true });
|
|
61
|
+
const entries = Object.entries(files);
|
|
62
|
+
for (const [name, contents] of entries) {
|
|
63
|
+
const path = join(generated, name);
|
|
64
|
+
await writeFile(path, contents);
|
|
65
|
+
}
|
|
66
|
+
const nativePath = join(generated, 'native.ts');
|
|
67
|
+
// Type-check the generated FFI wrappers without emitting JavaScript.
|
|
68
|
+
await typecheck([nativePath]);
|
|
69
|
+
|
|
70
|
+
// Step 3: Turn typescript bindings into shermes C code:
|
|
71
|
+
const nativeC_OutputPath = join(generated, `${config.name}.c`);
|
|
72
|
+
const relativeNativeC = relative(targetDirectory, nativeC_OutputPath);
|
|
73
|
+
const relativeNativePath = relative(targetDirectory, nativePath);
|
|
74
|
+
// Emit the optimized FFI wrappers as an exported C unit for native compilation.
|
|
75
|
+
await run([
|
|
76
|
+
compiler, '-typed', '-parse-flow', '-O', '-g0', '-emit-c', `-exported-unit=${config.name}`,
|
|
77
|
+
'-o', relativeNativeC, relativeNativePath,
|
|
78
|
+
], targetDirectory);
|
|
79
|
+
console.log(`${config.name} target built in ${directory}`);
|
|
80
|
+
|
|
81
|
+
// The platform build pipeline will integrate the shermes c code into the final build
|
|
82
|
+
return { compiler, directory, generated, hermesBuild, nativePath, tsc, nativeBuild };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (import.meta.main) {
|
|
86
|
+
const argument = process.argv[2];
|
|
87
|
+
if (argument === undefined || process.argv.length !== 3) {
|
|
88
|
+
console.error('Usage: bun packages/react-nitro-ffigen/src/build.ts path/to/ffi.config.ts');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await buildTarget(argument);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
console.error(message);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subset of Clang's `-ast-dump=json` output consumed by the generator.
|
|
3
|
+
* These types describe the parsed C declarations.
|
|
4
|
+
*/
|
|
5
|
+
export interface AstType {
|
|
6
|
+
qualType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AstNode {
|
|
10
|
+
kind: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
type?: AstType;
|
|
13
|
+
inner?: AstNode[];
|
|
14
|
+
value?: string;
|
|
15
|
+
variadic?: boolean;
|
|
16
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { generateEnumAdapter } from './adapters/enum';
|
|
5
|
+
import type { FunctionBinding, Parameter, Type } from './binding-types';
|
|
6
|
+
import type { AstNode } from './clang-ast-types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The main generator that turns a c header into TypeScript bindings.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface BindingOptions {
|
|
13
|
+
/** C include spelling, for example yoga/Yoga.h. */
|
|
14
|
+
header: string;
|
|
15
|
+
/** Global object installed by the compiled Hermes unit. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Explicit public function allowlist. Unsupported selections are errors. */
|
|
18
|
+
functions: string[];
|
|
19
|
+
/** Include paths, defines, target and SDK flags for the actual library build. */
|
|
20
|
+
clangArgs?: string[];
|
|
21
|
+
clang?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BindingConfig extends BindingOptions {
|
|
25
|
+
outDir: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Map of c-scalar types to shermes ffi type names
|
|
29
|
+
const nativeTypes = new Map([
|
|
30
|
+
['char', 'c_char'],
|
|
31
|
+
['signed char', 'c_schar'],
|
|
32
|
+
['unsigned char', 'c_uchar'],
|
|
33
|
+
['short', 'c_short'],
|
|
34
|
+
['unsigned short', 'c_ushort'],
|
|
35
|
+
['int', 'c_int'],
|
|
36
|
+
['unsigned int', 'c_uint'],
|
|
37
|
+
['long', 'c_long'],
|
|
38
|
+
['unsigned long', 'c_ulong'],
|
|
39
|
+
['long long', 'c_longlong'],
|
|
40
|
+
['unsigned long long', 'c_ulonglong'],
|
|
41
|
+
['float', 'c_float'],
|
|
42
|
+
['double', 'c_double'],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function identifier(value: string): void {
|
|
46
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) return;
|
|
47
|
+
throw new Error(`Invalid identifier: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function constantValue(node: AstNode): string | undefined {
|
|
51
|
+
if (node.kind === 'ConstantExpr' && node.value !== undefined) {
|
|
52
|
+
return node.value;
|
|
53
|
+
}
|
|
54
|
+
for (const child of node.inner ?? []) {
|
|
55
|
+
const value = constantValue(child);
|
|
56
|
+
if (value !== undefined) return value;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function wrapperType(type: Type): string {
|
|
62
|
+
switch (type.kind) {
|
|
63
|
+
case 'pointer': return 'c_ptr';
|
|
64
|
+
case 'boolean': return 'boolean';
|
|
65
|
+
case 'void': return 'void';
|
|
66
|
+
default: return 'number';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* To parse a library's C header we use clang, which gives us a structured JSON representation.
|
|
72
|
+
*/
|
|
73
|
+
async function parseHeader(options: BindingOptions, cwd: string): Promise<AstNode> {
|
|
74
|
+
if (/^[A-Za-z0-9_./+-]+$/.test(options.header) === false) {
|
|
75
|
+
throw new Error('header must be a C include path without quotes or newlines');
|
|
76
|
+
}
|
|
77
|
+
const temporaryRoot = tmpdir();
|
|
78
|
+
const prefix = join(temporaryRoot, 'shermes-ffigen-');
|
|
79
|
+
const directory = await mkdtemp(prefix);
|
|
80
|
+
try {
|
|
81
|
+
// We create a temporary c file that include's the library's header
|
|
82
|
+
const input = join(directory, 'input.c');
|
|
83
|
+
await writeFile(input, `#include <${options.header}>\n`);
|
|
84
|
+
// We use clang to get an AST dump of the library's header file
|
|
85
|
+
const command = [
|
|
86
|
+
options.clang ?? 'clang', '-x', 'c', '-std=c11',
|
|
87
|
+
...options.clangArgs ?? [],
|
|
88
|
+
'-Xclang', '-ast-dump=json', '-fsyntax-only', input,
|
|
89
|
+
];
|
|
90
|
+
const process = Bun.spawn(command, { cwd, stdout: 'pipe', stderr: 'pipe' });
|
|
91
|
+
const output = new Response(process.stdout);
|
|
92
|
+
const errors = new Response(process.stderr);
|
|
93
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
94
|
+
output.text(), errors.text(), process.exited,
|
|
95
|
+
]);
|
|
96
|
+
if (exitCode !== 0) {
|
|
97
|
+
throw new Error(`Clang could not parse ${options.header}:\n${stderr}`);
|
|
98
|
+
}
|
|
99
|
+
return JSON.parse(stdout) as AstNode;
|
|
100
|
+
} finally {
|
|
101
|
+
await rm(directory, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function generateBindings(
|
|
106
|
+
options: BindingOptions,
|
|
107
|
+
cwd: string,
|
|
108
|
+
): Promise<Record<string, string>> {
|
|
109
|
+
identifier(options.name);
|
|
110
|
+
if (options.functions.length === 0) {
|
|
111
|
+
throw new Error('Select at least one function');
|
|
112
|
+
}
|
|
113
|
+
const selected = new Set(options.functions);
|
|
114
|
+
if (selected.size !== options.functions.length) {
|
|
115
|
+
throw new Error('The function allowlist contains duplicates');
|
|
116
|
+
}
|
|
117
|
+
const ast = await parseHeader(options, cwd);
|
|
118
|
+
const declarations = new Map<string, AstNode>();
|
|
119
|
+
const enums = new Map<string, AstNode>();
|
|
120
|
+
const typedefs = new Map<string, AstNode>();
|
|
121
|
+
for (const node of ast.inner ?? []) {
|
|
122
|
+
if (node.name === undefined) continue;
|
|
123
|
+
if (node.kind === 'FunctionDecl') declarations.set(node.name, node);
|
|
124
|
+
if (node.kind === 'TypedefDecl') typedefs.set(node.name, node);
|
|
125
|
+
if (node.kind === 'EnumDecl') enums.set(node.name, node);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const publicTypes = new Map<string, string>();
|
|
129
|
+
const constants = new Map<string, string>();
|
|
130
|
+
|
|
131
|
+
function resolveType(spelling: string, visiting = new Set<string>()): Type {
|
|
132
|
+
const trimmed = spelling.trim();
|
|
133
|
+
// Top-level const has no effect for scalar values passed by value.
|
|
134
|
+
const name = trimmed.endsWith('*') ? trimmed : trimmed.replace(/^const /, '');
|
|
135
|
+
const native = nativeTypes.get(name);
|
|
136
|
+
if (native !== undefined) {
|
|
137
|
+
return { kind: 'scalar', native, public: 'number', c: spelling };
|
|
138
|
+
}
|
|
139
|
+
if (name === '_Bool' || name === 'bool') {
|
|
140
|
+
return { kind: 'boolean', native: 'c_bool', public: 'boolean', c: spelling };
|
|
141
|
+
}
|
|
142
|
+
if (name === 'void') {
|
|
143
|
+
return { kind: 'void', native: 'void', public: 'void', c: spelling };
|
|
144
|
+
}
|
|
145
|
+
const typedef = typedefs.get(name);
|
|
146
|
+
if (typedef?.type !== undefined) {
|
|
147
|
+
if (visiting.has(name)) throw new Error(`Recursive typedef: ${name}`);
|
|
148
|
+
visiting.add(name);
|
|
149
|
+
const resolved = resolveType(typedef.type.qualType, visiting);
|
|
150
|
+
if (resolved.public !== name) publicTypes.set(name, resolved.public);
|
|
151
|
+
return { ...resolved, public: name, c: spelling };
|
|
152
|
+
}
|
|
153
|
+
const pointer = /^(const )?(struct|union) ([A-Za-z_][A-Za-z0-9_]*) \*$/.exec(name);
|
|
154
|
+
if (pointer !== null) {
|
|
155
|
+
const record = `${pointer[2]} ${pointer[3]}`;
|
|
156
|
+
const constant = pointer[1] === undefined ? 'false' : 'boolean';
|
|
157
|
+
const publicType = `Pointer<"${record}", ${constant}>`;
|
|
158
|
+
return { kind: 'pointer', native: 'c_ptr', public: publicType, c: spelling };
|
|
159
|
+
}
|
|
160
|
+
if (name.startsWith('enum ')) {
|
|
161
|
+
const enumName = name.slice(5);
|
|
162
|
+
const enumeration = enums.get(enumName);
|
|
163
|
+
if (enumeration === undefined) throw new Error(`Incomplete enum: ${name}`);
|
|
164
|
+
const values: string[] = [];
|
|
165
|
+
let next = 0n;
|
|
166
|
+
for (const member of enumeration.inner ?? []) {
|
|
167
|
+
if (member.kind !== 'EnumConstantDecl' || member.name === undefined) continue;
|
|
168
|
+
const explicit = constantValue(member);
|
|
169
|
+
const value = explicit === undefined ? next : BigInt(explicit);
|
|
170
|
+
// The enum adapter exposes int32_t to Hermes; every constant must fit.
|
|
171
|
+
if (value < -2147483648n || value > 2147483647n) {
|
|
172
|
+
throw new Error(`Enum ${enumName} exceeds the supported signed 32-bit range`);
|
|
173
|
+
}
|
|
174
|
+
const text = value.toString();
|
|
175
|
+
constants.set(member.name, text);
|
|
176
|
+
values.push(text);
|
|
177
|
+
next = value + 1n;
|
|
178
|
+
}
|
|
179
|
+
if (values.length === 0) throw new Error(`Empty enum: ${enumName}`);
|
|
180
|
+
const unique = new Set(values);
|
|
181
|
+
const literals = [...unique];
|
|
182
|
+
const publicType = literals.join(' | ');
|
|
183
|
+
publicTypes.set(enumName, publicType);
|
|
184
|
+
return { kind: 'enum', native: 'c_i32', public: enumName, c: spelling };
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Unsupported C type "${spelling}"; v1 supports scalars, enums and opaque struct/union pointers`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const functions: FunctionBinding[] = [];
|
|
190
|
+
for (const name of options.functions) {
|
|
191
|
+
identifier(name);
|
|
192
|
+
const declaration = declarations.get(name);
|
|
193
|
+
if (declaration?.type === undefined) throw new Error(`Function not found: ${name}`);
|
|
194
|
+
const signature = declaration.type.qualType;
|
|
195
|
+
if (declaration.variadic || signature.includes('...')) {
|
|
196
|
+
throw new Error(`${name}: variadic functions are not supported`);
|
|
197
|
+
}
|
|
198
|
+
if (signature.endsWith('()')) {
|
|
199
|
+
throw new Error(`${name}: a full C function prototype is required`);
|
|
200
|
+
}
|
|
201
|
+
if (signature.includes('__attribute__')) {
|
|
202
|
+
throw new Error(`${name}: attributed calling conventions are not supported`);
|
|
203
|
+
}
|
|
204
|
+
const opening = signature.indexOf('(');
|
|
205
|
+
if (opening < 0 || signature.includes('(*')) {
|
|
206
|
+
throw new Error(`${name}: unsupported function signature ${signature}`);
|
|
207
|
+
}
|
|
208
|
+
const resultSpelling = signature.slice(0, opening);
|
|
209
|
+
try {
|
|
210
|
+
const result = resolveType(resultSpelling);
|
|
211
|
+
const parameters: Parameter[] = [];
|
|
212
|
+
for (const parameter of declaration.inner ?? []) {
|
|
213
|
+
if (parameter.kind !== 'ParmVarDecl' || parameter.type === undefined) continue;
|
|
214
|
+
const parameterName = parameter.name ?? `arg${parameters.length}`;
|
|
215
|
+
const type = resolveType(parameter.type.qualType);
|
|
216
|
+
parameters.push({ name: `_${parameterName}`, type });
|
|
217
|
+
}
|
|
218
|
+
functions.push({ name, parameters, result });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
221
|
+
throw new Error(`${name}: ${message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const source: string[] = [
|
|
226
|
+
'// Generated by react-nitro-ffigen. Do not edit.',
|
|
227
|
+
'/// <reference path="./native.d.ts" />',
|
|
228
|
+
'"use strict";',
|
|
229
|
+
'',
|
|
230
|
+
];
|
|
231
|
+
const nativeApi: string[] = [];
|
|
232
|
+
const publicApi: string[] = [];
|
|
233
|
+
const adapterName = `ffigen_${options.name}_adapters.h`;
|
|
234
|
+
const adapters = [
|
|
235
|
+
'// Generated by react-nitro-ffigen. Do not edit.',
|
|
236
|
+
'#pragma once',
|
|
237
|
+
'#include <stdint.h>',
|
|
238
|
+
`#include <${options.header}>`,
|
|
239
|
+
'',
|
|
240
|
+
];
|
|
241
|
+
const body = ['(function () {'];
|
|
242
|
+
const exported: string[] = [];
|
|
243
|
+
|
|
244
|
+
for (const binding of functions) {
|
|
245
|
+
const adapter = generateEnumAdapter(binding, options.name);
|
|
246
|
+
let symbol = binding.name;
|
|
247
|
+
let include = options.header;
|
|
248
|
+
if (adapter !== undefined) {
|
|
249
|
+
symbol = adapter.symbol;
|
|
250
|
+
include = adapterName;
|
|
251
|
+
adapters.push(...adapter.lines);
|
|
252
|
+
}
|
|
253
|
+
const index = exported.length;
|
|
254
|
+
const nativeParameters: string[] = [];
|
|
255
|
+
const wrapperParameters: string[] = [];
|
|
256
|
+
const publicParameters: string[] = [];
|
|
257
|
+
const argumentsList: string[] = [];
|
|
258
|
+
const conversions: string[] = [];
|
|
259
|
+
for (const parameter of binding.parameters) {
|
|
260
|
+
const { name, type } = parameter;
|
|
261
|
+
nativeParameters.push(`${name}: ${type.native}`);
|
|
262
|
+
const parameterType = wrapperType(type);
|
|
263
|
+
wrapperParameters.push(`${name}: ${parameterType}`);
|
|
264
|
+
publicParameters.push(`${name}: ${type.public}`);
|
|
265
|
+
if (type.kind === 'boolean') {
|
|
266
|
+
const converted = `nativeArg${argumentsList.length}`;
|
|
267
|
+
conversions.push(` const ${converted} = ${name} ? 1 : 0;`);
|
|
268
|
+
argumentsList.push(converted);
|
|
269
|
+
} else {
|
|
270
|
+
argumentsList.push(name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const nativeParams = nativeParameters.join(', ');
|
|
274
|
+
const wrapperParams = wrapperParameters.join(', ');
|
|
275
|
+
const publicParams = publicParameters.join(', ');
|
|
276
|
+
const argumentsText = argumentsList.join(', ');
|
|
277
|
+
const { result } = binding;
|
|
278
|
+
const wrapperResult = wrapperType(result);
|
|
279
|
+
nativeApi.push(` ${binding.name}: (${wrapperParams}) => ${wrapperResult};`);
|
|
280
|
+
publicApi.push(` ${binding.name}(${publicParams}): ${result.public};`);
|
|
281
|
+
body.push(
|
|
282
|
+
` const native_${index} = $SHBuiltin.extern_c(`,
|
|
283
|
+
` { include: "${include}" },`,
|
|
284
|
+
` function ${symbol}(${nativeParams}): ${result.native} { throw 0; },`,
|
|
285
|
+
' );',
|
|
286
|
+
` function call_${index}(${wrapperParams}): ${wrapperResult} {`,
|
|
287
|
+
...conversions,
|
|
288
|
+
);
|
|
289
|
+
const call = `native_${index}(${argumentsText})`;
|
|
290
|
+
if (result.kind === 'boolean') {
|
|
291
|
+
body.push(` const result = ${call};`, ' return result !== 0;');
|
|
292
|
+
} else if (result.kind === 'void') {
|
|
293
|
+
body.push(` ${call};`);
|
|
294
|
+
} else {
|
|
295
|
+
body.push(` return ${call};`);
|
|
296
|
+
}
|
|
297
|
+
body.push(' }', '');
|
|
298
|
+
exported.push(`${binding.name}: call_${index}`);
|
|
299
|
+
}
|
|
300
|
+
for (const [name, value] of constants) {
|
|
301
|
+
if (selected.has(name)) throw new Error(`Enum constant conflicts with function: ${name}`);
|
|
302
|
+
nativeApi.push(` ${name}: number;`);
|
|
303
|
+
publicApi.push(` readonly ${name}: ${value};`);
|
|
304
|
+
exported.push(`${name}: ${value}`);
|
|
305
|
+
}
|
|
306
|
+
const exportList = exported.join(', ');
|
|
307
|
+
source.push(...body);
|
|
308
|
+
source.push(` globalThis.${options.name} = { ${exportList} };`, '})();', '');
|
|
309
|
+
const types = [
|
|
310
|
+
'// Generated public types. Import in the app, not the native compilation.',
|
|
311
|
+
'declare const pointer: unique symbol;',
|
|
312
|
+
'type Pointer<Record extends string, Const extends boolean> = {',
|
|
313
|
+
' readonly [pointer]: readonly [Record, Const];',
|
|
314
|
+
'};',
|
|
315
|
+
'',
|
|
316
|
+
];
|
|
317
|
+
for (const [name, type] of publicTypes) types.push(`export type ${name} = ${type};`);
|
|
318
|
+
types.push(
|
|
319
|
+
'',
|
|
320
|
+
'export interface API {',
|
|
321
|
+
...publicApi,
|
|
322
|
+
'}',
|
|
323
|
+
'',
|
|
324
|
+
'declare global {',
|
|
325
|
+
` var ${options.name}: API;`,
|
|
326
|
+
'}',
|
|
327
|
+
'',
|
|
328
|
+
);
|
|
329
|
+
const nativeDeclaration = [
|
|
330
|
+
'// Generated native API types. Include only when checking native.ts.',
|
|
331
|
+
'import type {} from "react-nitro-ffigen/hermes";',
|
|
332
|
+
'',
|
|
333
|
+
'declare global {',
|
|
334
|
+
` var ${options.name}: {`,
|
|
335
|
+
...nativeApi,
|
|
336
|
+
' };',
|
|
337
|
+
'}',
|
|
338
|
+
'',
|
|
339
|
+
];
|
|
340
|
+
return {
|
|
341
|
+
'native.ts': source.join('\n'),
|
|
342
|
+
'api.d.ts': types.join('\n'),
|
|
343
|
+
'native.d.ts': nativeDeclaration.join('\n'),
|
|
344
|
+
[adapterName]: adapters.join('\n'),
|
|
345
|
+
};
|
|
346
|
+
}
|
package/src/hermes.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shermes only works properly with flow typed files, however, we want to consume the binding JS code
|
|
3
|
+
* as properly typed TypeScript definitions.
|
|
4
|
+
* This provides definitions currently missing in shermes.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
type c_char = number;
|
|
10
|
+
type c_schar = number;
|
|
11
|
+
type c_uchar = number;
|
|
12
|
+
type c_short = number;
|
|
13
|
+
type c_ushort = number;
|
|
14
|
+
type c_int = number;
|
|
15
|
+
type c_uint = number;
|
|
16
|
+
type c_long = number;
|
|
17
|
+
type c_ulong = number;
|
|
18
|
+
type c_longlong = number;
|
|
19
|
+
type c_ulonglong = number;
|
|
20
|
+
type c_float = number;
|
|
21
|
+
type c_double = number;
|
|
22
|
+
type c_bool = number;
|
|
23
|
+
type c_i32 = number;
|
|
24
|
+
const nativePointer: unique symbol;
|
|
25
|
+
type c_ptr = { readonly [nativePointer]: true };
|
|
26
|
+
|
|
27
|
+
const $SHBuiltin: {
|
|
28
|
+
extern_c<T extends (...args: any[]) => any>(
|
|
29
|
+
options: { include: string },
|
|
30
|
+
signature: T,
|
|
31
|
+
): T;
|
|
32
|
+
};
|
|
33
|
+
}
|
package/src/tsc.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import typescript from 'typescript/package.json';
|
|
6
|
+
|
|
7
|
+
const packageURL = import.meta.resolve('typescript/package.json');
|
|
8
|
+
const executableURL = new URL(typescript.bin.tsc, packageURL);
|
|
9
|
+
export const tscPath = fileURLToPath(executableURL);
|
|
10
|
+
|
|
11
|
+
export async function typecheck(sources: string[]): Promise<void> {
|
|
12
|
+
const files: string[] = [];
|
|
13
|
+
for (const source of sources) {
|
|
14
|
+
const path = resolve(source);
|
|
15
|
+
files.push(path);
|
|
16
|
+
}
|
|
17
|
+
const baseConfig = resolve(import.meta.dir, '../../../config/tsconfig.json');
|
|
18
|
+
const root = tmpdir();
|
|
19
|
+
const prefix = join(root, 'ffigen-tsconfig-');
|
|
20
|
+
const directory = await mkdtemp(prefix);
|
|
21
|
+
try {
|
|
22
|
+
const config = {
|
|
23
|
+
extends: baseConfig,
|
|
24
|
+
compilerOptions: { composite: false, noEmit: true },
|
|
25
|
+
files,
|
|
26
|
+
include: [],
|
|
27
|
+
};
|
|
28
|
+
const contents = JSON.stringify(config, null, 2);
|
|
29
|
+
const project = join(directory, 'tsconfig.json');
|
|
30
|
+
await writeFile(project, contents);
|
|
31
|
+
const child = Bun.spawnSync([process.execPath, tscPath, '--project', project], {
|
|
32
|
+
stdout: 'pipe',
|
|
33
|
+
stderr: 'pipe',
|
|
34
|
+
});
|
|
35
|
+
if (child.exitCode !== 0) {
|
|
36
|
+
const output = child.stdout.toString();
|
|
37
|
+
const errors = child.stderr.toString();
|
|
38
|
+
throw new Error(`TypeScript checking failed:\n${output}${errors}`);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
await rm(directory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (import.meta.main) {
|
|
46
|
+
const sources = process.argv.slice(2);
|
|
47
|
+
if (sources.length === 0) {
|
|
48
|
+
console.error('Usage: bun packages/react-nitro-ffigen/src/tsc.ts <source.ts> [...]');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
await typecheck(sources);
|
|
52
|
+
}
|