janela 0.8.0 → 0.10.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/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 tick handlers
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 (*on_tick)(void *) = nullptr;
63
- void *on_tick_ctx = nullptr;
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
- std::vector<Pending> pending; // deferred invokes, addressed by index
73
- bool deferred = false; // set by wv_defer() during the current call
74
- std::thread ticker; // pure-C++ thread; never touches TS itself
75
- std::atomic<bool> ticking{false};
76
- std::atomic<int32_t> tick_ms{16};
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, gets an id back immediately, and polls
96
- // wv_job_status() from its tick loop until the job is terminal.
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 the tick handler underneath the invoke handler that asked
106
- // for the dialog). So the job is posted with webview_dispatch and runs at
107
- // the top of a later turn, with no TS frame beneath it.
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 tick, say — fails with -1
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
- // Runs on the UI thread (posted by the ticker via webview_dispatch), so the
623
- // TS it calls stays single-threaded — scriptc's runtime is NOT thread-safe.
624
- void tick_on_ui_thread(webview_t, void *arg) {
625
- App *a = &g_apps[reinterpret_cast<uintptr_t>(arg)];
626
- if (!a->used || !a->on_tick) return; // app quit between dispatch and delivery
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->on_tick(a->on_tick_ctx);
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].on_tick = nullptr;
646
- g_apps[i].on_tick_ctx = nullptr;
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].ticking.store(false);
654
- g_apps[i].tick_ms.store(16);
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 get called back periodically
727
- // on the UI thread to make progress (wv_tick_start / wv_tick_stop).
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
- // Start pumping the retained tick handler every interval_ms. The thread
763
- // itself only sleeps and posts; all TS execution happens on the UI thread.
764
- int32_t wv_tick_start(int32_t h, int32_t interval_ms) {
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
- a->tick_ms = interval_ms > 0 ? interval_ms : 16;
768
- if (a->ticking.exchange(true)) return 0; // already running
769
- uintptr_t idx = static_cast<uintptr_t>(h);
770
- a->ticker = std::thread([a, idx]() {
771
- while (a->ticking.load()) {
772
- std::this_thread::sleep_for(
773
- std::chrono::milliseconds(a->tick_ms.load()));
774
- if (!a->ticking.load()) break;
775
- webview_dispatch(a->w, tick_on_ui_thread, reinterpret_cast<void *>(idx));
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
- int32_t wv_tick_stop(int32_t h) {
782
- App *a = app_at(h);
783
- if (!a) return -1;
784
- if (!a->ticking.exchange(false)) return 0;
785
- if (a->ticker.joinable()) a->ticker.join();
786
- return 0;
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 polls wv_job_status() from its tick loop and drains the payload
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 ticker pumps on the UI thread.
998
- int32_t wv_on_tick(int32_t h, void (*cb)(void *), void *ctx) {
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->on_tick = cb;
1002
- a->on_tick_ctx = ctx;
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->ticking.store(false);
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->on_tick = nullptr;
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->ticking.store(false);
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;
@@ -6,5 +6,10 @@
6
6
  "title": "__NAME__",
7
7
  "width": 640,
8
8
  "height": 420
9
+ },
10
+ "ios": {
11
+ "identifier": "dev.janela.__NAME__",
12
+ "displayName": "__NAME__",
13
+ "minimumVersion": "15.0"
9
14
  }
10
15
  }
@@ -6,5 +6,10 @@
6
6
  "title": "__NAME__",
7
7
  "width": 640,
8
8
  "height": 420
9
+ },
10
+ "ios": {
11
+ "identifier": "dev.janela.__NAME__",
12
+ "displayName": "__NAME__",
13
+ "minimumVersion": "15.0"
9
14
  }
10
15
  }
@@ -6,5 +6,10 @@
6
6
  "title": "__NAME__",
7
7
  "width": 640,
8
8
  "height": 420
9
+ },
10
+ "ios": {
11
+ "identifier": "dev.janela.__NAME__",
12
+ "displayName": "__NAME__",
13
+ "minimumVersion": "15.0"
9
14
  }
10
15
  }
@@ -6,5 +6,10 @@
6
6
  "title": "__NAME__",
7
7
  "width": 640,
8
8
  "height": 420
9
+ },
10
+ "ios": {
11
+ "identifier": "dev.janela.__NAME__",
12
+ "displayName": "__NAME__",
13
+ "minimumVersion": "15.0"
9
14
  }
10
15
  }
@@ -6,5 +6,10 @@
6
6
  "title": "__NAME__",
7
7
  "width": 640,
8
8
  "height": 420
9
+ },
10
+ "ios": {
11
+ "identifier": "dev.janela.__NAME__",
12
+ "displayName": "__NAME__",
13
+ "minimumVersion": "15.0"
9
14
  }
10
15
  }