@napi-rs/cli 3.9.1 → 3.10.1
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/README.md +17 -4
- package/dist/cli.js +1594 -180
- package/dist/index.cjs +1594 -180
- package/dist/index.d.cts +69 -2
- package/dist/index.d.ts +69 -2
- package/dist/index.js +1594 -180
- package/docs/wasi.md +335 -4
- package/package.json +3 -3
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +4361 -15
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build-regressions.spec.ts +210 -3
- package/src/api/__tests__/build.spec.ts +2139 -3
- package/src/api/__tests__/create-npm-dirs.spec.ts +101 -0
- package/src/api/__tests__/pre-publish.spec.ts +153 -1
- package/src/api/__tests__/templates.spec.ts +1352 -0
- package/src/api/build.ts +637 -31
- package/src/api/create-npm-dirs.ts +21 -9
- package/src/api/pre-publish.ts +29 -1
- package/src/api/templates/binding-target.ts +176 -0
- package/src/api/templates/index.ts +1 -0
- package/src/api/templates/js-binding.ts +48 -3
- package/src/api/templates/load-wasi-template.ts +747 -45
- package/src/utils/__tests__/reconciliation.spec.ts +683 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/misc.ts +347 -74
- package/src/utils/typegen.ts +367 -12
|
@@ -1,13 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BINDING_TARGET_STAMP_HELPER,
|
|
3
|
+
NAPI_BINDING_TARGET_EXPORT,
|
|
4
|
+
NAPI_BINDING_TARGET_STAMP_FN,
|
|
5
|
+
} from './binding-target.js'
|
|
6
|
+
|
|
1
7
|
const WASI_DISPOSE_SYMBOL = 'napi.rs.wasi.dispose'
|
|
2
8
|
const WASI_ROLLBACK_REGISTRY_SYMBOL = 'napi.rs.wasi.rollback.registry.v1'
|
|
3
9
|
|
|
4
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `Context.destroy()` disables JavaScript calls *before* it runs cleanup hooks
|
|
12
|
+
* (`setStopping` -> `setCanCallIntoJs(false)` -> `runCleanup`), and the
|
|
13
|
+
* threadsafe function's cleanup hook then drains its queue with a null env and
|
|
14
|
+
* discards it. So `napi_prepare_wasm_env_cleanup` has to run while the
|
|
15
|
+
* environment is still live — before `destroy()`, never from a hook inside it.
|
|
16
|
+
*
|
|
17
|
+
* The loaders already order their own teardown that way. A `destroy()` called
|
|
18
|
+
* by anyone else — an embedder or test harness holding the context, emnapi's
|
|
19
|
+
* own `beforeExit` auto-destroy on a host where `suppressDestroy()` is absent —
|
|
20
|
+
* would skip the barrier and discard exactly the settlements it exists to
|
|
21
|
+
* cancel and deliver. Own the ordering on the object rather than on each call
|
|
22
|
+
* site: shadow `destroy` once at creation, so every caller gets the barrier.
|
|
23
|
+
*
|
|
24
|
+
* The barrier is reentrant-hostile, so the wrapper has to be reentrancy-aware.
|
|
25
|
+
* `napi_prepare_wasm_env_cleanup` settles the promises it cancels synchronously,
|
|
26
|
+
* under a non-reentrant lifecycle mutex on the Rust side; a V8 promise hook
|
|
27
|
+
* (`promiseHooks.onSettled`, or the `async_hooks` hook `AsyncLocalStorage`
|
|
28
|
+
* installs) that calls `destroy()` therefore re-enters this wrapper from inside
|
|
29
|
+
* the barrier, and calling the barrier again aborts the whole wasm instance.
|
|
30
|
+
* While a prepare is in flight the nested `destroy()` is a no-op rather than a
|
|
31
|
+
* deferred one: the frame that started the barrier destroys the moment it
|
|
32
|
+
* returns, still synchronously, and `Context.destroy()` is typed `void`, so
|
|
33
|
+
* answering `undefined` loses nothing a caller could have observed. Letting the
|
|
34
|
+
* nested call through instead would tear the environment down mid-barrier and
|
|
35
|
+
* strand every settlement the barrier had not reached yet.
|
|
36
|
+
*
|
|
37
|
+
* Defensive, not strict: a context whose `destroy` cannot be read or redefined
|
|
38
|
+
* is returned unchanged rather than failing the load. A barrier that throws
|
|
39
|
+
* still propagates, exactly as it does from `__destroyEmnapiContext`.
|
|
40
|
+
*
|
|
41
|
+
* This is NOT a replacement for `dispose()`: only the disposal chain yields
|
|
42
|
+
* event-loop turns until `napi_wasm_env_cleanup_pending` reads zero, so a
|
|
43
|
+
* direct `destroy()` still cannot wait for a settlement queued by another
|
|
44
|
+
* thread. It delivers everything the barrier settles on this thread.
|
|
45
|
+
*/
|
|
46
|
+
const emnapiContextDestroyWrapper = `
|
|
47
|
+
function __wrapEmnapiContextDestroyForSettlement(
|
|
48
|
+
context,
|
|
49
|
+
prepareEnvCleanup,
|
|
50
|
+
isPreparingEnvCleanup,
|
|
51
|
+
) {
|
|
52
|
+
let destroy
|
|
53
|
+
try {
|
|
54
|
+
destroy = context.destroy
|
|
55
|
+
} catch {
|
|
56
|
+
return context
|
|
57
|
+
}
|
|
58
|
+
if (typeof destroy !== 'function') {
|
|
59
|
+
return context
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
Object.defineProperty(context, 'destroy', {
|
|
63
|
+
configurable: true,
|
|
64
|
+
enumerable: false,
|
|
65
|
+
writable: true,
|
|
66
|
+
value: function () {
|
|
67
|
+
// Reentered from a promise hook that fired inside the barrier: the
|
|
68
|
+
// frame running it destroys as soon as it returns.
|
|
69
|
+
if (isPreparingEnvCleanup?.()) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
prepareEnvCleanup?.()
|
|
73
|
+
return Reflect.apply(destroy, this, arguments)
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
} catch {}
|
|
77
|
+
return context
|
|
78
|
+
}
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Host teardown must run while the environment can still accept N-API calls,
|
|
83
|
+
* and after the settlement drain: the drain is what lets the barrier's queued
|
|
84
|
+
* promise settlements reach JavaScript, and the task host is what publishes the
|
|
85
|
+
* CurrentThread turns they may still need. `__destroyEmnapiContext` is the
|
|
86
|
+
* single funnel every teardown path reaches — `dispose()`, the initialization
|
|
87
|
+
* rollback, and the CJS 'exit' handler — and it is reached only after
|
|
88
|
+
* `__startWasiDisposal` / the rollback have already prepared and drained, so
|
|
89
|
+
* one call there covers all three.
|
|
90
|
+
*/
|
|
91
|
+
const createEmnapiContextLifecycle = (asyncRuntime: boolean) => {
|
|
92
|
+
const currentThreadHosts = asyncRuntime
|
|
93
|
+
? `
|
|
94
|
+
let __currentThreadHostsDisposer
|
|
95
|
+
|
|
96
|
+
function __reportCurrentThreadHostDisposalError(error) {
|
|
97
|
+
try {
|
|
98
|
+
const consoleHost = globalThis.console
|
|
99
|
+
if (consoleHost && typeof consoleHost.error === 'function') {
|
|
100
|
+
consoleHost.error(error)
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Unregister the CurrentThread task and timer hosts this loader installed.
|
|
107
|
+
* Idempotent, and never throws: an unregister failure must not abort
|
|
108
|
+
* \`Context.destroy()\`, which would retain the whole environment over a
|
|
109
|
+
* bookkeeping error. The failure is reported instead.
|
|
110
|
+
*/
|
|
111
|
+
function __disposeCurrentThreadHosts() {
|
|
112
|
+
const dispose = __currentThreadHostsDisposer
|
|
113
|
+
if (dispose === undefined) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
__currentThreadHostsDisposer = undefined
|
|
117
|
+
try {
|
|
118
|
+
dispose()
|
|
119
|
+
} catch (error) {
|
|
120
|
+
__reportCurrentThreadHostDisposalError(error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
`
|
|
124
|
+
: ''
|
|
125
|
+
const disposeCurrentThreadHosts = asyncRuntime
|
|
126
|
+
? ' __disposeCurrentThreadHosts()\n'
|
|
127
|
+
: ''
|
|
128
|
+
|
|
129
|
+
return `
|
|
5
130
|
const __wasiDisposeSymbol = Symbol.for('${WASI_DISPOSE_SYMBOL}')
|
|
6
131
|
const __wasiWorkers = new Set()
|
|
132
|
+
// The thread manager has to be reachable *before* anything that can throw
|
|
133
|
+
// during load or registration. Initialization can fail after the pool has
|
|
134
|
+
// already spawned workers, and the rollback still has to mark their
|
|
135
|
+
// terminations as expected — but \`__napiModule\` is assigned only when
|
|
136
|
+
// instantiation RETURNS, so on exactly that path it is still undefined. A
|
|
137
|
+
// plugin factory runs while the emnapi module is being created, before the
|
|
138
|
+
// wasm is loaded and before any registration function runs, and its context
|
|
139
|
+
// carries the very same manager instance.
|
|
140
|
+
let __wasiThreadManager
|
|
141
|
+
|
|
142
|
+
function __captureWasiThreadManager(context) {
|
|
143
|
+
if (context && context.PThread) {
|
|
144
|
+
__wasiThreadManager = context.PThread
|
|
145
|
+
}
|
|
146
|
+
return {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function __getWasiThreadManager() {
|
|
150
|
+
const manager =
|
|
151
|
+
__wasiThreadManager !== undefined
|
|
152
|
+
? __wasiThreadManager
|
|
153
|
+
: __napiModule
|
|
154
|
+
? __napiModule.PThread
|
|
155
|
+
: undefined
|
|
156
|
+
if (manager && typeof manager.terminateWorker === 'function') {
|
|
157
|
+
return manager
|
|
158
|
+
}
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
7
161
|
let __napiInstance
|
|
8
162
|
let __emnapiContextDestroyed = false
|
|
9
163
|
let __emnapiContextDestroyPromise
|
|
10
164
|
let __emnapiWasmEnvCleanupPrepared = false
|
|
165
|
+
let __emnapiWasmEnvCleanupPreparing = false
|
|
11
166
|
let __emnapiWasmEnvCleanupRan = false
|
|
12
167
|
let __emnapiWasmEnvCleanupDrained = false
|
|
13
168
|
let __emnapiWasmEnvCleanupDrainPromise
|
|
@@ -18,7 +173,7 @@ let __completeWasiDisposal = function () {}
|
|
|
18
173
|
// that stopped short of destroying the context. See
|
|
19
174
|
// \`__rollbackWasiInitialization\`.
|
|
20
175
|
let __retainWasiRollbackForRetry = function () {}
|
|
21
|
-
|
|
176
|
+
${currentThreadHosts}
|
|
22
177
|
function __isThenable(value) {
|
|
23
178
|
return (
|
|
24
179
|
value !== null &&
|
|
@@ -80,14 +235,26 @@ function __attachCleanupErrors(error, cleanupErrors) {
|
|
|
80
235
|
} catch {}
|
|
81
236
|
return aggregate
|
|
82
237
|
}
|
|
238
|
+
${emnapiContextDestroyWrapper}
|
|
239
|
+
function __isPreparingWasmEnvCleanup() {
|
|
240
|
+
return __emnapiWasmEnvCleanupPreparing
|
|
241
|
+
}
|
|
83
242
|
|
|
84
243
|
function __prepareWasmEnvCleanup() {
|
|
85
|
-
if (__emnapiWasmEnvCleanupPrepared) {
|
|
244
|
+
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
|
|
86
245
|
return
|
|
87
246
|
}
|
|
88
247
|
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
|
|
89
248
|
if (typeof prepare === 'function') {
|
|
90
|
-
|
|
249
|
+
// The addon settles the promises it cancels synchronously, under a
|
|
250
|
+
// non-reentrant lifecycle mutex: anything a promise hook calls from in
|
|
251
|
+
// here must not reach this export again.
|
|
252
|
+
__emnapiWasmEnvCleanupPreparing = true
|
|
253
|
+
try {
|
|
254
|
+
prepare()
|
|
255
|
+
} finally {
|
|
256
|
+
__emnapiWasmEnvCleanupPreparing = false
|
|
257
|
+
}
|
|
91
258
|
__emnapiWasmEnvCleanupRan = true
|
|
92
259
|
}
|
|
93
260
|
__emnapiWasmEnvCleanupPrepared = true
|
|
@@ -259,6 +426,7 @@ function __destroyEmnapiContext() {
|
|
|
259
426
|
return __emnapiContextDestroyPromise
|
|
260
427
|
}
|
|
261
428
|
|
|
429
|
+
${disposeCurrentThreadHosts}\
|
|
262
430
|
__prepareWasmEnvCleanup()
|
|
263
431
|
const result = __emnapiContext.destroy()
|
|
264
432
|
if (!__isThenable(result)) {
|
|
@@ -280,13 +448,80 @@ function __destroyEmnapiContext() {
|
|
|
280
448
|
return destroyPromise
|
|
281
449
|
}
|
|
282
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Holds the event loop open until \`work\` settles.
|
|
453
|
+
*
|
|
454
|
+
* Nothing else can: the pool workers are deliberately unreferenced so an idle
|
|
455
|
+
* binding cannot keep a process alive, and referencing them again for the
|
|
456
|
+
* termination does not hold either — emnapi unreferences a worker the moment it
|
|
457
|
+
* reports \`async-thread-ready\`, which for a worker that was still starting
|
|
458
|
+
* lands *after* the termination began. Without a handle of its own, an
|
|
459
|
+
* \`await dispose()\` with nothing else pending exits the process with its
|
|
460
|
+
* promise unsettled, and everything after the \`await\` is skipped.
|
|
461
|
+
*
|
|
462
|
+
* The timer is cleared as soon as the work settles, so this never outlives the
|
|
463
|
+
* disposal that asked for it.
|
|
464
|
+
*/
|
|
465
|
+
function __keepEventLoopAliveUntil(work) {
|
|
466
|
+
const setTimer = globalThis.setInterval
|
|
467
|
+
const clearTimer = globalThis.clearInterval
|
|
468
|
+
if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') {
|
|
469
|
+
return work
|
|
470
|
+
}
|
|
471
|
+
let timer
|
|
472
|
+
try {
|
|
473
|
+
timer = setTimer(function () {}, 50)
|
|
474
|
+
} catch {
|
|
475
|
+
return work
|
|
476
|
+
}
|
|
477
|
+
const release = function () {
|
|
478
|
+
try {
|
|
479
|
+
clearTimer(timer)
|
|
480
|
+
} catch {}
|
|
481
|
+
}
|
|
482
|
+
return work.then(
|
|
483
|
+
(value) => {
|
|
484
|
+
release()
|
|
485
|
+
return value
|
|
486
|
+
},
|
|
487
|
+
(error) => {
|
|
488
|
+
release()
|
|
489
|
+
throw error
|
|
490
|
+
},
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* \`@emnapi/wasi-threads\` counts a worker exit as expected only when its own
|
|
496
|
+
* thread manager performed the termination. A bare \`worker.terminate()\` reaches
|
|
497
|
+
* the manager's \`exit\` listener instead, which reports
|
|
498
|
+
* \`worker (tid = N) sent an error! ... stopped with exit code 1\` and rethrows
|
|
499
|
+
* inside the emit — aborting the \`once('exit')\` that backs the terminate
|
|
500
|
+
* promise, so disposal never settles and the process dies with an uncaught
|
|
501
|
+
* exception. Mark the termination through the manager first.
|
|
502
|
+
*
|
|
503
|
+
* The manager comes from \`__getWasiThreadManager\`, not from \`__napiModule\`:
|
|
504
|
+
* the initialization rollback runs on the one path where instantiation never
|
|
505
|
+
* returned, so \`__napiModule\` is still undefined there while the workers it
|
|
506
|
+
* spawned are already registered and loaded.
|
|
507
|
+
*
|
|
508
|
+
* Not \`terminateAllThreads()\`: that one recreates the pool it just shut down.
|
|
509
|
+
*/
|
|
283
510
|
function __terminateWasiWorkers() {
|
|
284
511
|
const cleanupErrors = []
|
|
285
512
|
const pending = []
|
|
513
|
+
const threadManager = __getWasiThreadManager()
|
|
286
514
|
|
|
287
515
|
for (const worker of __wasiWorkers) {
|
|
288
516
|
let result
|
|
289
517
|
try {
|
|
518
|
+
if (threadManager) {
|
|
519
|
+
threadManager.terminateWorker(worker)
|
|
520
|
+
// \`terminateWorker\` leaves behind a reporter that logs every message
|
|
521
|
+
// still queued on the port, which Node flushes on exit. Nothing is
|
|
522
|
+
// listening for those any more.
|
|
523
|
+
worker.onmessage = undefined
|
|
524
|
+
}
|
|
290
525
|
result = worker.terminate()
|
|
291
526
|
} catch (error) {
|
|
292
527
|
cleanupErrors.push(error)
|
|
@@ -316,7 +551,9 @@ function __terminateWasiWorkers() {
|
|
|
316
551
|
)
|
|
317
552
|
}
|
|
318
553
|
}
|
|
319
|
-
return pending.length > 0
|
|
554
|
+
return pending.length > 0
|
|
555
|
+
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
|
|
556
|
+
: finish()
|
|
320
557
|
}
|
|
321
558
|
|
|
322
559
|
function __finishWasiDisposal() {
|
|
@@ -513,6 +750,7 @@ function __rollbackWasiInitialization() {
|
|
|
513
750
|
return __destroyContextForWasiRollback(cleanupErrors)
|
|
514
751
|
}
|
|
515
752
|
`
|
|
753
|
+
}
|
|
516
754
|
|
|
517
755
|
export const createWasiBrowserBinding = (
|
|
518
756
|
wasiFilename: string,
|
|
@@ -523,11 +761,24 @@ export const createWasiBrowserBinding = (
|
|
|
523
761
|
buffer = false,
|
|
524
762
|
errorEvent = false,
|
|
525
763
|
threads = true,
|
|
764
|
+
// `platformArchABI` of the flavor this loader belongs to. Defaults from
|
|
765
|
+
// `threads` so callers that predate the parameter keep their identity.
|
|
766
|
+
platformArchABI = threads ? 'wasm32-wasi' : 'wasm32-wasip1',
|
|
767
|
+
asyncRuntime = false,
|
|
526
768
|
) => {
|
|
527
769
|
// Threaded builds always get a pre-created worker pool (see
|
|
528
770
|
// `reuseWorkerOption` below), and pool pre-creation is asynchronous, so
|
|
529
771
|
// they always initialize asynchronously.
|
|
530
772
|
const effectiveAsyncInit = asyncInit || threads
|
|
773
|
+
const asyncRuntimeImport = asyncRuntime
|
|
774
|
+
? `import { installCurrentThreadHosts as __installCurrentThreadHosts } from '@napi-rs/async-runtime'\n`
|
|
775
|
+
: ''
|
|
776
|
+
const installAsyncRuntimeHosts = asyncRuntime
|
|
777
|
+
? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
|
|
778
|
+
__napiModule.exports,
|
|
779
|
+
)
|
|
780
|
+
`
|
|
781
|
+
: ''
|
|
531
782
|
const fsImport = fs
|
|
532
783
|
? buffer
|
|
533
784
|
? `import { memfs, Buffer } from '@napi-rs/wasm-runtime/fs'`
|
|
@@ -631,7 +882,7 @@ const __workerPoolSize = Math.max(
|
|
|
631
882
|
// `napi_generic_failure` stubs; with threads it would shadow the
|
|
632
883
|
// `@emnapi/core` threaded TSFN/async-work protocol the plugins implement.
|
|
633
884
|
const emnapiPluginImport = ` emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin as __emnapiTSFNPlugin,\n`
|
|
634
|
-
const emnapiPluginOption = ` plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],\n`
|
|
885
|
+
const emnapiPluginOption = ` plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],\n`
|
|
635
886
|
const workerOption = threads
|
|
636
887
|
? ` onCreateWorker() {
|
|
637
888
|
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
|
|
@@ -652,8 +903,11 @@ ${workerRuntimeImport}\
|
|
|
652
903
|
WASI as __WASI,
|
|
653
904
|
} from '@napi-rs/wasm-runtime'
|
|
654
905
|
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
|
|
906
|
+
${asyncRuntimeImport}\
|
|
655
907
|
${fsImport}
|
|
656
908
|
${bufferImport}
|
|
909
|
+
export const __napiBindingTarget = '${platformArchABI}'
|
|
910
|
+
${BINDING_TARGET_STAMP_HELPER}
|
|
657
911
|
${wasiCreation}
|
|
658
912
|
|
|
659
913
|
const __wasmUrl = new URL('./${wasiFilename}.wasm', import.meta.url).href
|
|
@@ -677,12 +931,16 @@ ${threads ? ' shared: true,\n' : ''}\
|
|
|
677
931
|
})
|
|
678
932
|
${workerPoolSizeBinding}\
|
|
679
933
|
let __emnapiContext
|
|
680
|
-
${
|
|
934
|
+
${createEmnapiContextLifecycle(asyncRuntime)}
|
|
681
935
|
let __wasiModule
|
|
682
936
|
let __napiModule
|
|
683
937
|
|
|
684
938
|
try {
|
|
685
|
-
__emnapiContext =
|
|
939
|
+
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
|
|
940
|
+
__emnapiCreateContext({ autoDestroy: false }),
|
|
941
|
+
__prepareWasmEnvCleanup,
|
|
942
|
+
__isPreparingWasmEnvCleanup,
|
|
943
|
+
)
|
|
686
944
|
__emnapiContext.suppressDestroy()
|
|
687
945
|
${emnapiInjectBuffer}
|
|
688
946
|
;({
|
|
@@ -715,6 +973,13 @@ ${workerOption}\
|
|
|
715
973
|
},
|
|
716
974
|
}))
|
|
717
975
|
__publishWasiDispose(__napiModule.exports)
|
|
976
|
+
${installAsyncRuntimeHosts}\
|
|
977
|
+
// The default export hands out this object; a named module export does not
|
|
978
|
+
// travel with it, so carry the marker on the binding itself too. After the
|
|
979
|
+
// host install, which hands the same object to addon-provided registration
|
|
980
|
+
// functions that may put anything on it, and inside this \`try\`, so a claimed
|
|
981
|
+
// name fails the load through the rollback below rather than past it.
|
|
982
|
+
${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
|
|
718
983
|
} catch (error) {
|
|
719
984
|
const cleanupErrors = await __rollbackWasiInitialization()
|
|
720
985
|
throw __attachCleanupErrors(error, cleanupErrors)
|
|
@@ -722,19 +987,232 @@ ${workerOption}\
|
|
|
722
987
|
`
|
|
723
988
|
}
|
|
724
989
|
|
|
990
|
+
/**
|
|
991
|
+
* Module-scope prelude of the deferred loader: the compiled-in memory
|
|
992
|
+
* descriptor, the per-module instance counters, and the resolver that turns a
|
|
993
|
+
* `createInstance()` options bag into the one `WebAssembly.Memory` that
|
|
994
|
+
* instance runs on.
|
|
995
|
+
*
|
|
996
|
+
* Nothing here allocates — workerd bans allocation in global scope, so the
|
|
997
|
+
* Memory itself is created inside `__createInstance`.
|
|
998
|
+
*/
|
|
999
|
+
const DEFERRED_MEMORY_PREAMBLE = (
|
|
1000
|
+
initialMemory: number,
|
|
1001
|
+
maximumMemory: number,
|
|
1002
|
+
) => `
|
|
1003
|
+
export const WASM_MEMORY = Object.freeze({
|
|
1004
|
+
initialPages: ${initialMemory},
|
|
1005
|
+
maximumPages: ${maximumMemory},
|
|
1006
|
+
pageBytes: 65536,
|
|
1007
|
+
initialBytes: ${initialMemory} * 65536,
|
|
1008
|
+
maximumBytes: ${maximumMemory} * 65536,
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
let __createdInstances = 0
|
|
1012
|
+
let __liveInstances = 0
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Counters for instances created by THIS module evaluation, not process-wide:
|
|
1016
|
+
* a second bundled copy of this loader keeps its own. Only successfully
|
|
1017
|
+
* created instances are counted, and \`liveInstances\` drops when an instance's
|
|
1018
|
+
* \`dispose()\` resolves.
|
|
1019
|
+
*
|
|
1020
|
+
* \`declaredInitialMemoryBytes\` is declared address space, not a host's
|
|
1021
|
+
* committed-memory metric; pair it with host telemetry rather than treating it
|
|
1022
|
+
* as a quota.
|
|
1023
|
+
*/
|
|
1024
|
+
export function getDeferredRuntimeStats() {
|
|
1025
|
+
return Object.freeze({
|
|
1026
|
+
createdInstances: __createdInstances,
|
|
1027
|
+
liveInstances: __liveInstances,
|
|
1028
|
+
declaredInitialMemoryBytes: WASM_MEMORY.initialBytes,
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor(
|
|
1033
|
+
ArrayBuffer.prototype,
|
|
1034
|
+
'byteLength',
|
|
1035
|
+
).get
|
|
1036
|
+
const __memoryBufferGetter = Object.getOwnPropertyDescriptor(
|
|
1037
|
+
WebAssembly.Memory.prototype,
|
|
1038
|
+
'buffer',
|
|
1039
|
+
).get
|
|
1040
|
+
// One managed initialization per Memory, success or failure: an attempt that
|
|
1041
|
+
// throws may already have written into linear memory, so the bytes are not a
|
|
1042
|
+
// clean slate for a second instance. Module-local, like the counters above.
|
|
1043
|
+
const __claimedMemories = new WeakSet()
|
|
1044
|
+
|
|
1045
|
+
function __resolveInstanceMemory(__options) {
|
|
1046
|
+
const __provided = __options == null ? undefined : __options.memory
|
|
1047
|
+
if (__provided === undefined || __provided === null) {
|
|
1048
|
+
// Page counts are handed to the engine unvalidated: it already rejects a
|
|
1049
|
+
// negative, over-4GiB or below-maximum value with a precise message, and a
|
|
1050
|
+
// second set of bounds here would only drift from it.
|
|
1051
|
+
const __allocated = new WebAssembly.Memory({
|
|
1052
|
+
initial:
|
|
1053
|
+
__options != null && __options.initialMemoryPages !== undefined
|
|
1054
|
+
? __options.initialMemoryPages
|
|
1055
|
+
: WASM_MEMORY.initialPages,
|
|
1056
|
+
maximum:
|
|
1057
|
+
__options != null && __options.maximumMemoryPages !== undefined
|
|
1058
|
+
? __options.maximumMemoryPages
|
|
1059
|
+
: WASM_MEMORY.maximumPages,
|
|
1060
|
+
})
|
|
1061
|
+
// Claimed like a caller-provided one. The handle publishes it as
|
|
1062
|
+
// \`instance.memory\`, so handing it back to \`createInstance()\` is as easy
|
|
1063
|
+
// as passing your own twice, and it would put two live instances on one
|
|
1064
|
+
// linear memory: each initialization rewrites the emnapi/WASI state the
|
|
1065
|
+
// other is still running on.
|
|
1066
|
+
__claimedMemories.add(__allocated)
|
|
1067
|
+
return __allocated
|
|
1068
|
+
}
|
|
1069
|
+
if (
|
|
1070
|
+
__options.initialMemoryPages !== undefined ||
|
|
1071
|
+
__options.maximumMemoryPages !== undefined
|
|
1072
|
+
) {
|
|
1073
|
+
throw new TypeError(
|
|
1074
|
+
'Pass either memory or initialMemoryPages/maximumMemoryPages, not both',
|
|
1075
|
+
)
|
|
1076
|
+
}
|
|
1077
|
+
let __buffer
|
|
1078
|
+
try {
|
|
1079
|
+
// Brand check: the getter throws for anything that is not a genuine
|
|
1080
|
+
// WebAssembly.Memory, including a cross-realm look-alike object.
|
|
1081
|
+
__buffer = Reflect.apply(__memoryBufferGetter, __provided, [])
|
|
1082
|
+
} catch {
|
|
1083
|
+
throw new TypeError('memory must be an unshared WebAssembly.Memory')
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
// Throws for a SharedArrayBuffer. This loader has no threads, and shared
|
|
1087
|
+
// growth does not detach: external views handed to the addon would
|
|
1088
|
+
// silently outlive the bytes they describe.
|
|
1089
|
+
Reflect.apply(__arrayBufferByteLengthGetter, __buffer, [])
|
|
1090
|
+
} catch {
|
|
1091
|
+
throw new TypeError(
|
|
1092
|
+
'The deferred loader requires an unshared WebAssembly.Memory',
|
|
1093
|
+
)
|
|
1094
|
+
}
|
|
1095
|
+
// The intrinsic getters above accept a genuine Memory from ANY realm, but
|
|
1096
|
+
// the loader's dependencies do not: \`WASI.setMemory\` in
|
|
1097
|
+
// \`@napi-rs/wasm-runtime\` and emnapi identify a Memory with a realm-local
|
|
1098
|
+
// \`instanceof\`. A Memory built in another realm (a \`node:vm\` context, a
|
|
1099
|
+
// same-origin iframe) would pass every check here and only fail deep inside
|
|
1100
|
+
// initialization. Reject it up front, and before the claim below, so the
|
|
1101
|
+
// caller keeps it usable in the realm that made it.
|
|
1102
|
+
if (!(__provided instanceof WebAssembly.Memory)) {
|
|
1103
|
+
throw new TypeError(
|
|
1104
|
+
'memory must be a WebAssembly.Memory created in the same realm as this loader',
|
|
1105
|
+
)
|
|
1106
|
+
}
|
|
1107
|
+
if (__claimedMemories.has(__provided)) {
|
|
1108
|
+
throw new TypeError(
|
|
1109
|
+
'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal',
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
// Last step, after every check: a rejected option bag must leave the Memory
|
|
1113
|
+
// unclaimed, or a caller could not fix the call and retry with it.
|
|
1114
|
+
__claimedMemories.add(__provided)
|
|
1115
|
+
return __provided
|
|
1116
|
+
}
|
|
1117
|
+
`
|
|
1118
|
+
|
|
725
1119
|
export const createWasiDeferredBrowserBinding = (
|
|
726
1120
|
wasiFilename: string,
|
|
1121
|
+
// Fed by `napi.wasm.threadlessInitialMemory ?? napi.wasm.initialMemory`.
|
|
727
1122
|
// 64 MiB leaves headroom for JS/runtime state under workerd's 128 MiB
|
|
728
1123
|
// isolate limit. The regular Node/browser loaders retain their historical
|
|
729
1124
|
// 4,000-page default.
|
|
730
1125
|
initialMemory = 1024,
|
|
731
1126
|
maximumMemory = 65536,
|
|
732
1127
|
buffer = false,
|
|
1128
|
+
// Deferred loaders are only emitted for non-threaded flavors, so the
|
|
1129
|
+
// default matches the only flavor `napi build` generates one for.
|
|
1130
|
+
platformArchABI = 'wasm32-wasip1',
|
|
1131
|
+
asyncRuntime = false,
|
|
733
1132
|
) => {
|
|
734
1133
|
const bufferImport = buffer ? `import { Buffer } from 'buffer'` : ''
|
|
735
1134
|
const emnapiInjectBuffer = buffer
|
|
736
1135
|
? ' __emnapiContext.features.Buffer = Buffer\n'
|
|
737
1136
|
: ''
|
|
1137
|
+
// This flavor creates N independent instances per realm, each with its own
|
|
1138
|
+
// emnapi context and its own `napiModule.exports`, so it uses the
|
|
1139
|
+
// per-instance helpers rather than `installCurrentThreadHosts`: those return
|
|
1140
|
+
// exact, idempotent disposers with no realm-global dedup, roll themselves
|
|
1141
|
+
// back on a setup failure, and degrade to a no-op disposer when the realm has
|
|
1142
|
+
// no `setTimeout`/`clearTimeout`.
|
|
1143
|
+
// The `/workerd` subpath, not the barrel: the barrel's `index.cjs` also
|
|
1144
|
+
// requires `current-thread-hosts.cjs`, whose realm-global registry and Node
|
|
1145
|
+
// timer-handle bookkeeping this flavor never executes, and a CJS barrel is
|
|
1146
|
+
// not tree-shakeable out of a worker bundle.
|
|
1147
|
+
const asyncRuntimeImport = asyncRuntime
|
|
1148
|
+
? `import {
|
|
1149
|
+
registerWorkerdCurrentThreadTaskHost as __registerWorkerdCurrentThreadTaskHost,
|
|
1150
|
+
registerWorkerdTimerHost as __registerWorkerdTimerHost,
|
|
1151
|
+
} from '@napi-rs/async-runtime/workerd'
|
|
1152
|
+
`
|
|
1153
|
+
: ''
|
|
1154
|
+
// `__createManagedEmnapiContext` calls `__prepareEnvCleanup?.()` on EVERY
|
|
1155
|
+
// destroy path (dispose(), managed beforeExit, module lifecycle), so a second
|
|
1156
|
+
// hook next to it covers them all with one edit.
|
|
1157
|
+
const managedHostDisposeParam = asyncRuntime ? ' __disposeHosts,\n' : ''
|
|
1158
|
+
const managedHostDisposeCall = asyncRuntime
|
|
1159
|
+
? ` __disposeHosts?.()\n`
|
|
1160
|
+
: ''
|
|
1161
|
+
const instanceHostState = asyncRuntime
|
|
1162
|
+
? ` let __disposeInstanceHosts
|
|
1163
|
+
const __reportInstanceHostDisposalError = (__error) => {
|
|
1164
|
+
try {
|
|
1165
|
+
const __consoleHost = globalThis.console
|
|
1166
|
+
if (__consoleHost && typeof __consoleHost.error === 'function') {
|
|
1167
|
+
__consoleHost.error(__error)
|
|
1168
|
+
}
|
|
1169
|
+
} catch {}
|
|
1170
|
+
}
|
|
1171
|
+
// Runs between the settlement drain and \`Context.destroy()\`; never throws,
|
|
1172
|
+
// for the same reason the eager loaders' disposer does not.
|
|
1173
|
+
const __disposeHostsBeforeDestroy = () => {
|
|
1174
|
+
const __dispose = __disposeInstanceHosts
|
|
1175
|
+
if (__dispose === undefined) {
|
|
1176
|
+
return
|
|
1177
|
+
}
|
|
1178
|
+
__disposeInstanceHosts = undefined
|
|
1179
|
+
__dispose()
|
|
1180
|
+
}
|
|
1181
|
+
`
|
|
1182
|
+
: ''
|
|
1183
|
+
const installInstanceHosts = asyncRuntime
|
|
1184
|
+
? ` const __disposeTaskHost = __registerWorkerdCurrentThreadTaskHost(
|
|
1185
|
+
__napiModule.exports,
|
|
1186
|
+
)
|
|
1187
|
+
try {
|
|
1188
|
+
const __disposeTimerHost = __registerWorkerdTimerHost(
|
|
1189
|
+
__napiModule.exports,
|
|
1190
|
+
)
|
|
1191
|
+
__disposeInstanceHosts = () => {
|
|
1192
|
+
try {
|
|
1193
|
+
__disposeTimerHost()
|
|
1194
|
+
} catch (__error) {
|
|
1195
|
+
__reportInstanceHostDisposalError(__error)
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
__disposeTaskHost()
|
|
1199
|
+
} catch (__error) {
|
|
1200
|
+
__reportInstanceHostDisposalError(__error)
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
} catch (__error) {
|
|
1204
|
+
try {
|
|
1205
|
+
__disposeTaskHost()
|
|
1206
|
+
} catch (__cleanupError) {
|
|
1207
|
+
__attachCleanupError(__error, __cleanupError)
|
|
1208
|
+
}
|
|
1209
|
+
throw __error
|
|
1210
|
+
}
|
|
1211
|
+
`
|
|
1212
|
+
: ''
|
|
1213
|
+
const managedHostDisposeArg = asyncRuntime
|
|
1214
|
+
? ' __disposeHostsBeforeDestroy,\n'
|
|
1215
|
+
: ''
|
|
738
1216
|
return `import {
|
|
739
1217
|
emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,
|
|
740
1218
|
emnapiTSFNPlugin as __emnapiTSFNPlugin,
|
|
@@ -742,7 +1220,11 @@ export const createWasiDeferredBrowserBinding = (
|
|
|
742
1220
|
WASI as __WASI,
|
|
743
1221
|
} from '@napi-rs/wasm-runtime'
|
|
744
1222
|
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
|
|
1223
|
+
${asyncRuntimeImport}\
|
|
745
1224
|
${bufferImport}
|
|
1225
|
+
${DEFERRED_MEMORY_PREAMBLE(initialMemory, maximumMemory)}
|
|
1226
|
+
export const __napiBindingTarget = '${platformArchABI}'
|
|
1227
|
+
${BINDING_TARGET_STAMP_HELPER}
|
|
746
1228
|
|
|
747
1229
|
/**
|
|
748
1230
|
* Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes.
|
|
@@ -841,7 +1323,7 @@ async function __normalizeModuleForEmnapi(__module) {
|
|
|
841
1323
|
'provide structuredClone or MessageChannel support.',
|
|
842
1324
|
)
|
|
843
1325
|
}
|
|
844
|
-
|
|
1326
|
+
${emnapiContextDestroyWrapper}
|
|
845
1327
|
function __captureEmnapiAutoDestroyListener(__process) {
|
|
846
1328
|
if (
|
|
847
1329
|
!__process ||
|
|
@@ -1229,7 +1711,10 @@ function __registerManagedEmnapiContext(__process, __destroy) {
|
|
|
1229
1711
|
}
|
|
1230
1712
|
}
|
|
1231
1713
|
|
|
1232
|
-
async function __createManagedEmnapiContext(
|
|
1714
|
+
async function __createManagedEmnapiContext(
|
|
1715
|
+
__prepareEnvCleanup,
|
|
1716
|
+
__isPreparingEnvCleanup,
|
|
1717
|
+
${managedHostDisposeParam}) {
|
|
1233
1718
|
const __process =
|
|
1234
1719
|
typeof process === 'object' && process !== null ? process : undefined
|
|
1235
1720
|
const __finishAutoDestroyCapture =
|
|
@@ -1238,7 +1723,11 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
|
|
|
1238
1723
|
let __contextInitializationError
|
|
1239
1724
|
let __contextInitializationFailed = false
|
|
1240
1725
|
try {
|
|
1241
|
-
__emnapiContext =
|
|
1726
|
+
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
|
|
1727
|
+
__emnapiCreateContext({ autoDestroy: false }),
|
|
1728
|
+
__prepareEnvCleanup,
|
|
1729
|
+
__isPreparingEnvCleanup,
|
|
1730
|
+
)
|
|
1242
1731
|
// emnapi 2.x still registers an unconditional process.once('beforeExit')
|
|
1243
1732
|
// auto-destroy listener on Node hosts, and suppressDestroy() only
|
|
1244
1733
|
// neutralizes its callback without removing it. This loader must stay
|
|
@@ -1288,6 +1777,18 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
|
|
|
1288
1777
|
// Context.destroy() disables JS before cleanup hooks run, so settle
|
|
1289
1778
|
// runtime-owned promises while this environment can still call JS.
|
|
1290
1779
|
__prepareEnvCleanup?.()
|
|
1780
|
+
if (__isPreparingEnvCleanup?.()) {
|
|
1781
|
+
// Reached from inside the barrier, so \`Context.destroy()\` below would
|
|
1782
|
+
// hit the wrapper's in-flight no-op. Recording that as a completed
|
|
1783
|
+
// destroy is what makes the frame that *did* start the barrier skip the
|
|
1784
|
+
// real one afterwards, leaving the context retained with its cleanup
|
|
1785
|
+
// hooks unrun. Refuse instead: nothing is flagged, the context stays
|
|
1786
|
+
// registered for managed beforeExit cleanup, and a later destroy still
|
|
1787
|
+
// works. dispose() coalesces reentrancy before it can get here, so this
|
|
1788
|
+
// is the backstop for any other caller that manages to.
|
|
1789
|
+
throw __createLifecycleReentryError('dispose')
|
|
1790
|
+
}
|
|
1791
|
+
${managedHostDisposeCall}\
|
|
1291
1792
|
__result = __emnapiContext.destroy()
|
|
1292
1793
|
} catch (error) {
|
|
1293
1794
|
__finishDestroyInvocation()
|
|
@@ -1392,6 +1893,7 @@ async function __createManagedEmnapiContext(__prepareEnvCleanup) {
|
|
|
1392
1893
|
|
|
1393
1894
|
async function __createInstance(
|
|
1394
1895
|
__wasmInput,
|
|
1896
|
+
__options,
|
|
1395
1897
|
__beforeExitDestroy,
|
|
1396
1898
|
__onManagedDestroyer,
|
|
1397
1899
|
) {
|
|
@@ -1401,31 +1903,40 @@ async function __createInstance(
|
|
|
1401
1903
|
version: 'preview1',
|
|
1402
1904
|
})
|
|
1403
1905
|
// The wasm module is linked with \`--import-memory\`, so a Memory must be
|
|
1404
|
-
// provided. It is
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
// leak a context that never reaches
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
maximum: ${maximumMemory},
|
|
1411
|
-
})
|
|
1906
|
+
// provided. It is resolved here in function scope (workerd bans global scope
|
|
1907
|
+
// allocation) and is never shared (no threads, no SharedArrayBuffer).
|
|
1908
|
+
// Resolve it before the emnapi context so a rejected option bag or a host
|
|
1909
|
+
// memory-limit failure cannot leak a context that never reaches
|
|
1910
|
+
// instantiation.
|
|
1911
|
+
const __wasmMemory = __resolveInstanceMemory(__options)
|
|
1412
1912
|
let __lifecycleState = 'pending'
|
|
1413
1913
|
let __destroyEmnapiContext
|
|
1414
1914
|
let __destroyOwnedContext
|
|
1415
1915
|
let __destroyManagedOwnedContext
|
|
1416
1916
|
let __napiInstance
|
|
1917
|
+
${instanceHostState}\
|
|
1417
1918
|
let __wasmEnvCleanupRan = false
|
|
1418
1919
|
let __wasmEnvCleanupPrepared = false
|
|
1920
|
+
let __wasmEnvCleanupPreparing = false
|
|
1419
1921
|
let __wasmEnvCleanupDrained = false
|
|
1420
1922
|
let __wasmEnvCleanupDrainPromise
|
|
1923
|
+
const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing
|
|
1421
1924
|
const __prepareEnvCleanup = () => {
|
|
1422
|
-
if (__wasmEnvCleanupPrepared) {
|
|
1925
|
+
if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) {
|
|
1423
1926
|
return
|
|
1424
1927
|
}
|
|
1425
1928
|
const __prepareWasmEnvCleanup =
|
|
1426
1929
|
__napiInstance?.exports.napi_prepare_wasm_env_cleanup
|
|
1427
1930
|
if (typeof __prepareWasmEnvCleanup === 'function') {
|
|
1428
|
-
|
|
1931
|
+
// The addon settles the promises it cancels synchronously, under a
|
|
1932
|
+
// non-reentrant lifecycle mutex: anything a promise hook calls from in
|
|
1933
|
+
// here must not reach this export again.
|
|
1934
|
+
__wasmEnvCleanupPreparing = true
|
|
1935
|
+
try {
|
|
1936
|
+
__prepareWasmEnvCleanup()
|
|
1937
|
+
} finally {
|
|
1938
|
+
__wasmEnvCleanupPreparing = false
|
|
1939
|
+
}
|
|
1429
1940
|
__wasmEnvCleanupRan = true
|
|
1430
1941
|
}
|
|
1431
1942
|
__wasmEnvCleanupPrepared = true
|
|
@@ -1472,6 +1983,63 @@ async function __createInstance(
|
|
|
1472
1983
|
__wasmEnvCleanupDrainPromise = __tracked
|
|
1473
1984
|
return __tracked
|
|
1474
1985
|
}
|
|
1986
|
+
let __disposed = false
|
|
1987
|
+
const __runInstanceDisposal = async () => {
|
|
1988
|
+
if (__lifecycleState !== 'failed') {
|
|
1989
|
+
__lifecycleState = 'disposal'
|
|
1990
|
+
}
|
|
1991
|
+
// Settle what the barrier cancelled before the environment stops
|
|
1992
|
+
// accepting JavaScript calls. Undefined unless something is queued, so
|
|
1993
|
+
// an idle disposal is not delayed by a single turn.
|
|
1994
|
+
const __drained = __prepareForDisposal()
|
|
1995
|
+
if (__drained) {
|
|
1996
|
+
await __drained
|
|
1997
|
+
}
|
|
1998
|
+
const __result = await (__beforeExitDestroy
|
|
1999
|
+
? __destroyManagedOwnedContext()
|
|
2000
|
+
: __destroyOwnedContext())
|
|
2001
|
+
// Only a completed destroy retires the instance; a throw above leaves
|
|
2002
|
+
// the counter untouched so a retried dispose() cannot double-decrement.
|
|
2003
|
+
if (!__disposed) {
|
|
2004
|
+
__disposed = true
|
|
2005
|
+
__liveInstances -= 1
|
|
2006
|
+
}
|
|
2007
|
+
return __result
|
|
2008
|
+
}
|
|
2009
|
+
let __instanceDisposePromise
|
|
2010
|
+
/**
|
|
2011
|
+
* The disposal frame runs the barrier, and the barrier settles the promises it
|
|
2012
|
+
* cancels synchronously — so a promise hook firing inside it can call this
|
|
2013
|
+
* same instance's dispose() again while the first call is still in its drain.
|
|
2014
|
+
* That nested call finds the barrier flagged in flight, prepares nothing,
|
|
2015
|
+
* drains nothing, and falls straight through to the context destroyer, whose
|
|
2016
|
+
* \`Context.destroy()\` hits the wrapper's in-flight no-op. It would record a
|
|
2017
|
+
* destruction that never happened, and the outer frame would then skip the
|
|
2018
|
+
* real one: both disposals resolve, no cleanup hook runs, the context stays
|
|
2019
|
+
* retained.
|
|
2020
|
+
*
|
|
2021
|
+
* Memoize before any of that starts, exactly like the eager loaders'
|
|
2022
|
+
* \`__disposeWasiBinding\`, so there is only ever one disposal frame per
|
|
2023
|
+
* instance and a reentrant caller awaits it instead of racing it. Cleared on
|
|
2024
|
+
* rejection: a drain that failed has to stay retryable.
|
|
2025
|
+
*/
|
|
2026
|
+
const __disposeInstance = () => {
|
|
2027
|
+
if (__instanceDisposePromise) {
|
|
2028
|
+
return __instanceDisposePromise
|
|
2029
|
+
}
|
|
2030
|
+
let __resolveDispose
|
|
2031
|
+
let __rejectDispose
|
|
2032
|
+
const __disposePromise = new Promise((__resolve, __reject) => {
|
|
2033
|
+
__resolveDispose = __resolve
|
|
2034
|
+
__rejectDispose = __reject
|
|
2035
|
+
})
|
|
2036
|
+
__instanceDisposePromise = __disposePromise
|
|
2037
|
+
__runInstanceDisposal().then(__resolveDispose, (__error) => {
|
|
2038
|
+
__instanceDisposePromise = undefined
|
|
2039
|
+
__rejectDispose(__error)
|
|
2040
|
+
})
|
|
2041
|
+
return __disposePromise
|
|
2042
|
+
}
|
|
1475
2043
|
const __destroyBeforeExit = __beforeExitDestroy
|
|
1476
2044
|
? async () => {
|
|
1477
2045
|
if (__lifecycleState === 'failed') {
|
|
@@ -1497,7 +2065,10 @@ async function __createInstance(
|
|
|
1497
2065
|
destroy,
|
|
1498
2066
|
destroyForModuleLifecycle,
|
|
1499
2067
|
registerCleanup: __registerCleanup,
|
|
1500
|
-
} = await __createManagedEmnapiContext(
|
|
2068
|
+
} = await __createManagedEmnapiContext(
|
|
2069
|
+
__prepareEnvCleanup,
|
|
2070
|
+
__isPreparingEnvCleanup,
|
|
2071
|
+
${managedHostDisposeArg} )
|
|
1501
2072
|
__destroyEmnapiContext = destroy
|
|
1502
2073
|
__destroyOwnedContext = () => __destroyEmnapiContext()
|
|
1503
2074
|
__destroyManagedOwnedContext = destroyForModuleLifecycle
|
|
@@ -1534,26 +2105,34 @@ ${emnapiInjectBuffer}\
|
|
|
1534
2105
|
}
|
|
1535
2106
|
},
|
|
1536
2107
|
}))
|
|
2108
|
+
${installInstanceHosts}\
|
|
2109
|
+
// \`instantiate()\` and \`createInstance().exports\` hand out this object; a
|
|
2110
|
+
// named module export does not travel with it. After the instance host
|
|
2111
|
+
// install, which hands the same object to addon-provided registration
|
|
2112
|
+
// functions that may put anything on it, and inside this \`try\`, so a
|
|
2113
|
+
// claimed name flips \`__lifecycleState\` to 'failed' and tears the instance
|
|
2114
|
+
// down rather than escaping a half-built one.
|
|
2115
|
+
${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
|
|
1537
2116
|
if (__lifecycleState === 'pending') {
|
|
1538
2117
|
__lifecycleState = 'succeeded'
|
|
1539
2118
|
}
|
|
2119
|
+
__createdInstances += 1
|
|
2120
|
+
__liveInstances += 1
|
|
1540
2121
|
return {
|
|
1541
2122
|
exports: __napiModule.exports,
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
__lifecycleState = 'disposal'
|
|
1545
|
-
}
|
|
1546
|
-
// Settle what the barrier cancelled before the environment stops
|
|
1547
|
-
// accepting JavaScript calls. Undefined unless something is queued, so
|
|
1548
|
-
// an idle disposal is not delayed by a single turn.
|
|
1549
|
-
const __drained = __prepareForDisposal()
|
|
1550
|
-
if (__drained) {
|
|
1551
|
-
await __drained
|
|
1552
|
-
}
|
|
1553
|
-
return __beforeExitDestroy
|
|
1554
|
-
? __destroyManagedOwnedContext()
|
|
1555
|
-
: __destroyOwnedContext()
|
|
2123
|
+
get memory() {
|
|
2124
|
+
return __wasmMemory
|
|
1556
2125
|
},
|
|
2126
|
+
get memoryBytes() {
|
|
2127
|
+
// The Memory outlives the environment, so this stays readable after a
|
|
2128
|
+
// FAILED dispose() (which leaves the instance undisposed and
|
|
2129
|
+
// retryable). It reports 0 only once disposal has actually completed.
|
|
2130
|
+
return __disposed ? 0 : __wasmMemory.buffer.byteLength
|
|
2131
|
+
},
|
|
2132
|
+
get disposed() {
|
|
2133
|
+
return __disposed
|
|
2134
|
+
},
|
|
2135
|
+
dispose: __disposeInstance,
|
|
1557
2136
|
}
|
|
1558
2137
|
} catch (error) {
|
|
1559
2138
|
__lifecycleState = 'failed'
|
|
@@ -1625,11 +2204,22 @@ ${emnapiInjectBuffer}\
|
|
|
1625
2204
|
}
|
|
1626
2205
|
|
|
1627
2206
|
/**
|
|
1628
|
-
* Create an independent instance. Call dispose() when the instance
|
|
1629
|
-
* longer needed so emnapi cleanup hooks run deterministically.
|
|
2207
|
+
* Create an independent instance. Call and await dispose() when the instance
|
|
2208
|
+
* is no longer needed so emnapi cleanup hooks run deterministically.
|
|
2209
|
+
*
|
|
2210
|
+
* The optional second argument selects this instance's linear memory: either
|
|
2211
|
+
* \`memory\` (an unshared, single-use WebAssembly.Memory you allocated) or
|
|
2212
|
+
* \`initialMemoryPages\` / \`maximumMemoryPages\`, never both. Omitted, the
|
|
2213
|
+
* loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages.
|
|
2214
|
+
*
|
|
2215
|
+
* A provided Memory must come from this loader's own realm: the WASI and
|
|
2216
|
+
* emnapi layers underneath identify one with a realm-local \`instanceof\`, so a
|
|
2217
|
+
* Memory built in a \`node:vm\` context or another frame is rejected. Every
|
|
2218
|
+
* Memory an instance runs on is single-use, the loader-allocated one included:
|
|
2219
|
+
* \`instance.memory\` cannot be recycled into a second \`createInstance()\`.
|
|
1630
2220
|
*/
|
|
1631
|
-
export async function createInstance(__wasmInput) {
|
|
1632
|
-
return __createInstance(__wasmInput)
|
|
2221
|
+
export async function createInstance(__wasmInput, __options) {
|
|
2222
|
+
return __createInstance(__wasmInput, __options)
|
|
1633
2223
|
}
|
|
1634
2224
|
|
|
1635
2225
|
let __defaultModulePromise
|
|
@@ -1666,6 +2256,7 @@ export function instantiate(__wasmInput) {
|
|
|
1666
2256
|
const __instancePromise = __modulePromise.then((__module) =>
|
|
1667
2257
|
__createInstance(
|
|
1668
2258
|
__module,
|
|
2259
|
+
undefined,
|
|
1669
2260
|
__disposeDefaultInstance,
|
|
1670
2261
|
(__managedDestroyer) => {
|
|
1671
2262
|
__defaultManagedDestroyers.set(
|
|
@@ -1814,21 +2405,90 @@ export function dispose() {
|
|
|
1814
2405
|
|
|
1815
2406
|
export const createWasiDeferredBrowserBindingTypeDef = (
|
|
1816
2407
|
packageName: string,
|
|
2408
|
+
platformArchABI = 'wasm32-wasip1',
|
|
1817
2409
|
) => `export type WasiBinding = typeof import('${packageName}')
|
|
1818
2410
|
|
|
1819
2411
|
export type WasiModuleInput =
|
|
1820
2412
|
| WebAssembly.Module
|
|
1821
2413
|
| PromiseLike<WebAssembly.Module>
|
|
1822
2414
|
|
|
2415
|
+
/** Run the instance on a linear memory the caller allocated. */
|
|
2416
|
+
export interface WasiCallerMemoryOptions {
|
|
2417
|
+
/**
|
|
2418
|
+
* A caller-allocated linear memory for this instance. It must be unshared
|
|
2419
|
+
* and created in this loader's own realm — the WASI and emnapi layers
|
|
2420
|
+
* underneath identify a Memory with a realm-local \`instanceof\`, so one from
|
|
2421
|
+
* a \`node:vm\` context or another frame is rejected. It is single-use: once
|
|
2422
|
+
* a validated initialization attempt has begun, the same Memory cannot be
|
|
2423
|
+
* passed again — including after that attempt failed, and after the instance
|
|
2424
|
+
* was disposed.
|
|
2425
|
+
*/
|
|
2426
|
+
memory: WebAssembly.Memory
|
|
2427
|
+
/** Not available beside \`memory\`: the loader allocates neither. */
|
|
2428
|
+
initialMemoryPages?: never
|
|
2429
|
+
/** Not available beside \`memory\`: the loader allocates neither. */
|
|
2430
|
+
maximumMemoryPages?: never
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/** Let the loader allocate the linear memory, optionally sized. */
|
|
2434
|
+
export interface WasiAllocatedMemoryOptions {
|
|
2435
|
+
/** Not available beside the page counts: they size the loader's own Memory. */
|
|
2436
|
+
memory?: never
|
|
2437
|
+
/** @default WASM_MEMORY.initialPages */
|
|
2438
|
+
initialMemoryPages?: number
|
|
2439
|
+
/** @default WASM_MEMORY.maximumPages */
|
|
2440
|
+
maximumMemoryPages?: number
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* Either memory form, never a mix of the two: the loader throws a TypeError
|
|
2445
|
+
* on \`memory\` beside a page count. \`{}\` and an omitted argument select the
|
|
2446
|
+
* loader defaults.
|
|
2447
|
+
*/
|
|
2448
|
+
export type WasiInstanceOptions =
|
|
2449
|
+
| WasiCallerMemoryOptions
|
|
2450
|
+
| WasiAllocatedMemoryOptions
|
|
2451
|
+
|
|
2452
|
+
export interface WasiRuntimeStats {
|
|
2453
|
+
/** Instances created by this evaluated loader module, not process-wide. */
|
|
2454
|
+
createdInstances: number
|
|
2455
|
+
/** Created instances whose dispose() has not completed. */
|
|
2456
|
+
liveInstances: number
|
|
2457
|
+
/** Declared initial address space, not committed memory. */
|
|
2458
|
+
declaredInitialMemoryBytes: number
|
|
2459
|
+
}
|
|
2460
|
+
|
|
1823
2461
|
export interface WasiInstance {
|
|
1824
2462
|
readonly exports: WasiBinding
|
|
2463
|
+
/** This instance's linear memory. Claimed, so it cannot start another one. */
|
|
2464
|
+
readonly memory: WebAssembly.Memory
|
|
2465
|
+
/** Current linear-memory size; 0 once dispose() has completed. */
|
|
2466
|
+
readonly memoryBytes: number
|
|
2467
|
+
readonly disposed: boolean
|
|
1825
2468
|
dispose(): Promise<void>
|
|
1826
2469
|
}
|
|
1827
2470
|
|
|
2471
|
+
/** The memory descriptor compiled into this loader. */
|
|
2472
|
+
export const WASM_MEMORY: Readonly<{
|
|
2473
|
+
initialPages: number
|
|
2474
|
+
maximumPages: number
|
|
2475
|
+
pageBytes: number
|
|
2476
|
+
initialBytes: number
|
|
2477
|
+
maximumBytes: number
|
|
2478
|
+
}>
|
|
2479
|
+
|
|
2480
|
+
export function getDeferredRuntimeStats(): Readonly<WasiRuntimeStats>
|
|
2481
|
+
|
|
1828
2482
|
export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
|
|
1829
|
-
export function createInstance(
|
|
2483
|
+
export function createInstance(
|
|
2484
|
+
wasmInput: WasiModuleInput,
|
|
2485
|
+
options?: WasiInstanceOptions,
|
|
2486
|
+
): Promise<WasiInstance>
|
|
1830
2487
|
/** Dispose the singleton and retry retained failed-initialization cleanup. */
|
|
1831
2488
|
export function dispose(): Promise<void>
|
|
2489
|
+
|
|
2490
|
+
/** The WASI flavor this deferred loader instantiates. */
|
|
2491
|
+
export declare const __napiBindingTarget: '${platformArchABI}'
|
|
1832
2492
|
`
|
|
1833
2493
|
|
|
1834
2494
|
export const createWasiBinding = (
|
|
@@ -1842,7 +2502,20 @@ export const createWasiBinding = (
|
|
|
1842
2502
|
// wasm artifact.
|
|
1843
2503
|
platformArchABI = 'wasm32-wasi',
|
|
1844
2504
|
packageWasmFileName = wasmFileName,
|
|
2505
|
+
asyncRuntime = false,
|
|
1845
2506
|
) => {
|
|
2507
|
+
const asyncRuntimeImport = asyncRuntime
|
|
2508
|
+
? `const {
|
|
2509
|
+
installCurrentThreadHosts: __installCurrentThreadHosts,
|
|
2510
|
+
} = require('@napi-rs/async-runtime')
|
|
2511
|
+
`
|
|
2512
|
+
: ''
|
|
2513
|
+
const installAsyncRuntimeHosts = asyncRuntime
|
|
2514
|
+
? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
|
|
2515
|
+
__napiModule.exports,
|
|
2516
|
+
)
|
|
2517
|
+
`
|
|
2518
|
+
: ''
|
|
1846
2519
|
const workerImports = threads
|
|
1847
2520
|
? `const { Worker } = require('node:worker_threads')
|
|
1848
2521
|
`
|
|
@@ -1955,10 +2628,10 @@ function __createWasiWorker(filename) {
|
|
|
1955
2628
|
}
|
|
1956
2629
|
})(),
|
|
1957
2630
|
reuseWorker: true,
|
|
1958
|
-
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
|
2631
|
+
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
|
1959
2632
|
`
|
|
1960
2633
|
: ` asyncWorkPoolSize: 0,
|
|
1961
|
-
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
|
2634
|
+
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
|
1962
2635
|
`
|
|
1963
2636
|
// Every build links a "basic" emnapi archive without the C async-work and
|
|
1964
2637
|
// threadsafe-function implementations (the `emnapi-napi-rs(-mt)` archives shipped by the emnapi package), so the
|
|
@@ -1996,6 +2669,10 @@ function __createWasiWorker(filename) {
|
|
|
1996
2669
|
}
|
|
1997
2670
|
|
|
1998
2671
|
worker.unref()
|
|
2672
|
+
// These stubs stay in place for the worker's whole life, disposal
|
|
2673
|
+
// included: \`__keepEventLoopAliveUntil\` is what holds the process open
|
|
2674
|
+
// while a termination is pending, precisely because a worker's own
|
|
2675
|
+
// references cannot be relied on for it.
|
|
1999
2676
|
}
|
|
2000
2677
|
return worker
|
|
2001
2678
|
},
|
|
@@ -2005,6 +2682,9 @@ function __createWasiWorker(filename) {
|
|
|
2005
2682
|
return `/* eslint-disable */
|
|
2006
2683
|
/* auto-generated by NAPI-RS */
|
|
2007
2684
|
|
|
2685
|
+
const __napiBindingTarget = '${platformArchABI}'
|
|
2686
|
+
${BINDING_TARGET_STAMP_HELPER}
|
|
2687
|
+
|
|
2008
2688
|
const __nodeFs = require('node:fs')
|
|
2009
2689
|
const __nodePath = require('node:path')
|
|
2010
2690
|
const { WASI: __nodeWASI } = require('node:wasi')
|
|
@@ -2016,6 +2696,7 @@ ${workerRuntimeImport}\
|
|
|
2016
2696
|
instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync,
|
|
2017
2697
|
} = require('@napi-rs/wasm-runtime')
|
|
2018
2698
|
const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
|
|
2699
|
+
${asyncRuntimeImport}\
|
|
2019
2700
|
${workerExecArgv}\
|
|
2020
2701
|
|
|
2021
2702
|
const __cwd = process.cwd()
|
|
@@ -2059,7 +2740,7 @@ if (__nodeFs.existsSync(__wasmDebugFilePath)) {
|
|
|
2059
2740
|
|
|
2060
2741
|
const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
|
|
2061
2742
|
let __emnapiContext
|
|
2062
|
-
${
|
|
2743
|
+
${createEmnapiContextLifecycle(asyncRuntime)}
|
|
2063
2744
|
const __wasiRollbackRegistrySymbol = Symbol.for('${WASI_ROLLBACK_REGISTRY_SYMBOL}')
|
|
2064
2745
|
const __wasiRollbackRegistryKey =
|
|
2065
2746
|
typeof __filename === 'string' ? __filename : __wasmFilePath
|
|
@@ -2233,7 +2914,11 @@ function __captureEmnapiAutoDestroyListener() {
|
|
|
2233
2914
|
try {
|
|
2234
2915
|
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
|
|
2235
2916
|
try {
|
|
2236
|
-
__emnapiContext =
|
|
2917
|
+
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
|
|
2918
|
+
__emnapiCreateContext({ autoDestroy: false }),
|
|
2919
|
+
__prepareWasmEnvCleanup,
|
|
2920
|
+
__isPreparingWasmEnvCleanup,
|
|
2921
|
+
)
|
|
2237
2922
|
// emnapi 2.x still registers an unconditional once-listener for
|
|
2238
2923
|
// beforeExit that auto-destroys the context, and suppressDestroy() only
|
|
2239
2924
|
// neutralizes its callback without removing it. This loader owns cleanup
|
|
@@ -2273,6 +2958,23 @@ ${workerOption}\
|
|
|
2273
2958
|
},
|
|
2274
2959
|
}))
|
|
2275
2960
|
__publishWasiDispose(__napiModule.exports)
|
|
2961
|
+
${installAsyncRuntimeHosts}\
|
|
2962
|
+
// The CommonJS tail below aliases \`__napiModule.exports\`; a named module
|
|
2963
|
+
// export does not travel with it, so carry the marker on the binding itself
|
|
2964
|
+
// too. Three things pin the stamp to exactly this spot:
|
|
2965
|
+
// - inside this \`try\`, because the guard throws on a
|
|
2966
|
+
// \`#[napi(module_exports)]\` hook that claimed the name, and only the
|
|
2967
|
+
// catch below tears the environment — context, workers, exit listener —
|
|
2968
|
+
// back down;
|
|
2969
|
+
// - after the async runtime host install, which hands this same object to
|
|
2970
|
+
// addon-provided registration functions that may put anything on it;
|
|
2971
|
+
// - assigning onto the loader's own \`module.exports\`, which is still the
|
|
2972
|
+
// original object here, so an addon accessor with a refusing setter is
|
|
2973
|
+
// never written through. \`cjs-module-lexer\` — Node's CJS -> ESM named
|
|
2974
|
+
// export detection — reads the static \`module.exports.<name> =\` either
|
|
2975
|
+
// way, and the later \`module.exports = __napiModule.exports\` does not
|
|
2976
|
+
// undo that.
|
|
2977
|
+
module.exports.${NAPI_BINDING_TARGET_EXPORT} = ${NAPI_BINDING_TARGET_STAMP_FN}(__napiModule.exports, __napiBindingTarget)
|
|
2276
2978
|
__registerWasiExitListener()
|
|
2277
2979
|
} catch (error) {
|
|
2278
2980
|
const rollback = {
|