crossbind 2.0.0-beta.63 → 2.0.0-beta.64
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/AGENTS.md +2 -1
- package/package.json +2 -2
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildNode.js +4 -6
- package/src/actions/buildWasiCommand.js +10 -5
- package/src/actions/buildWasm.js +24 -3
- package/src/actions/createImportedBridges.js +7 -0
- package/src/actions/createInterface.js +3 -3
- package/src/actions/createLib.js +14 -2
- package/src/actions/getData.js +2 -1
- package/src/actions/getEnv.js +7 -0
- package/src/actions/runnerCommands.js +16 -4
- package/src/actions/writeRuntimeEntry.js +18 -7
- package/src/assets/cpp-runtime/node.cpp +18 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +51 -0
- package/src/assets/js-runtime/core.js +5 -4
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/bin.js +29 -179
- package/src/cli.js +14 -0
- package/src/integration/getCrossbindScript.js +4 -3
- package/src/program.js +169 -0
- package/src/state/index.js +4 -3
- package/src/state/loadConfig.js +4 -11
- package/src/utils/appRustCrates.js +4 -0
- package/src/utils/bridgeExtras.js +74 -6
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cppDts.js +2 -0
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/packageLicense.js +4 -2
- package/src/utils/resetCMakeCacheForSource.js +26 -0
- package/src/utils/rustBridgeGen.js +13 -4
- package/src/utils/rustMt.js +5 -0
- package/src/utils/rustSysroot.js +7 -5
- package/src/utils/swigInterface.js +5 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/targets.js +11 -0
- package/src/utils/wasiHttp.js +50 -0
|
@@ -4,14 +4,16 @@ import { parseMacroDump, stripComments, declaresAtFileScope } from './swigInterf
|
|
|
4
4
|
// SWIG binds no function-like macro, variadic function or mutable global. crossbind binds them for the names an app
|
|
5
5
|
// imports by name: a function-like macro takes the parameter types of the one function it calls, a macro naming a
|
|
6
6
|
// function binds that function under its own name, a variadic function takes its extra arguments by their JavaScript
|
|
7
|
-
// type,
|
|
8
|
-
//
|
|
7
|
+
// type, a mutable global binds as a handle to its address, and a constant SWIG drops for a cast in its value binds as
|
|
8
|
+
// the number the compiler makes of it. A macro binds only through a function SWIG bound, the one place that proves the
|
|
9
|
+
// name is a single declared function.
|
|
9
10
|
|
|
10
11
|
const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
|
|
11
12
|
const MACRO_LINE = /^\/\/ crossbind:macro ([A-Za-z_]\w*) (#define .*)$/gm;
|
|
12
13
|
const ALIAS_LINE = /^\/\/ crossbind:alias ([A-Za-z_]\w*) ([A-Za-z_]\w*)$/gm;
|
|
14
|
+
const CONSTANT_LINE = /^\/\/ crossbind:constant ([A-Za-z_]\w*)$/gm;
|
|
13
15
|
const NOTE_MARKER = '// crossbind:note ';
|
|
14
|
-
const EXTRA_LINES = /^(?:\/\/ crossbind:(?:macro|alias|note) .*\n)+\n/m;
|
|
16
|
+
const EXTRA_LINES = /^(?:\/\/ crossbind:(?:macro|alias|constant|note) .*\n)+\n/m;
|
|
15
17
|
// The arguments one call may add to a variadic function: each registration count dispatches over 2^n argument kinds.
|
|
16
18
|
const MAX_VARIADIC_ARGUMENTS = 6;
|
|
17
19
|
const VARIADIC_WARNING = /^Variable length arguments are not supported by embind, (\S+) skipped\.$/;
|
|
@@ -90,9 +92,66 @@ export function macroCall(macro) {
|
|
|
90
92
|
// The identifier an object-like macro stands for, as in `#define iconv_open libiconv_open`.
|
|
91
93
|
const identifierOf = (macro) => (macro && !macro.params && C_IDENTIFIER.test(macro.body) ? macro.body : null);
|
|
92
94
|
|
|
95
|
+
const NUMBER_TYPE_WORDS = new Set(['unsigned', 'signed', 'short', 'long', 'int', 'char', 'float', 'double', 'bool', '_Bool']);
|
|
96
|
+
const NUMBER_TYPEDEFS = new Set([
|
|
97
|
+
'size_t', 'ssize_t', 'ptrdiff_t', 'intptr_t', 'uintptr_t', 'intmax_t', 'uintmax_t', 'off_t', 'wchar_t',
|
|
98
|
+
'int8_t', 'uint8_t', 'int16_t', 'uint16_t', 'int32_t', 'uint32_t', 'int64_t', 'uint64_t',
|
|
99
|
+
]);
|
|
100
|
+
const VALUE_TOKEN = /0[xX][\dA-Fa-f]+[uUlL]*|\d+(?:\.\d*)?(?:[eE][-+]?\d+)?[uUlLfF]*|'(?:\\.|[^'\\])+'|[A-Za-z_]\w*|<<|>>|\S/g;
|
|
101
|
+
const OPERATOR = /^(?:<<|>>|[-+*/%&|^~!()<>=?:])$/;
|
|
102
|
+
const LITERAL = /^(?:\d|')/;
|
|
103
|
+
const CAST_OPERAND = /^(?:\d|'|[-+~!(])/;
|
|
104
|
+
|
|
105
|
+
// A type named by number words, a standard number typedef, or a typedef of one that the header declares.
|
|
106
|
+
function isNumberType(words, headerText, depth = 0) {
|
|
107
|
+
if (words.every((word) => NUMBER_TYPE_WORDS.has(word))) return true;
|
|
108
|
+
if (words.length !== 1 || depth > 3) return false;
|
|
109
|
+
if (NUMBER_TYPEDEFS.has(words[0])) return true;
|
|
110
|
+
const declared = stripComments(headerText).match(new RegExp(`\\btypedef\\s+([A-Za-z_][\\w\\s]*?)\\s+${words[0]}\\s*;`));
|
|
111
|
+
return Boolean(declared) && isNumberType(declared[1].trim().split(/\s+/), headerText, depth + 1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The tokens of an object-like macro's value with the object-like macros in it spelled out; null for anything else.
|
|
115
|
+
function expandedTokens(name, macros, depth = 0) {
|
|
116
|
+
const macro = macros.get(name);
|
|
117
|
+
if (!macro || macro.params || depth > 16) return null;
|
|
118
|
+
const tokens = [];
|
|
119
|
+
for (const token of macro.body.match(VALUE_TOKEN) ?? []) {
|
|
120
|
+
if (!C_IDENTIFIER.test(token) || !macros.has(token)) {
|
|
121
|
+
tokens.push(token);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const inner = expandedTokens(token, macros, depth + 1);
|
|
125
|
+
if (!inner) return null;
|
|
126
|
+
tokens.push('(', ...inner, ')');
|
|
127
|
+
}
|
|
128
|
+
return tokens;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// SWIG drops a #define whose value holds a name, so every cast. A value of numbers, operators and casts binds through
|
|
132
|
+
// the compiler when every cast is to a number type: { numbers }, or null for a value with no cast or with other names.
|
|
133
|
+
function castConstant(name, macros, headerText) {
|
|
134
|
+
const tokens = expandedTokens(name, macros);
|
|
135
|
+
if (!tokens) return null;
|
|
136
|
+
let casts = 0;
|
|
137
|
+
let numbers = true;
|
|
138
|
+
for (let at = 0; at < tokens.length; at += 1) {
|
|
139
|
+
if (LITERAL.test(tokens[at]) || OPERATOR.test(tokens[at])) continue;
|
|
140
|
+
const close = tokens.indexOf(')', at);
|
|
141
|
+
const words = tokens.slice(at, close);
|
|
142
|
+
const isCast = tokens[at - 1] === '(' && close > at && CAST_OPERAND.test(tokens[close + 1] ?? '')
|
|
143
|
+
&& words.every((word) => word === '*' || C_IDENTIFIER.test(word));
|
|
144
|
+
if (!isCast) return null;
|
|
145
|
+
casts += 1;
|
|
146
|
+
numbers &&= !words.includes('*') && isNumberType(words, headerText);
|
|
147
|
+
at = close;
|
|
148
|
+
}
|
|
149
|
+
return casts ? { numbers } : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
93
152
|
// What the interface carries for the names an app imports by name, read by the bridge once SWIG has run: their
|
|
94
153
|
// function-like macros and their macros naming a function, and the functions they reach, which SWIG has to bind.
|
|
95
|
-
export function interfaceExtras(names, macros) {
|
|
154
|
+
export function interfaceExtras(names, macros, headerText = '') {
|
|
96
155
|
if (names === ALL_NAMES || !names.length) return { lines: [], functions: [] };
|
|
97
156
|
const lines = [];
|
|
98
157
|
const functions = [];
|
|
@@ -104,6 +163,10 @@ export function interfaceExtras(names, macros) {
|
|
|
104
163
|
if (target && target !== name && !macros.has(target) && !C_KEYWORDS.has(target)) {
|
|
105
164
|
lines.push(`// crossbind:alias ${name} ${target}`);
|
|
106
165
|
functions.push(target);
|
|
166
|
+
} else {
|
|
167
|
+
const constant = castConstant(name, macros, headerText);
|
|
168
|
+
if (constant?.numbers) lines.push(`// crossbind:constant ${name}`);
|
|
169
|
+
else if (constant) lines.push(`${NOTE_MARKER}${name}: a constant whose value has a cast binds only when every cast is to a number type the header declares; skipped.`);
|
|
107
170
|
}
|
|
108
171
|
continue;
|
|
109
172
|
}
|
|
@@ -168,7 +231,8 @@ export function bridgeExtras({ interfaceText = '', named = [], bridgeText = '',
|
|
|
168
231
|
const globalWarnings = warningsAbout(warnings, GLOBAL_WARNING, imported);
|
|
169
232
|
const globals = [...globalWarnings.keys()].filter((name) => declaresAtFileScope(headerText, name));
|
|
170
233
|
const answered = new Set([...variadics.map((name) => variadicWarnings.get(name)), ...globals.map((name) => globalWarnings.get(name))]);
|
|
171
|
-
|
|
234
|
+
const constants = [...interfaceText.matchAll(CONSTANT_LINE)].map(([, name]) => name).filter((name) => !bridgeText.includes(`bindConstant("${name}"`));
|
|
235
|
+
if (!macros.length && !aliases.length && !variadics.length && !globals.length && !constants.length) return { code: '', exports: [], answered, notes };
|
|
172
236
|
|
|
173
237
|
// The macros register ahead of the fallbacks below, which would make a function a variadic shares its name with an
|
|
174
238
|
// overload set.
|
|
@@ -184,10 +248,13 @@ export function bridgeExtras({ interfaceText = '', named = [], bridgeText = '',
|
|
|
184
248
|
`EMSCRIPTEN_BINDINGS(CrossbindExtras_${module}) {`,
|
|
185
249
|
...variadics.map((name) => ` { using F = decltype(crossbind::variadicType(&${name}, 0)); crossbind::bindVariadic<F, static_cast<F>(&${name}), ${MAX_VARIADIC_ARGUMENTS}>("${name}"); }`),
|
|
186
250
|
...globals.map((name) => ` crossbind::bindGlobal("${name}", &${name});`),
|
|
251
|
+
...constants.map((name) => ` crossbind::bindConstant("${name}", ${name});`),
|
|
187
252
|
'}',
|
|
188
253
|
'',
|
|
189
254
|
].join('\n');
|
|
190
|
-
const exports = [
|
|
255
|
+
const exports = [
|
|
256
|
+
...macros.map(({ name }) => name), ...aliases.map(({ name }) => name), ...variadics, ...globals, ...constants, ...(variadics.length ? ['vaDouble'] : []),
|
|
257
|
+
];
|
|
191
258
|
return { code, exports, answered, notes };
|
|
192
259
|
}
|
|
193
260
|
|
|
@@ -261,6 +328,7 @@ template<typename R> constexpr bool isJsResult = std::is_void_v<R> || isJsArgume
|
|
|
261
328
|
template<typename T> auto argumentFromJs(const emscripten::val &value) {
|
|
262
329
|
using U = std::remove_cv_t<T>;
|
|
263
330
|
if constexpr (std::is_same_v<U, const char *>) return fromJsCString<const char>(value);
|
|
331
|
+
else if constexpr (isFunctionPointer<U>) return fromJsCallback<U>(value);
|
|
264
332
|
else if constexpr (std::is_pointer_v<U>) return fieldFromJs<U>(value);
|
|
265
333
|
else if constexpr (std::is_same_v<U, bool>) {
|
|
266
334
|
double number = 0;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import writeIfChanged from './writeIfChanged.js';
|
|
4
|
+
|
|
5
|
+
// The versions of every crate the app's Rust links, next to crossbind.config.js and committed like conan.lock, since
|
|
6
|
+
// the manifests cargo resolves are generated under the cache. Not Cargo.lock: a package whose crate sits at the
|
|
7
|
+
// project root already has one.
|
|
8
|
+
const lockOf = (projectDir) => path.join(projectDir, 'crossbind.cargo.lock');
|
|
9
|
+
|
|
10
|
+
// Before cargo resolves a generated manifest, so it keeps every version the project pins.
|
|
11
|
+
export function followCargoLock(projectDir, manifestDir) {
|
|
12
|
+
const lock = lockOf(projectDir);
|
|
13
|
+
if (fs.existsSync(lock)) writeIfChanged(path.join(manifestDir, 'Cargo.lock'), fs.readFileSync(lock, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// After a build: what cargo resolved, a crate the lock did not list yet included.
|
|
17
|
+
export function recordCargoLock(projectDir, manifestDir) {
|
|
18
|
+
writeIfChanged(lockOf(projectDir), fs.readFileSync(path.join(manifestDir, 'Cargo.lock'), 'utf8'));
|
|
19
|
+
}
|
package/src/utils/cppDts.js
CHANGED
|
@@ -225,6 +225,8 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
|
|
|
225
225
|
for (const method of cls.methods) {
|
|
226
226
|
out.push(` ${method.isStatic ? 'static ' : ''}${method.name}(${method.args.map((a) => `${a.name}: ${a.type}`).join(', ')}): ${wrap(method.ret)};`);
|
|
227
227
|
}
|
|
228
|
+
// Frees the object now instead of when the garbage collector reclaims the handle (lifecycle-and-types.md).
|
|
229
|
+
out.push(` delete(): ${wrap('void')};`);
|
|
228
230
|
out.push('}');
|
|
229
231
|
}
|
|
230
232
|
for (const name of exportNames) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// embind frees an object when JavaScript's handle to it is collected only behind a smart pointer. A value a call
|
|
2
|
+
// returns, which embind copied for JavaScript, and an object JavaScript made through a raw constructor (a vector, a
|
|
3
|
+
// Rust class) were freed by .delete() alone, though they are as much JavaScript's own. This rewrite marks both on the
|
|
4
|
+
// count a handle shares with its clones and gives them the same finalizer. A copy is known only where its converter
|
|
5
|
+
// makes the handle, since embind then types the handle as a pointer to the object's own class. A raw pointer stays
|
|
6
|
+
// its library's. The debug glue's leak warning goes: a collected handle is freed by design. Without
|
|
7
|
+
// FinalizationRegistry (Hermes) nothing is registered, as before.
|
|
8
|
+
|
|
9
|
+
// The release glue spells each on one line, the debug glue one statement per line with comments.
|
|
10
|
+
const FINALIZER = /if\s*\(hasSmartPtr\)\s*\{[\s\S]*?finalizationRegistry\.register\(handle,\s*info,\s*handle\);?/;
|
|
11
|
+
const OWNED_FINALIZER = 'if(hasSmartPtr||$$.count.owned){finalizationRegistry.register(handle,{$$},handle);';
|
|
12
|
+
const LEAK_WARNING = /console\.warn\(info\.leakWarning\);?\s*/;
|
|
13
|
+
const FROM_WIRE = /function RegisteredPointer_fromWireType\(ptr\)\s*\{/g;
|
|
14
|
+
const OWN_FROM_WIRE = 'function crossbindOwnHandle(handle){var $$=handle.$$;if(!$$.smartPtr&&!$$.count.owned){$$.count.owned=true;'
|
|
15
|
+
+ 'if(finalizationRegistry)finalizationRegistry.register(handle,{$$},handle)}return handle}'
|
|
16
|
+
+ 'function RegisteredPointer_fromWireType(ptr){var handle=crossbindHandleFromWire.call(this,ptr);'
|
|
17
|
+
+ 'return this.isReference&&handle?crossbindOwnHandle(handle):handle}'
|
|
18
|
+
+ 'function crossbindHandleFromWire(ptr){';
|
|
19
|
+
const CONSTRUCTED = /return body\.apply\(this,\s*args\);?/g;
|
|
20
|
+
const OWN_CONSTRUCTED = 'return crossbindOwnHandle(body.apply(this,args));';
|
|
21
|
+
|
|
22
|
+
const count = (text, pattern) => text.match(pattern)?.length ?? 0;
|
|
23
|
+
|
|
24
|
+
// Returns the rewritten glue and whether it holds embind classes this rewrite does not recognise.
|
|
25
|
+
export function releaseOwnedHandles(glue) {
|
|
26
|
+
if (!glue.includes('attachFinalizer')) return { text: glue, missed: false };
|
|
27
|
+
const recognised = count(glue, FROM_WIRE) === 1 && count(glue, CONSTRUCTED) === 1;
|
|
28
|
+
// Functions, not strings: `$$` in a replacement string is a lone `$`.
|
|
29
|
+
const text = glue
|
|
30
|
+
.replace(FINALIZER, () => OWNED_FINALIZER)
|
|
31
|
+
.replace(LEAK_WARNING, '')
|
|
32
|
+
.replace(FROM_WIRE, () => OWN_FROM_WIRE)
|
|
33
|
+
.replace(CONSTRUCTED, () => OWN_CONSTRUCTED);
|
|
34
|
+
return { text, missed: !recognised || !text.includes(OWNED_FINALIZER) };
|
|
35
|
+
}
|
|
@@ -74,9 +74,11 @@ export function nodePackageLicense({ name, version, repository, projectPath, add
|
|
|
74
74
|
|
|
75
75
|
export function setManifestLicense(dir, expression) {
|
|
76
76
|
const file = path.join(dir, 'package.json');
|
|
77
|
-
const
|
|
77
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
78
|
+
const manifest = JSON.parse(text);
|
|
78
79
|
if (manifest.license === expression) return;
|
|
79
|
-
|
|
80
|
+
const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? 4;
|
|
81
|
+
fs.writeFileSync(file, `${JSON.stringify({ ...manifest, license: expression }, null, indent)}\n`);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export function writeNodePackageLicense(dir, meta) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import state from '../state/index.js';
|
|
3
|
+
import replaceBasePathForDocker from './replaceBasePathForDocker.js';
|
|
4
|
+
import { isLocalRunner } from './selectRunner.js';
|
|
5
|
+
import { HOST_BUILT_PLATFORMS } from './targets.js';
|
|
6
|
+
|
|
7
|
+
const normalize = (dir) => dir.replaceAll('\\', '/').replace(/\/+$/, '');
|
|
8
|
+
|
|
9
|
+
// The directory as the cmake run() starts sees it: a container, Docker or a remote runner, has the project at
|
|
10
|
+
// DOCKER_BASE; macOS and iOS builds and the LOCAL runner see this machine's paths.
|
|
11
|
+
function seenByCMake(dir, target) {
|
|
12
|
+
const isLocal = HOST_BUILT_PLATFORMS.includes(target.platform) || isLocalRunner(state.config.system);
|
|
13
|
+
return isLocal ? dir : replaceBasePathForDocker(dir, state.config.paths.base);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A CMake build tree remembers the source directory it was configured from and refuses another one. The CMake
|
|
17
|
+
// projects crossbind ships move with its version (node_modules/.pnpm/crossbind@<version>/...), so a tree configured
|
|
18
|
+
// from another directory starts afresh, as `cmake --fresh` would.
|
|
19
|
+
export default function resetCMakeCacheForSource(buildPath, sourceDir, target) {
|
|
20
|
+
const cache = `${buildPath}/CMakeCache.txt`;
|
|
21
|
+
if (!fs.existsSync(cache)) return;
|
|
22
|
+
const configuredFrom = fs.readFileSync(cache, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$/m)?.[1];
|
|
23
|
+
if (configuredFrom !== undefined && normalize(configuredFrom) === normalize(seenByCMake(sourceDir, target))) return;
|
|
24
|
+
fs.rmSync(cache, { force: true });
|
|
25
|
+
fs.rmSync(`${buildPath}/CMakeFiles`, { recursive: true, force: true });
|
|
26
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { followCargoLock } from './cargoLock.js';
|
|
4
5
|
import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
|
|
5
6
|
import runCargo, { toHostPath } from './runCargo.js';
|
|
6
7
|
import writeIfChanged from './writeIfChanged.js';
|
|
@@ -303,7 +304,9 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
|
|
|
303
304
|
// through the root and through a module registers once and is the same JS class for both.
|
|
304
305
|
const crateSourceCache = new Map();
|
|
305
306
|
const crateModelCache = new Map();
|
|
306
|
-
export function createCrateImportBridge({
|
|
307
|
+
export function createCrateImportBridge({
|
|
308
|
+
crateName, modulePath = [], spec, cacheDir, projectDir, dtsMode = 'sync', log = console.log,
|
|
309
|
+
}) {
|
|
307
310
|
const importName = [crateName, ...modulePath].join('/');
|
|
308
311
|
const crateIdent = crateName.replaceAll('-', '_');
|
|
309
312
|
const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
|
|
@@ -343,7 +346,9 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
|
|
|
343
346
|
// cargo metadata needs a resolvable lib target before the real bridge exists.
|
|
344
347
|
if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
|
|
345
348
|
|
|
346
|
-
const { srcDir, features } = crateSource({
|
|
349
|
+
const { srcDir, features } = crateSource({
|
|
350
|
+
dir, depLine, crateName, importName, projectDir,
|
|
351
|
+
});
|
|
347
352
|
const ownKey = modulePath.join('::');
|
|
348
353
|
const imports = [];
|
|
349
354
|
for (const importPath of crateImports(cacheDir, crateName, modulePath)) {
|
|
@@ -385,10 +390,14 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
|
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
// cargo metadata locates the crate's source and the feature set it resolves to (fetching the crate
|
|
388
|
-
// on first use); both hold for every import of the crate.
|
|
389
|
-
|
|
393
|
+
// on first use); both hold for every import of the crate. It resolves with the project's pins, so
|
|
394
|
+
// the bridge is made from the version the app links.
|
|
395
|
+
function crateSource({
|
|
396
|
+
dir, depLine, crateName, importName, projectDir,
|
|
397
|
+
}) {
|
|
390
398
|
const cacheKey = `${dir}|${depLine}`;
|
|
391
399
|
if (crateSourceCache.has(cacheKey)) return crateSourceCache.get(cacheKey);
|
|
400
|
+
followCargoLock(projectDir, dir);
|
|
392
401
|
let meta;
|
|
393
402
|
try {
|
|
394
403
|
const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
|
package/src/utils/rustMt.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
// One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
|
|
@@ -39,6 +40,10 @@ export function cargoTargetDirFor(baseDir, target) {
|
|
|
39
40
|
// points at whatever version that image pinned, so the CLI carries no version of its own.
|
|
40
41
|
const CONTAINER_SYSROOT = '/opt/crossbind/rust/current';
|
|
41
42
|
|
|
43
|
+
// True only inside the toolchain image. A CLI running there with RUNNER=LOCAL (the playground
|
|
44
|
+
// compiler) has these trees, but not rustup's stock wasm target.
|
|
45
|
+
export const hasContainerSysroot = () => fs.existsSync(CONTAINER_SYSROOT);
|
|
46
|
+
|
|
42
47
|
export function cargoBuildInvocation({
|
|
43
48
|
target, triple, targetDir, manifestPath, sysroot = false,
|
|
44
49
|
}) {
|
package/src/utils/rustSysroot.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import withDirLock from './dirLock.js';
|
|
6
6
|
import fetchOciLayer from './ociArtifact.js';
|
|
7
7
|
import { cargoRunner } from './runCargo.js';
|
|
8
|
-
import { isMtWasm } from './rustMt.js';
|
|
8
|
+
import { hasContainerSysroot, isMtWasm } from './rustMt.js';
|
|
9
9
|
import PIN from './rustSysrootPin.js';
|
|
10
10
|
|
|
11
11
|
// The second consumption channel for the Rust sysroots. A containerized build gets them as an image
|
|
@@ -149,7 +149,7 @@ let resolved = null;
|
|
|
149
149
|
// Called from buildDependencies, the one async step every build path awaits before the sync work
|
|
150
150
|
// starts. Only downloads when a target will actually consume it.
|
|
151
151
|
export async function prepareRustSysroot(targets, log = console.log) {
|
|
152
|
-
if (resolved || !PIN) return resolved;
|
|
152
|
+
if (resolved || !PIN || hasContainerSysroot()) return resolved;
|
|
153
153
|
if (!targets.some((target) => isMtWasm(target) && cargoRunner(target) === 'LOCAL')) return null;
|
|
154
154
|
try {
|
|
155
155
|
resolved = await ensureRustSysroot({ image: PIN.image, index: PIN.index });
|
|
@@ -161,11 +161,13 @@ export async function prepareRustSysroot(targets, log = console.log) {
|
|
|
161
161
|
return resolved;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees
|
|
165
|
-
// absolute path where a downloaded artifact answers,
|
|
166
|
-
// st needs nothing - rustup's stock std is already
|
|
164
|
+
// What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees (also a
|
|
165
|
+
// LOCAL build running inside the image), an absolute path where a downloaded artifact answers,
|
|
166
|
+
// false where mt must rebuild std on nightly. st needs nothing - rustup's stock std is already
|
|
167
|
+
// correct for it.
|
|
167
168
|
export function sysrootFor(target) {
|
|
168
169
|
if (cargoRunner(target) !== 'LOCAL') return true;
|
|
170
|
+
if (target.platform === 'wasm' && hasContainerSysroot()) return true;
|
|
169
171
|
if (!isMtWasm(target)) return false;
|
|
170
172
|
return resolved ?? false;
|
|
171
173
|
}
|
|
@@ -75,8 +75,9 @@ export function buildInterfaceContent({
|
|
|
75
75
|
// The marker keeps a preamble that starts with #define apart from the macro block the retry drops.
|
|
76
76
|
const preambleBlock = preamble.length ? `// package preamble\n${preamble.join('\n')}\n\n` : '';
|
|
77
77
|
const extraBlock = extras.length ? `${extras.join('\n')}\n\n` : '';
|
|
78
|
-
// Named constants are asked for ahead of the macros, which may define
|
|
79
|
-
// SWIG binds an interface's own #define lines, and the prelude only carries other headers'
|
|
78
|
+
// Named constants and functions are asked for ahead of the macros, which may define an imported name. Every constant
|
|
79
|
+
// is asked for after them: SWIG binds an interface's own #define lines, and the prelude only carries other headers'
|
|
80
|
+
// values for the parse.
|
|
80
81
|
const everyConstant = constants === ALL_NAMES;
|
|
81
82
|
const module = moduleName.replace(/\W/g, '_');
|
|
82
83
|
return `#ifndef _${module}_I
|
|
@@ -88,10 +89,10 @@ ${extraBlock}%{
|
|
|
88
89
|
${includes}
|
|
89
90
|
%}
|
|
90
91
|
|
|
91
|
-
${everyConstant ? '' : constantRequests(constants)}%feature("shared_ptr");
|
|
92
|
+
${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
|
|
92
93
|
%feature("polymorphic_shared_ptr");
|
|
93
94
|
|
|
94
|
-
${macros}${
|
|
95
|
+
${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
|
|
95
96
|
|
|
96
97
|
#endif
|
|
97
98
|
`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import loadJson from './loadJson.js';
|
|
3
|
+
import systemKeys from './systemKeys.js';
|
|
4
|
+
|
|
5
|
+
export const systemConfigPath = () => `${os.homedir()}/.crossbind.json`;
|
|
6
|
+
|
|
7
|
+
export default function loadSystemConfig() {
|
|
8
|
+
const system = loadJson(systemConfigPath()) || {};
|
|
9
|
+
const defaults = Object.entries(systemKeys)
|
|
10
|
+
.filter(([key]) => !(key in system))
|
|
11
|
+
.map(([key, value]) => [key, value.default]);
|
|
12
|
+
return { ...system, ...Object.fromEntries(defaults) };
|
|
13
|
+
}
|
package/src/utils/targets.js
CHANGED
|
@@ -68,6 +68,12 @@ const BASE_TARGETS = [
|
|
|
68
68
|
{
|
|
69
69
|
platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'wasi',
|
|
70
70
|
},
|
|
71
|
+
{
|
|
72
|
+
platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'http',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'http',
|
|
76
|
+
},
|
|
71
77
|
{
|
|
72
78
|
platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
|
|
73
79
|
},
|
|
@@ -192,6 +198,11 @@ export function nodeAddonNamesOf(target, projectName) {
|
|
|
192
198
|
};
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
// A wasi build is one component, with no JS glue: the command keeps the plain name, an http component adds its own.
|
|
202
|
+
export const wasiNamesOf = (target, projectName) => ({
|
|
203
|
+
wasmName: `${projectName}-${targetPathOf(target)}${target.runtimeEnv === 'http' ? '.http' : ''}.wasm`,
|
|
204
|
+
});
|
|
205
|
+
|
|
195
206
|
// One executable per platform, arch and build type, named like the addons it replaces.
|
|
196
207
|
export function nativeCommandNamesOf(target, projectName) {
|
|
197
208
|
const suffix = target.buildType === 'debug' ? '.debug' : '';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The runtime of an http build: crossbind's wasi:http/handler export, which calls the app's
|
|
2
|
+
// crossbind::http::handle, and the bindings wit-bindgen generated for it (assets/wasi-runtime/http/README.md).
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const RUNTIME = 'assets/wasi-runtime/http';
|
|
8
|
+
// The stack of each request: wasi-libc mallocs one per concurrent request, with no guard below it.
|
|
9
|
+
const STACK_SIZE = 1024 * 1024;
|
|
10
|
+
const HEADER = 'include/crossbind/http.h';
|
|
11
|
+
const LINKED = [
|
|
12
|
+
'handler.cpp',
|
|
13
|
+
'bindings/crossbind_http.c',
|
|
14
|
+
'bindings/crossbind_http.h',
|
|
15
|
+
'bindings/crossbind_http_component_type.o',
|
|
16
|
+
HEADER,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// paths.cli can lie outside the docker mount, so the files are copied into the build tree first.
|
|
20
|
+
function stage(cliPath, dir, files) {
|
|
21
|
+
files.forEach((file) => {
|
|
22
|
+
fs.mkdirSync(path.dirname(`${dir}/${file}`), { recursive: true });
|
|
23
|
+
fs.copyFileSync(`${cliPath}/${RUNTIME}/${file}`, `${dir}/${file}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const wasiHttpFiles = (cliPath) => LINKED.map((file) => `${cliPath}/${RUNTIME}/${file}`);
|
|
28
|
+
|
|
29
|
+
export const wasiHttpHeader = (cliPath) => `${cliPath}/${RUNTIME}/${HEADER}`;
|
|
30
|
+
|
|
31
|
+
// The include dir of a wasi source build, where the app's handler finds <crossbind/http.h>.
|
|
32
|
+
export function stageWasiHttpInclude(cliPath, dir) {
|
|
33
|
+
stage(cliPath, dir, [HEADER]);
|
|
34
|
+
return `${dir}/include`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// What an http link adds to a command's: the handler export, its bindings (C, hence -x c) with the world
|
|
38
|
+
// they implement, the reactor model, which has no main, and a stack as large as the wasm builds'.
|
|
39
|
+
export function stageWasiHttpLink(cliPath, dir) {
|
|
40
|
+
stage(cliPath, dir, LINKED);
|
|
41
|
+
return [
|
|
42
|
+
`-I${dir}/include`,
|
|
43
|
+
`-I${dir}/bindings`,
|
|
44
|
+
`${dir}/handler.cpp`,
|
|
45
|
+
'-x', 'c', `${dir}/bindings/crossbind_http.c`, '-x', 'none',
|
|
46
|
+
`${dir}/bindings/crossbind_http_component_type.o`,
|
|
47
|
+
'-mexec-model=reactor',
|
|
48
|
+
`-Wl,-z,stack-size=${STACK_SIZE}`,
|
|
49
|
+
];
|
|
50
|
+
}
|