janela 0.8.0 → 0.9.0
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 +4 -3
- package/bin/janela.mjs +6 -6
- package/package.json +1 -1
- package/runtime/janela.ts +88 -82
- package/shim/wvshim.cc +166 -59
package/README.md
CHANGED
|
@@ -279,9 +279,10 @@ in scriptc and can cost far more than the read did.
|
|
|
279
279
|
**Use `app.sleep`, not `setTimeout`.** scriptc's own event loop is parked for
|
|
280
280
|
as long as the program sits inside the `run()` FFI call, so `setTimeout`,
|
|
281
281
|
`queueMicrotask` and `await` in host code never fire while the window is open
|
|
282
|
-
(they all run after it closes). janela
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
(they all run after it closes). janela schedules through the shell instead: the
|
|
283
|
+
runtime parks a continuation under an id, the shell keeps the clock and calls
|
|
284
|
+
it back on the UI thread when it comes due. Nothing polls, so an idle app
|
|
285
|
+
costs nothing at all.
|
|
285
286
|
|
|
286
287
|
**Still single-threaded.** scriptc's runtime is not thread-safe (concurrent
|
|
287
288
|
calls from several threads abort the process), so host code always runs on the
|
package/bin/janela.mjs
CHANGED
|
@@ -324,21 +324,21 @@ function ffiManifest(shimLib) {
|
|
|
324
324
|
returns: "i32",
|
|
325
325
|
},
|
|
326
326
|
{
|
|
327
|
-
name: "
|
|
327
|
+
name: "wvOnTimer", symbol: "wv_on_timer",
|
|
328
328
|
params: [
|
|
329
329
|
"i32",
|
|
330
|
-
{ callback: { id: "
|
|
331
|
-
{ context: "
|
|
330
|
+
{ callback: { id: "timer", params: ["i32", { context: "timer" }], returns: "void", lifetime: "retained" } },
|
|
331
|
+
{ context: "timer" },
|
|
332
332
|
],
|
|
333
333
|
returns: "i32",
|
|
334
334
|
},
|
|
335
335
|
{ name: "wvRun", symbol: "wv_run", params: ["i32"], returns: "i32" },
|
|
336
336
|
{ name: "wvTerminate", symbol: "wv_terminate", params: ["i32"], returns: "i32" },
|
|
337
|
-
// async: deferred returns
|
|
337
|
+
// async: the held-reply table (deferred returns) plus shell-owned
|
|
338
|
+
// scheduling — TS parks a continuation id, the shell calls it back due.
|
|
338
339
|
{ name: "wvDefer", symbol: "wv_defer", params: ["i32"], returns: "i32" },
|
|
339
340
|
{ name: "wvResolve", symbol: "wv_resolve", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
340
|
-
{ name: "
|
|
341
|
-
{ name: "wvTickStop", symbol: "wv_tick_stop", params: ["i32"], returns: "i32" },
|
|
341
|
+
{ name: "wvSchedule", symbol: "wv_schedule", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
342
342
|
// async file I/O: the blocking syscall runs on a shim worker thread
|
|
343
343
|
{ name: "wvFsRead", symbol: "wv_fs_read", params: ["i32", "string"], returns: "i32" },
|
|
344
344
|
{ name: "wvFsWrite", symbol: "wv_fs_write", params: ["i32", "string", "string"], returns: "i32" },
|
package/package.json
CHANGED
package/runtime/janela.ts
CHANGED
|
@@ -19,13 +19,12 @@ declare function wvEval(h: number, js: string): number;
|
|
|
19
19
|
declare function wvBind(h: number, name: string): number;
|
|
20
20
|
declare function wvReply(h: number, body: string): number;
|
|
21
21
|
declare function wvOnInvoke(h: number, cb: (req: string) => number): number;
|
|
22
|
-
declare function
|
|
22
|
+
declare function wvOnTimer(h: number, cb: (id: number) => void): number;
|
|
23
23
|
declare function wvRun(h: number): number;
|
|
24
24
|
declare function wvTerminate(h: number): number;
|
|
25
25
|
declare function wvDefer(h: number): number;
|
|
26
26
|
declare function wvResolve(h: number, id: number, status: number): number;
|
|
27
|
-
declare function
|
|
28
|
-
declare function wvTickStop(h: number): number;
|
|
27
|
+
declare function wvSchedule(h: number, id: number, ms: number): number;
|
|
29
28
|
declare function wvFsRead(h: number, path: string): number;
|
|
30
29
|
declare function wvFsWrite(h: number, path: string, data: string): number;
|
|
31
30
|
declare function wvJobStatus(h: number, id: number): number;
|
|
@@ -148,11 +147,10 @@ function encodeFilters(filters: DialogFilter[] | undefined): string {
|
|
|
148
147
|
const DRAIN_BUDGET_MS = 4; // = a quarter of a 60fps frame
|
|
149
148
|
const DRAIN_SLICE = 131072; // 128 KB - granularity within the budget
|
|
150
149
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
const TICK_DRAIN_MS = 4;
|
|
150
|
+
// The shell posts this id when a file read or a dialog reaches a terminal
|
|
151
|
+
// state. It is not a continuation - it means "service the jobs you are
|
|
152
|
+
// waiting on". Continuation ids start at 1, so the two can never collide.
|
|
153
|
+
const TIMER_JOBS = -1;
|
|
156
154
|
|
|
157
155
|
/**
|
|
158
156
|
* A running janela app.
|
|
@@ -171,21 +169,23 @@ export class JanelaAppImpl<
|
|
|
171
169
|
names: string[] = [];
|
|
172
170
|
handlers: CommandHandler[] = [];
|
|
173
171
|
|
|
174
|
-
// ----
|
|
172
|
+
// ---- scheduling ----------------------------------------------------------
|
|
175
173
|
// scriptc's event loop is parked for as long as the program sits inside the
|
|
176
174
|
// wvRun() FFI call, so setTimeout/await never fire while the window is open.
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
175
|
+
// The shell schedules instead: a continuation is parked here under an id and
|
|
176
|
+
// handed to wvSchedule(), and the shim calls onTimer(id) back on the UI
|
|
177
|
+
// thread once it comes due. Nothing here polls and nothing wakes
|
|
178
|
+
// periodically - an idle app is genuinely idle.
|
|
179
|
+
//
|
|
180
|
+
// This is the same shape the iOS shell must use, where the compiled TS links
|
|
181
|
+
// no event loop at all and could not hold a timer even if it wanted to.
|
|
180
182
|
asyncNames: string[] = [];
|
|
181
183
|
asyncHandlers: AsyncCommandHandler[] = [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
contIds: number[] = [];
|
|
185
|
+
contFns: (() => void)[] = [];
|
|
186
|
+
nextCont = 1; // ids start at 1; TIMER_JOBS (-1) is the shell's own
|
|
185
187
|
jobIds: number[] = [];
|
|
186
188
|
jobCbs: FsCallback[] = [];
|
|
187
|
-
ticking = false;
|
|
188
|
-
tickMs = TICK_IDLE_MS;
|
|
189
189
|
|
|
190
190
|
// ---- the drain -----------------------------------------------------------
|
|
191
191
|
// A finished job's bytes still have to be decoded into a TypeScript string,
|
|
@@ -194,6 +194,7 @@ export class JanelaAppImpl<
|
|
|
194
194
|
// decoded a slice at a time, giving the run loop the thread back between
|
|
195
195
|
// slices - total work is unchanged, but no single turn carries much of it.
|
|
196
196
|
drainIds: number[] = [];
|
|
197
|
+
draining = false; // a drain continuation is already queued
|
|
197
198
|
drainCbs: FsCallback[] = [];
|
|
198
199
|
drainOk: boolean[] = [];
|
|
199
200
|
drainParts: string[][] = [];
|
|
@@ -208,37 +209,49 @@ export class JanelaAppImpl<
|
|
|
208
209
|
wvInit(h, BOOTSTRAP);
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this.
|
|
221
|
-
this.
|
|
222
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Park `fn` with the shell and ask to be called back in `ms`.
|
|
214
|
+
*
|
|
215
|
+
* The id is the whole protocol: TS keeps the closure, the shell keeps the
|
|
216
|
+
* clock, and neither needs to know anything else about the other.
|
|
217
|
+
*/
|
|
218
|
+
schedule(ms: number, fn: () => void): void {
|
|
219
|
+
const id = this.nextCont;
|
|
220
|
+
this.nextCont = id + 1;
|
|
221
|
+
this.contIds.push(id);
|
|
222
|
+
this.contFns.push(fn);
|
|
223
|
+
const delay = ms > 0 ? ms : 0;
|
|
224
|
+
// `+ 0` per the note at the top of this file: a bare FFI call is not safe
|
|
225
|
+
// in every position, and this one is silently dropped without it.
|
|
226
|
+
const rc = wvSchedule(this.handle, id, delay) + 0;
|
|
227
|
+
if (rc < 0) console.log("[janela] could not schedule continuation", id);
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
/** Run the continuation parked under `id`, if it is still waiting. */
|
|
231
|
+
runCont(id: number): void {
|
|
232
|
+
for (let i = 0; i < this.contIds.length; i++) {
|
|
233
|
+
if (this.contIds[i] === id) {
|
|
234
|
+
const fn = this.contFns[i];
|
|
235
|
+
// Unregister BEFORE running: a continuation that schedules another one
|
|
236
|
+
// must not disturb the entry being removed, and a continuation that
|
|
237
|
+
// throws must not stay parked forever.
|
|
238
|
+
this.contIds.splice(i, 1);
|
|
239
|
+
this.contFns.splice(i, 1);
|
|
240
|
+
fn();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
234
243
|
}
|
|
235
|
-
this.ticking = false;
|
|
236
|
-
wvTickStop(this.handle);
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
// Decode as much of the pending payloads as the budget allows, then yield.
|
|
240
247
|
// Slices are taken from one job at a time so a big read finishes promptly
|
|
241
248
|
// rather than every concurrent read finishing slowly.
|
|
249
|
+
/**
|
|
250
|
+
* Decode as much of the pending payloads as the budget allows, then hand the
|
|
251
|
+
* thread back. If work remains, a zero-delay continuation carries on at the
|
|
252
|
+
* top of the next turn, so a 100 MB read is spread across frames instead of
|
|
253
|
+
* freezing one.
|
|
254
|
+
*/
|
|
242
255
|
drainSome(): void {
|
|
243
256
|
if (this.drainIds.length === 0) return;
|
|
244
257
|
const started = Date.now() + 0;
|
|
@@ -281,38 +294,39 @@ export class JanelaAppImpl<
|
|
|
281
294
|
// before starting another payload.
|
|
282
295
|
}
|
|
283
296
|
|
|
284
|
-
if (Date.now() - started >= DRAIN_BUDGET_MS)
|
|
297
|
+
if (Date.now() - started >= DRAIN_BUDGET_MS) {
|
|
298
|
+
this.drainMore();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
285
301
|
}
|
|
286
302
|
}
|
|
287
303
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
this.timerFns = keptFns;
|
|
310
|
-
this.timerDue = keptDue;
|
|
311
|
-
for (let i = 0; i < fire.length; i++) fire[i]();
|
|
304
|
+
/** Continue draining on the next turn, without stacking duplicate work. */
|
|
305
|
+
drainMore(): void {
|
|
306
|
+
if (this.drainIds.length === 0 || this.draining) return;
|
|
307
|
+
this.draining = true;
|
|
308
|
+
this.schedule(0, () => {
|
|
309
|
+
this.draining = false;
|
|
310
|
+
this.drainSome();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The shell calls here on the UI thread when something it was holding comes
|
|
316
|
+
* due: a continuation TS parked (id >= 1), or a job that finished
|
|
317
|
+
* (TIMER_JOBS). This is the only entry into the loop, and it always lands at
|
|
318
|
+
* the top of a fresh turn — never underneath a TS frame.
|
|
319
|
+
*/
|
|
320
|
+
onTimer(id: number): void {
|
|
321
|
+
if (id !== TIMER_JOBS) {
|
|
322
|
+
this.runCont(id);
|
|
323
|
+
return;
|
|
312
324
|
}
|
|
325
|
+
this.serviceJobs();
|
|
326
|
+
}
|
|
313
327
|
|
|
314
|
-
|
|
315
|
-
|
|
328
|
+
/** A job reached a terminal state: move the finished ones to the drain. */
|
|
329
|
+
serviceJobs(): void {
|
|
316
330
|
if (this.jobIds.length > 0) {
|
|
317
331
|
const keptIds: number[] = [];
|
|
318
332
|
const keptCbs: FsCallback[] = [];
|
|
@@ -346,8 +360,6 @@ export class JanelaAppImpl<
|
|
|
346
360
|
}
|
|
347
361
|
|
|
348
362
|
this.drainSome();
|
|
349
|
-
this.retick();
|
|
350
|
-
this.idle();
|
|
351
363
|
}
|
|
352
364
|
|
|
353
365
|
// Both dialog kinds share one path: start the job, then let the same drain
|
|
@@ -371,8 +383,7 @@ export class JanelaAppImpl<
|
|
|
371
383
|
encodeFilters(filters),
|
|
372
384
|
) + 0;
|
|
373
385
|
if (id < 0) {
|
|
374
|
-
this.
|
|
375
|
-
this.wake();
|
|
386
|
+
this.defer(() => cb(null, "EAGAIN: could not open a dialog"));
|
|
376
387
|
return;
|
|
377
388
|
}
|
|
378
389
|
this.jobIds.push(id);
|
|
@@ -384,7 +395,6 @@ export class JanelaAppImpl<
|
|
|
384
395
|
// "null" is a cancel; anything else is a JSON array of paths.
|
|
385
396
|
cb(JSON.parse(text) as string[] | null);
|
|
386
397
|
});
|
|
387
|
-
this.wake();
|
|
388
398
|
}
|
|
389
399
|
|
|
390
400
|
/**
|
|
@@ -427,16 +437,14 @@ export class JanelaAppImpl<
|
|
|
427
437
|
|
|
428
438
|
/** Run fn on the next turn of the host loop - the way to slice long work. */
|
|
429
439
|
defer(fn: () => void): void {
|
|
430
|
-
|
|
431
|
-
this.
|
|
440
|
+
// Zero delay: the shell posts straight to the next turn, no timer at all.
|
|
441
|
+
this.schedule(0, fn);
|
|
432
442
|
}
|
|
433
443
|
|
|
434
|
-
/** Run fn after at least ms. The
|
|
444
|
+
/** Run fn after at least ms. The shell owns the clock; scriptc's setTimeout
|
|
435
445
|
* cannot fire while the window is open (its loop is parked inside run()). */
|
|
436
446
|
sleep(ms: number, fn: () => void): void {
|
|
437
|
-
this.
|
|
438
|
-
this.timerDue.push(Date.now() + (ms > 0 ? ms : 0));
|
|
439
|
-
this.wake();
|
|
447
|
+
this.schedule(ms, fn);
|
|
440
448
|
}
|
|
441
449
|
|
|
442
450
|
/**
|
|
@@ -451,7 +459,6 @@ export class JanelaAppImpl<
|
|
|
451
459
|
}
|
|
452
460
|
this.jobIds.push(id);
|
|
453
461
|
this.jobCbs.push(cb);
|
|
454
|
-
this.wake();
|
|
455
462
|
}
|
|
456
463
|
|
|
457
464
|
/** Write a file without blocking the window; cb(null) on success. */
|
|
@@ -465,7 +472,6 @@ export class JanelaAppImpl<
|
|
|
465
472
|
// The write payload is empty on success; the shared callback shape just
|
|
466
473
|
// ignores the text argument.
|
|
467
474
|
this.jobCbs.push((err, _text) => cb(err));
|
|
468
|
-
this.wake();
|
|
469
475
|
}
|
|
470
476
|
|
|
471
477
|
/** Show the native "open" dialog; cb gets the paths, or null on cancel. */
|
|
@@ -531,8 +537,8 @@ export class JanelaAppImpl<
|
|
|
531
537
|
const h = this.handle;
|
|
532
538
|
// Both handlers are retained: registered once here, called by the shim
|
|
533
539
|
// for as long as the window is open.
|
|
534
|
-
|
|
535
|
-
this.
|
|
540
|
+
wvOnTimer(h, (id) => {
|
|
541
|
+
this.onTimer(id);
|
|
536
542
|
});
|
|
537
543
|
wvOnInvoke(h, (req) => {
|
|
538
544
|
const env = JSON.parse(req) as string[];
|
package/shim/wvshim.cc
CHANGED
|
@@ -8,15 +8,22 @@
|
|
|
8
8
|
// * format 3 — callback params may be `string`/`bytes`, so a payload crosses
|
|
9
9
|
// into TS as one argument instead of one FFI call per byte. Payloads going
|
|
10
10
|
// the other way ride `string` params on ordinary functions.
|
|
11
|
-
// * format 4 — callbacks may be `retained`, so the invoke and
|
|
11
|
+
// * format 4 — callbacks may be `retained`, so the invoke and timer handlers
|
|
12
12
|
// are registered once and live for the app's lifetime. wv_run() is a plain
|
|
13
13
|
// blocking call again; it no longer has to carry a callback whose "call
|
|
14
14
|
// scope" was standing in for "app lifetime".
|
|
15
|
+
//
|
|
16
|
+
// The shell owns scheduling. TS never holds a timer: it registers a
|
|
17
|
+
// continuation under an id and calls wv_schedule(), and this shim calls back
|
|
18
|
+
// into TS with that id once the delay is up. That is the same shape a library
|
|
19
|
+
// -mode host (iOS) must use, where the compiled TS links no event loop at all.
|
|
15
20
|
|
|
16
21
|
#include "webview.h"
|
|
17
22
|
|
|
23
|
+
#include <algorithm>
|
|
18
24
|
#include <atomic>
|
|
19
25
|
#include <chrono>
|
|
26
|
+
#include <condition_variable>
|
|
20
27
|
#include <cstdint>
|
|
21
28
|
#include <cstdio>
|
|
22
29
|
#include <cstring>
|
|
@@ -50,6 +57,13 @@ struct Pending {
|
|
|
50
57
|
std::string call_id;
|
|
51
58
|
};
|
|
52
59
|
|
|
60
|
+
// A continuation TS has parked with the shell: run whatever TS registered
|
|
61
|
+
// under `id` once `due` has passed. The shell owns the clock; TS owns the id.
|
|
62
|
+
struct Timer {
|
|
63
|
+
int32_t id;
|
|
64
|
+
std::chrono::steady_clock::time_point due;
|
|
65
|
+
};
|
|
66
|
+
|
|
53
67
|
struct App {
|
|
54
68
|
webview_t w = nullptr;
|
|
55
69
|
bool used = false;
|
|
@@ -59,8 +73,8 @@ struct App {
|
|
|
59
73
|
// request rides in as a (ptr, len) string param.
|
|
60
74
|
int32_t (*on_invoke)(const uint8_t *, size_t, void *) = nullptr;
|
|
61
75
|
void *on_invoke_ctx = nullptr;
|
|
62
|
-
void (*
|
|
63
|
-
void *
|
|
76
|
+
void (*on_timer)(int32_t, void *) = nullptr;
|
|
77
|
+
void *on_timer_ctx = nullptr;
|
|
64
78
|
|
|
65
79
|
// Staging for the in-flight request.
|
|
66
80
|
std::string req; // JSON args array from JS
|
|
@@ -69,13 +83,26 @@ struct App {
|
|
|
69
83
|
uint32_t seq = 0;
|
|
70
84
|
|
|
71
85
|
// ---- async support ----
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
std::
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
// The held-reply table: an invoke whose answer is not ready yet. The page's
|
|
87
|
+
// promise stays unsettled until wv_resolve() answers this call id.
|
|
88
|
+
std::vector<Pending> pending;
|
|
89
|
+
bool deferred = false; // set by wv_defer() during the current call
|
|
90
|
+
|
|
91
|
+
// The shell's timer queue, and the one thread that watches it. The thread
|
|
92
|
+
// only sleeps and posts — it never touches TS, because scriptc's runtime is
|
|
93
|
+
// not thread-safe. Everything reaches TS through webview_dispatch, on the UI
|
|
94
|
+
// thread, on a LATER turn of the shell's own loop (see timer_on_ui_thread).
|
|
95
|
+
std::vector<Timer> timers;
|
|
96
|
+
std::mutex timers_mu;
|
|
97
|
+
std::condition_variable timers_cv;
|
|
98
|
+
std::thread scheduler;
|
|
99
|
+
std::atomic<bool> scheduling{false};
|
|
77
100
|
};
|
|
78
101
|
|
|
102
|
+
// Reserved timer id: not a TS continuation but "a job changed state, service
|
|
103
|
+
// them". TS allocates its own continuation ids from 1 upwards.
|
|
104
|
+
const int32_t TIMER_JOBS = -1;
|
|
105
|
+
|
|
79
106
|
// Fixed-size table: handles are indices, never pointers.
|
|
80
107
|
App g_apps[8];
|
|
81
108
|
|
|
@@ -92,8 +119,9 @@ std::string to_str(const uint8_t *p, size_t n) {
|
|
|
92
119
|
// ---- jobs ------------------------------------------------------------------
|
|
93
120
|
//
|
|
94
121
|
// A job is any unit of work whose answer cannot be produced during the FFI
|
|
95
|
-
// call that asks for it. TS starts one
|
|
96
|
-
//
|
|
122
|
+
// call that asks for it. TS starts one and gets an id back immediately; when
|
|
123
|
+
// the job reaches a terminal state it posts TIMER_JOBS to the UI thread, and
|
|
124
|
+
// TS then reads wv_job_status() for the jobs it is waiting on.
|
|
97
125
|
//
|
|
98
126
|
// Two kinds use this pool, for opposite reasons:
|
|
99
127
|
// * file I/O — the blocking syscall must happen off the UI thread, so a
|
|
@@ -102,9 +130,9 @@ std::string to_str(const uint8_t *p, size_t n) {
|
|
|
102
130
|
// later, on the UI thread.
|
|
103
131
|
// * native dialogs — the modal must run ON the UI thread, but not while TS
|
|
104
132
|
// is on the stack (runModal/gtk_dialog_run spin a nested event loop, which
|
|
105
|
-
// would re-enter
|
|
106
|
-
//
|
|
107
|
-
//
|
|
133
|
+
// would re-enter TS underneath the invoke handler that asked for the
|
|
134
|
+
// dialog). So the job is posted with webview_dispatch and runs at the top
|
|
135
|
+
// of a later turn, with no TS frame beneath it.
|
|
108
136
|
|
|
109
137
|
const int32_t JOB_PENDING = 0;
|
|
110
138
|
const int32_t JOB_OK = 1;
|
|
@@ -119,6 +147,9 @@ struct Job {
|
|
|
119
147
|
std::string data; // payload on success, the error message on failure
|
|
120
148
|
std::thread worker; // unused by dialog jobs, which run on the UI thread
|
|
121
149
|
bool used = false;
|
|
150
|
+
// Which app to wake when this job finishes. Without the ticker there is
|
|
151
|
+
// nothing polling, so a finished job has to announce itself.
|
|
152
|
+
int32_t app = -1;
|
|
122
153
|
};
|
|
123
154
|
|
|
124
155
|
// Jobs are addressed by index and held behind unique_ptr so the vector may
|
|
@@ -135,7 +166,7 @@ Job *job_at(int32_t id) {
|
|
|
135
166
|
|
|
136
167
|
// Reuses a finished slot when one is free, so a long-running app that reads
|
|
137
168
|
// many files does not grow the table without bound.
|
|
138
|
-
int32_t new_job() {
|
|
169
|
+
int32_t new_job(int32_t app) {
|
|
139
170
|
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
140
171
|
for (size_t i = 0; i < g_jobs.size(); i++) {
|
|
141
172
|
if (g_jobs[i]->used) continue;
|
|
@@ -143,10 +174,12 @@ int32_t new_job() {
|
|
|
143
174
|
g_jobs[i]->status.store(JOB_PENDING);
|
|
144
175
|
g_jobs[i]->data.clear();
|
|
145
176
|
g_jobs[i]->used = true;
|
|
177
|
+
g_jobs[i]->app = app;
|
|
146
178
|
return static_cast<int32_t>(i);
|
|
147
179
|
}
|
|
148
180
|
g_jobs.push_back(std::unique_ptr<Job>(new Job()));
|
|
149
181
|
g_jobs.back()->used = true;
|
|
182
|
+
g_jobs.back()->app = app;
|
|
150
183
|
return static_cast<int32_t>(g_jobs.size() - 1);
|
|
151
184
|
}
|
|
152
185
|
|
|
@@ -166,9 +199,18 @@ std::string fs_error_message(const std::string &path, const char *op) {
|
|
|
166
199
|
return "EIO: failed to " + std::string(op) + " '" + path + "'";
|
|
167
200
|
}
|
|
168
201
|
|
|
202
|
+
// Defined below, once the app table is in scope. Posts `id` to the app's UI
|
|
203
|
+
// thread via webview_dispatch, so TS is entered on a later turn of the shell's
|
|
204
|
+
// own loop and never underneath a frame it is already inside.
|
|
205
|
+
void post_timer(int32_t app, int32_t id);
|
|
206
|
+
|
|
169
207
|
void job_finish(Job *j, int32_t status, std::string payload) {
|
|
170
208
|
j->data = std::move(payload);
|
|
171
209
|
j->status.store(status, std::memory_order_release);
|
|
210
|
+
// Nothing polls any more, so a finished job announces itself. Safe from a
|
|
211
|
+
// worker thread: webview_dispatch is the documented cross-thread hand-off,
|
|
212
|
+
// and it only queues — TS runs later, on the UI thread.
|
|
213
|
+
if (j->app >= 0) post_timer(j->app, TIMER_JOBS);
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
void fs_read_worker(Job *j, std::string path) {
|
|
@@ -614,18 +656,67 @@ void trampoline(const char *id, const char *req, void *arg) {
|
|
|
614
656
|
webview_return(a->w, a->cur_id.c_str(), status,
|
|
615
657
|
a->reply.empty() ? "null" : a->reply.c_str());
|
|
616
658
|
// wv_defer() treats a non-empty cur_id as "an invoke is in flight". Clearing
|
|
617
|
-
// it here means a defer from anywhere else — a
|
|
659
|
+
// it here means a defer from anywhere else — a timer, say — fails with -1
|
|
618
660
|
// instead of stealing this already-answered call's id.
|
|
619
661
|
a->cur_id.clear();
|
|
620
662
|
}
|
|
621
663
|
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
void
|
|
625
|
-
|
|
626
|
-
|
|
664
|
+
// The app index and the timer id, packed into the single void* that
|
|
665
|
+
// webview_dispatch carries.
|
|
666
|
+
void *pack_timer(int32_t app, int32_t id) {
|
|
667
|
+
uintptr_t packed = (static_cast<uintptr_t>(static_cast<uint32_t>(app)) << 32) |
|
|
668
|
+
static_cast<uint32_t>(id);
|
|
669
|
+
return reinterpret_cast<void *>(packed);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Runs on the UI thread, posted via webview_dispatch, so the TS it calls stays
|
|
673
|
+
// single-threaded — scriptc's runtime is NOT thread-safe.
|
|
674
|
+
//
|
|
675
|
+
// This is also the one place that guarantees the shell never re-enters TS from
|
|
676
|
+
// inside a frame TS is already in. Everything that wants to reach TS — a due
|
|
677
|
+
// timer, a finished file read, a dismissed dialog — goes through a dispatch
|
|
678
|
+
// and therefore lands at the top of a later turn, with no TS beneath it. That
|
|
679
|
+
// rule is invisible when broken: a violating host gets correct-looking results
|
|
680
|
+
// right up until it doesn't, so it is kept by construction, not by testing.
|
|
681
|
+
void timer_on_ui_thread(webview_t, void *arg) {
|
|
682
|
+
uintptr_t packed = reinterpret_cast<uintptr_t>(arg);
|
|
683
|
+
App *a = &g_apps[packed >> 32];
|
|
684
|
+
int32_t id = static_cast<int32_t>(static_cast<uint32_t>(packed & 0xffffffffu));
|
|
685
|
+
if (!a->used || !a->on_timer) return; // app quit between dispatch and delivery
|
|
627
686
|
a->seq++;
|
|
628
|
-
a->
|
|
687
|
+
a->on_timer(id, a->on_timer_ctx);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
void post_timer(int32_t app, int32_t id) {
|
|
691
|
+
if (app < 0 || app >= 8) return;
|
|
692
|
+
App *a = &g_apps[app];
|
|
693
|
+
if (!a->used || !a->w) return;
|
|
694
|
+
webview_dispatch(a->w, timer_on_ui_thread, pack_timer(app, id));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// The scheduler thread: sleep until the earliest timer is due, hand its id to
|
|
698
|
+
// the UI thread, repeat. It never touches TS and holds no TS state.
|
|
699
|
+
void scheduler_loop(App *a, int32_t h) {
|
|
700
|
+
std::unique_lock<std::mutex> lk(a->timers_mu);
|
|
701
|
+
while (a->scheduling.load()) {
|
|
702
|
+
if (a->timers.empty()) {
|
|
703
|
+
a->timers_cv.wait(lk);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
auto soonest = std::min_element(
|
|
707
|
+
a->timers.begin(), a->timers.end(),
|
|
708
|
+
[](const Timer &x, const Timer &y) { return x.due < y.due; });
|
|
709
|
+
auto due = soonest->due;
|
|
710
|
+
if (due > std::chrono::steady_clock::now()) {
|
|
711
|
+
a->timers_cv.wait_until(lk, due);
|
|
712
|
+
continue; // re-check: an earlier timer may have arrived meanwhile
|
|
713
|
+
}
|
|
714
|
+
int32_t id = soonest->id;
|
|
715
|
+
a->timers.erase(soonest);
|
|
716
|
+
lk.unlock();
|
|
717
|
+
post_timer(h, id);
|
|
718
|
+
lk.lock();
|
|
719
|
+
}
|
|
629
720
|
}
|
|
630
721
|
|
|
631
722
|
} // namespace
|
|
@@ -642,16 +733,16 @@ int32_t wv_create(int32_t debug) {
|
|
|
642
733
|
g_apps[i].binds.clear();
|
|
643
734
|
g_apps[i].on_invoke = nullptr;
|
|
644
735
|
g_apps[i].on_invoke_ctx = nullptr;
|
|
645
|
-
g_apps[i].
|
|
646
|
-
g_apps[i].
|
|
736
|
+
g_apps[i].on_timer = nullptr;
|
|
737
|
+
g_apps[i].on_timer_ctx = nullptr;
|
|
647
738
|
g_apps[i].req.clear();
|
|
648
739
|
g_apps[i].cur_id.clear();
|
|
649
740
|
g_apps[i].reply.clear();
|
|
650
741
|
g_apps[i].seq = 0;
|
|
651
742
|
g_apps[i].pending.clear();
|
|
652
743
|
g_apps[i].deferred = false;
|
|
653
|
-
g_apps[i].
|
|
654
|
-
g_apps[i].
|
|
744
|
+
g_apps[i].scheduling.store(false);
|
|
745
|
+
g_apps[i].timers.clear();
|
|
655
746
|
g_apps[i].w = w;
|
|
656
747
|
g_apps[i].used = true;
|
|
657
748
|
return i;
|
|
@@ -723,8 +814,8 @@ int32_t wv_reply(int32_t h, const uint8_t *p, size_t n) {
|
|
|
723
814
|
// call, and wv_run() is one such call for the app's whole life — so setTimeout
|
|
724
815
|
// and promise continuations in TS never fire while the window is open. These
|
|
725
816
|
// four functions supply the missing loop: TS may postpone an invoke's answer
|
|
726
|
-
// (wv_defer), answer it later (wv_resolve), and
|
|
727
|
-
// on the UI thread
|
|
817
|
+
// (wv_defer), answer it later (wv_resolve), and park a continuation with the
|
|
818
|
+
// shell to be called back on the UI thread when it comes due (wv_schedule).
|
|
728
819
|
|
|
729
820
|
// Postpone the answer to the invoke being handled right now. Returns a
|
|
730
821
|
// pending id to hand back to wv_resolve(), or -1 outside a bind callback.
|
|
@@ -759,43 +850,60 @@ int32_t wv_resolve(int32_t h, int32_t id, int32_t status) {
|
|
|
759
850
|
return 0;
|
|
760
851
|
}
|
|
761
852
|
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
|
|
853
|
+
// Ask the shell to call the retained timer handler with `id` after `ms`.
|
|
854
|
+
//
|
|
855
|
+
// This is the whole of scheduling: TS keeps the continuation, the shell keeps
|
|
856
|
+
// the clock. A zero delay is not a special case — it posts on the next turn of
|
|
857
|
+
// the loop, which is exactly what app.defer() wants, with no timer involved.
|
|
858
|
+
//
|
|
859
|
+
// An idle app now costs nothing at all: with no timers queued the scheduler
|
|
860
|
+
// thread blocks on a condition variable rather than waking every few
|
|
861
|
+
// milliseconds to find nothing to do.
|
|
862
|
+
int32_t wv_schedule(int32_t h, int32_t id, int32_t ms) {
|
|
765
863
|
App *a = app_at(h);
|
|
766
864
|
if (!a) return -1;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
865
|
+
|
|
866
|
+
// Zero delay skips the queue: there is nothing to wait for, and posting
|
|
867
|
+
// straight to the UI thread keeps a defer() chain as short as possible.
|
|
868
|
+
if (ms <= 0) {
|
|
869
|
+
post_timer(h, id);
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
{
|
|
874
|
+
std::lock_guard<std::mutex> lock(a->timers_mu);
|
|
875
|
+
a->timers.push_back(
|
|
876
|
+
Timer{id, std::chrono::steady_clock::now() +
|
|
877
|
+
std::chrono::milliseconds(ms)});
|
|
878
|
+
if (!a->scheduling.exchange(true)) {
|
|
879
|
+
a->scheduler = std::thread(scheduler_loop, a, h);
|
|
776
880
|
}
|
|
777
|
-
}
|
|
881
|
+
}
|
|
882
|
+
a->timers_cv.notify_one();
|
|
778
883
|
return 0;
|
|
779
884
|
}
|
|
780
885
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (!a->
|
|
785
|
-
|
|
786
|
-
|
|
886
|
+
// Stop the scheduler thread and drop any timers that never came due. Called on
|
|
887
|
+
// the way out of wv_run(), so nothing can reach TS after the window closes.
|
|
888
|
+
void stop_scheduler(App *a) {
|
|
889
|
+
if (!a->scheduling.exchange(false)) return;
|
|
890
|
+
a->timers_cv.notify_all();
|
|
891
|
+
if (a->scheduler.joinable()) a->scheduler.join();
|
|
892
|
+
std::lock_guard<std::mutex> lock(a->timers_mu);
|
|
893
|
+
a->timers.clear();
|
|
787
894
|
}
|
|
788
895
|
|
|
789
896
|
// ---- async file I/O ---------------------------------------------------------
|
|
790
897
|
//
|
|
791
898
|
// wv_fs_read/wv_fs_write start a worker thread and return immediately with a
|
|
792
|
-
// job id. TS
|
|
899
|
+
// job id. TS is woken with TIMER_JOBS when it finishes, reads wv_job_status()
|
|
900
|
+
// and drains the payload
|
|
793
901
|
// with wv_fs_byte() once the job is terminal. On failure the payload is the
|
|
794
902
|
// error message, so success and failure share one drain path.
|
|
795
903
|
|
|
796
904
|
int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
797
905
|
if (!app_at(h)) return -1;
|
|
798
|
-
int32_t id = new_job();
|
|
906
|
+
int32_t id = new_job(h);
|
|
799
907
|
Job *j = job_at(id);
|
|
800
908
|
if (!j) return -1;
|
|
801
909
|
j->worker = std::thread(fs_read_worker, j, to_str(p, n));
|
|
@@ -805,7 +913,7 @@ int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
|
805
913
|
int32_t wv_fs_write(int32_t h, const uint8_t *p, size_t n, const uint8_t *dp,
|
|
806
914
|
size_t dn) {
|
|
807
915
|
if (!app_at(h)) return -1;
|
|
808
|
-
int32_t id = new_job();
|
|
916
|
+
int32_t id = new_job(h);
|
|
809
917
|
Job *j = job_at(id);
|
|
810
918
|
if (!j) return -1;
|
|
811
919
|
j->worker = std::thread(fs_write_worker, j, to_str(p, n), to_str(dp, dn));
|
|
@@ -906,7 +1014,7 @@ int32_t wv_dialog(int32_t h, int32_t kind, int32_t flags, const uint8_t *tp,
|
|
|
906
1014
|
size_t nn, const uint8_t *fp, size_t fn) {
|
|
907
1015
|
App *a = app_at(h);
|
|
908
1016
|
if (!a) return -1;
|
|
909
|
-
int32_t id = new_job();
|
|
1017
|
+
int32_t id = new_job(h);
|
|
910
1018
|
Job *j = job_at(id);
|
|
911
1019
|
if (!j) return -1;
|
|
912
1020
|
|
|
@@ -994,12 +1102,13 @@ int32_t wv_on_invoke(int32_t h,
|
|
|
994
1102
|
return 0;
|
|
995
1103
|
}
|
|
996
1104
|
|
|
997
|
-
// Register the retained handler the
|
|
998
|
-
|
|
1105
|
+
// Register the retained handler the shell calls when a scheduled id comes due
|
|
1106
|
+
// (and with TIMER_JOBS when a file read or dialog finishes).
|
|
1107
|
+
int32_t wv_on_timer(int32_t h, void (*cb)(int32_t, void *), void *ctx) {
|
|
999
1108
|
App *a = app_at(h);
|
|
1000
1109
|
if (!a) return -1;
|
|
1001
|
-
a->
|
|
1002
|
-
a->
|
|
1110
|
+
a->on_timer = cb;
|
|
1111
|
+
a->on_timer_ctx = ctx;
|
|
1003
1112
|
return 0;
|
|
1004
1113
|
}
|
|
1005
1114
|
|
|
@@ -1009,11 +1118,10 @@ int32_t wv_run(int32_t h) {
|
|
|
1009
1118
|
if (!a) return -1;
|
|
1010
1119
|
int rc = webview_run(a->w);
|
|
1011
1120
|
// Nothing may call into TS once run() has returned.
|
|
1012
|
-
a
|
|
1013
|
-
if (a->ticker.joinable()) a->ticker.join();
|
|
1121
|
+
stop_scheduler(a);
|
|
1014
1122
|
jobs_join_all(); // nor may an in-flight read outlive the app
|
|
1015
1123
|
a->on_invoke = nullptr;
|
|
1016
|
-
a->
|
|
1124
|
+
a->on_timer = nullptr;
|
|
1017
1125
|
return rc;
|
|
1018
1126
|
}
|
|
1019
1127
|
|
|
@@ -1026,8 +1134,7 @@ int32_t wv_terminate(int32_t h) {
|
|
|
1026
1134
|
int32_t wv_destroy(int32_t h) {
|
|
1027
1135
|
App *a = app_at(h);
|
|
1028
1136
|
if (!a) return -1;
|
|
1029
|
-
a
|
|
1030
|
-
if (a->ticker.joinable()) a->ticker.join();
|
|
1137
|
+
stop_scheduler(a);
|
|
1031
1138
|
webview_destroy(a->w);
|
|
1032
1139
|
a->used = false;
|
|
1033
1140
|
a->w = nullptr;
|