mini_racer 0.21.4 → 0.22.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.
@@ -2,6 +2,7 @@
2
2
  #include "v8-profiler.h"
3
3
  #include "libplatform/libplatform.h"
4
4
  #include "mini_racer_v8.h"
5
+ #include <atomic>
5
6
  #include <memory>
6
7
  #include <vector>
7
8
  #include <cassert>
@@ -68,6 +69,12 @@ struct Callback
68
69
  int32_t id;
69
70
  };
70
71
 
72
+ enum : unsigned
73
+ {
74
+ NON_WATCHDOG_TERMINATION = 1,
75
+ WATCHDOG_TERMINATION = 2,
76
+ };
77
+
71
78
  // NOTE: do *not* use thread_locals to store state. In single-threaded
72
79
  // mode, V8 runs on the same thread as Ruby and the Ruby runtime clobbers
73
80
  // thread-locals when it context-switches threads. Ruby 3.4.0 has a new
@@ -91,6 +98,14 @@ struct State
91
98
  Context *ruby_context;
92
99
  int64_t max_memory;
93
100
  int err_reason;
101
+ // TerminateExecution() while idle doesn't make IsExecutionTerminating() true
102
+ std::atomic<unsigned> terminate_requested;
103
+ // Armed before checking terminate_requested so a concurrent terminator
104
+ // either gets observed or posts a task. Uses sequential consistency.
105
+ std::atomic<bool> waiting_for_task;
106
+ // Tracks reentrant call/eval dispatches so nested await calls can fail
107
+ // instead of deadlocking and nested pumps preserve outer termination.
108
+ int javascript_call_depth;
94
109
  bool verbose_exceptions;
95
110
  std::vector<Callback*> callbacks;
96
111
  std::unique_ptr<v8::ArrayBuffer::Allocator> allocator;
@@ -416,6 +431,7 @@ void v8_gc_callback(v8::Isolate*, v8::GCType, v8::GCCallbackFlags, void *data)
416
431
  int64_t used_heap_size = static_cast<int64_t>(s.used_heap_size());
417
432
  if (used_heap_size > st.max_memory) {
418
433
  st.err_reason = MEMORY_ERROR;
434
+ st.terminate_requested.fetch_or(NON_WATCHDOG_TERMINATION);
419
435
  st.isolate->TerminateExecution();
420
436
  }
421
437
  }
@@ -586,8 +602,76 @@ fail:
586
602
  reply_retry(st, err);
587
603
  }
588
604
 
605
+ struct JavascriptCallScope
606
+ {
607
+ int& depth;
608
+
609
+ explicit JavascriptCallScope(int& depth) : depth(depth) { depth++; }
610
+ ~JavascriptCallScope() { depth--; }
611
+ };
612
+
613
+ void throw_nested_await_call(State& st)
614
+ {
615
+ // V8 does not run microtask checkpoints recursively. A nested await call
616
+ // from an attached Ruby callback can therefore deadlock, so reject it
617
+ // before entering JavaScript.
618
+ auto message = v8::String::NewFromUtf8Literal(
619
+ st.isolate, "nested call_await/eval_await is not supported");
620
+ st.isolate->ThrowException(v8::Exception::Error(message));
621
+ }
622
+
623
+ // awaits |*result| if it's a promise; false means an exception is pending
624
+ bool await_promise(State& st, v8::Local<v8::Value> *result)
625
+ {
626
+ if (!(*result)->IsPromise()) return true;
627
+ auto promise = result->As<v8::Promise>();
628
+ for (;;) {
629
+ v8::MicrotasksScope::PerformCheckpoint(st.isolate);
630
+ switch (promise->State()) {
631
+ case v8::Promise::kFulfilled:
632
+ *result = promise->Result();
633
+ return true;
634
+ case v8::Promise::kRejected:
635
+ st.isolate->ThrowException(promise->Result());
636
+ return false;
637
+ case v8::Promise::kPending:
638
+ break;
639
+ }
640
+ st.waiting_for_task.store(true);
641
+ if (st.terminate_requested.load() || st.isolate->IsExecutionTerminating()) {
642
+ st.waiting_for_task.store(false);
643
+ return false;
644
+ }
645
+ // blocks until the next task; a concurrent v8_terminate_execution
646
+ // observes waiting_for_task and posts one to end the wait
647
+ v8::platform::PumpMessageLoop(
648
+ platform, st.isolate,
649
+ v8::platform::MessageLoopBehavior::kWaitForWork);
650
+ st.waiting_for_task.store(false);
651
+ }
652
+ }
653
+
654
+ // Nested calls report termination but leave it pending for the enclosing call.
655
+ // Outermost calls consume it as before.
656
+ bool suspend_termination(State& st, bool nested, int& cause)
657
+ {
658
+ unsigned requests = nested ? st.terminate_requested.load()
659
+ : st.terminate_requested.exchange(0);
660
+ if (!requests && !st.isolate->IsExecutionTerminating()) return false;
661
+ st.isolate->CancelTerminateExecution();
662
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
663
+ if (!nested) st.err_reason = NO_ERROR;
664
+ return nested;
665
+ }
666
+
667
+ void restore_termination(State& st, bool suspended)
668
+ {
669
+ if (suspended)
670
+ st.isolate->TerminateExecution();
671
+ }
672
+
589
673
  // response is errback [result, err] array
590
- extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
674
+ void v8_call_impl(State *pst, const uint8_t *p, size_t n, bool await)
591
675
  {
592
676
  State& st = *pst;
593
677
  v8::TryCatch try_catch(st.isolate);
@@ -598,6 +682,14 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
598
682
  des.ReadHeader(st.context).Check();
599
683
  v8::Local<v8::Value> result;
600
684
  int cause = INTERNAL_ERROR;
685
+ bool preserve_termination = false;
686
+ bool nested = st.javascript_call_depth > 0;
687
+ JavascriptCallScope call_scope(st.javascript_call_depth);
688
+ if (await && nested) {
689
+ throw_nested_await_call(st);
690
+ cause = RUNTIME_ERROR;
691
+ goto fail;
692
+ }
601
693
  {
602
694
  v8::Local<v8::Value> request_v;
603
695
  if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
@@ -645,16 +737,16 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
645
737
  auto maybe_result_v = function->Call(st.context, obj, args.size(), args.data());
646
738
  v8::Local<v8::Value> result_v;
647
739
  if (!maybe_result_v.ToLocal(&result_v)) goto fail;
740
+ if (await && !await_promise(st, &result_v)) goto fail;
648
741
  result = sanitize(st, result_v);
649
742
  }
650
743
  cause = NO_ERROR;
651
744
  fail:
652
- if (st.isolate->IsExecutionTerminating()) {
653
- st.isolate->CancelTerminateExecution();
654
- cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
655
- st.err_reason = NO_ERROR;
745
+ preserve_termination = suspend_termination(st, nested, cause);
746
+ if (bubble_up_ruby_exception(st, &try_catch)) {
747
+ restore_termination(st, preserve_termination);
748
+ return;
656
749
  }
657
- if (bubble_up_ruby_exception(st, &try_catch)) return;
658
750
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
659
751
  if (cause) result = v8::Undefined(st.isolate);
660
752
  auto err = to_error(st, &try_catch, cause);
@@ -662,10 +754,21 @@ fail:
662
754
  assert(try_catch.HasCaught());
663
755
  goto fail; // retry; can be termination exception
664
756
  }
757
+ restore_termination(st, preserve_termination);
758
+ }
759
+
760
+ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
761
+ {
762
+ v8_call_impl(pst, p, n, false);
763
+ }
764
+
765
+ extern "C" void v8_call_await(State *pst, const uint8_t *p, size_t n)
766
+ {
767
+ v8_call_impl(pst, p, n, true);
665
768
  }
666
769
 
667
770
  // response is errback [result, err] array
668
- extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
771
+ void v8_eval_impl(State *pst, const uint8_t *p, size_t n, bool await)
669
772
  {
670
773
  State& st = *pst;
671
774
  v8::TryCatch try_catch(st.isolate);
@@ -675,6 +778,14 @@ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
675
778
  des.ReadHeader(st.context).Check();
676
779
  v8::Local<v8::Value> result;
677
780
  int cause = INTERNAL_ERROR;
781
+ bool preserve_termination = false;
782
+ bool nested = st.javascript_call_depth > 0;
783
+ JavascriptCallScope call_scope(st.javascript_call_depth);
784
+ if (await && nested) {
785
+ throw_nested_await_call(st);
786
+ cause = RUNTIME_ERROR;
787
+ goto fail;
788
+ }
678
789
  {
679
790
  v8::Local<v8::Value> request_v;
680
791
  if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
@@ -694,16 +805,16 @@ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
694
805
  cause = RUNTIME_ERROR;
695
806
  auto maybe_result_v = script->Run(st.context);
696
807
  if (!maybe_result_v.ToLocal(&result_v)) goto fail;
808
+ if (await && !await_promise(st, &result_v)) goto fail;
697
809
  result = sanitize(st, result_v);
698
810
  }
699
811
  cause = NO_ERROR;
700
812
  fail:
701
- if (st.isolate->IsExecutionTerminating()) {
702
- st.isolate->CancelTerminateExecution();
703
- cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
704
- st.err_reason = NO_ERROR;
813
+ preserve_termination = suspend_termination(st, nested, cause);
814
+ if (bubble_up_ruby_exception(st, &try_catch)) {
815
+ restore_termination(st, preserve_termination);
816
+ return;
705
817
  }
706
- if (bubble_up_ruby_exception(st, &try_catch)) return;
707
818
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
708
819
  if (cause) result = v8::Undefined(st.isolate);
709
820
  auto err = to_error(st, &try_catch, cause);
@@ -711,6 +822,17 @@ fail:
711
822
  assert(try_catch.HasCaught());
712
823
  goto fail; // retry; can be termination exception
713
824
  }
825
+ restore_termination(st, preserve_termination);
826
+ }
827
+
828
+ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
829
+ {
830
+ v8_eval_impl(pst, p, n, false);
831
+ }
832
+
833
+ extern "C" void v8_eval_await(State *pst, const uint8_t *p, size_t n)
834
+ {
835
+ v8_eval_impl(pst, p, n, true);
714
836
  }
715
837
 
716
838
  extern "C" void v8_heap_stats(State *pst)
@@ -775,6 +897,7 @@ extern "C" void v8_perform_microtask_checkpoint(State *pst)
775
897
  // Leave any termination active so the enclosing v8_call/v8_eval frame
776
898
  // surfaces OOM (set by v8_gc_callback) or watchdog termination to Ruby.
777
899
  State& st = *pst;
900
+ JavascriptCallScope call_scope(st.javascript_call_depth);
778
901
  v8::TryCatch try_catch(st.isolate);
779
902
  try_catch.SetVerbose(st.verbose_exceptions);
780
903
  v8::HandleScope handle_scope(st.isolate);
@@ -785,6 +908,8 @@ extern "C" void v8_perform_microtask_checkpoint(State *pst)
785
908
  extern "C" void v8_pump_message_loop(State *pst)
786
909
  {
787
910
  State& st = *pst;
911
+ bool nested = st.javascript_call_depth > 0;
912
+ JavascriptCallScope call_scope(st.javascript_call_depth);
788
913
  v8::TryCatch try_catch(st.isolate);
789
914
  try_catch.SetVerbose(st.verbose_exceptions);
790
915
  v8::HandleScope handle_scope(st.isolate);
@@ -800,7 +925,10 @@ extern "C" void v8_pump_message_loop(State *pst)
800
925
  if (try_catch.HasCaught()) goto fail;
801
926
  }
802
927
  fail:
803
- if (st.isolate->IsExecutionTerminating()) {
928
+ // A nested pump must leave termination active for the enclosing call/eval.
929
+ if (!nested &&
930
+ (st.terminate_requested.exchange(0) ||
931
+ st.isolate->IsExecutionTerminating())) {
804
932
  st.isolate->CancelTerminateExecution();
805
933
  st.err_reason = NO_ERROR;
806
934
  }
@@ -914,7 +1042,8 @@ extern "C" void v8_snapshot(State *pst, const uint8_t *p, size_t n)
914
1042
  }
915
1043
  cause = NO_ERROR;
916
1044
  fail:
917
- if (st.isolate->IsExecutionTerminating()) {
1045
+ if (st.terminate_requested.exchange(0) ||
1046
+ st.isolate->IsExecutionTerminating()) {
918
1047
  st.isolate->CancelTerminateExecution();
919
1048
  cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
920
1049
  st.err_reason = NO_ERROR;
@@ -984,7 +1113,8 @@ extern "C" void v8_warmup(State *pst, const uint8_t *p, size_t n)
984
1113
  }
985
1114
  cause = NO_ERROR;
986
1115
  fail:
987
- if (st.isolate->IsExecutionTerminating()) {
1116
+ if (st.terminate_requested.exchange(0) ||
1117
+ st.isolate->IsExecutionTerminating()) {
988
1118
  st.isolate->CancelTerminateExecution();
989
1119
  cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
990
1120
  st.err_reason = NO_ERROR;
@@ -1008,10 +1138,42 @@ extern "C" void v8_low_memory_notification(State *pst)
1008
1138
  pst->isolate->LowMemoryNotification();
1009
1139
  }
1010
1140
 
1011
- // called from ruby thread
1012
- extern "C" void v8_terminate_execution(State *pst)
1141
+ struct WakeupTask : public v8::Task
1142
+ {
1143
+ void Run() final {}
1144
+ };
1145
+
1146
+ // called from ruby or watchdog thread
1147
+ void request_termination(State *pst, unsigned request)
1013
1148
  {
1149
+ pst->terminate_requested.fetch_or(request);
1014
1150
  pst->isolate->TerminateExecution();
1151
+ // Wake await_promise if it may be blocked. A racing early return can leave
1152
+ // one harmless no-op task queued.
1153
+ if (pst->waiting_for_task.exchange(false)) {
1154
+ platform->GetForegroundTaskRunner(pst->isolate)
1155
+ ->PostTask(std::make_unique<WakeupTask>());
1156
+ }
1157
+ }
1158
+
1159
+ extern "C" void v8_terminate_execution(State *pst)
1160
+ {
1161
+ request_termination(pst, NON_WATCHDOG_TERMINATION);
1162
+ }
1163
+
1164
+ extern "C" void v8_terminate_watchdog(State *pst)
1165
+ {
1166
+ request_termination(pst, WATCHDOG_TERMINATION);
1167
+ }
1168
+
1169
+ extern "C" void v8_cancel_watchdog_termination(State *pst)
1170
+ {
1171
+ unsigned requests = pst->terminate_requested.fetch_and(~WATCHDOG_TERMINATION);
1172
+ if (requests == WATCHDOG_TERMINATION) {
1173
+ pst->isolate->CancelTerminateExecution();
1174
+ if (pst->terminate_requested.load())
1175
+ pst->isolate->TerminateExecution();
1176
+ }
1015
1177
  }
1016
1178
 
1017
1179
  // called from ruby thread
@@ -1019,6 +1181,7 @@ extern "C" void v8_cancel_terminate_execution(State *pst)
1019
1181
  {
1020
1182
  // TerminateExecution can race with V8 completing and queue a termination
1021
1183
  // for the next entry without IsExecutionTerminating() becoming true.
1184
+ pst->terminate_requested.store(0);
1022
1185
  pst->isolate->CancelTerminateExecution();
1023
1186
  }
1024
1187
 
@@ -39,7 +39,9 @@ struct State *v8_thread_init(struct Context *c, const uint8_t *snapshot_buf,
39
39
  int verbose_exceptions); // calls v8_thread_main
40
40
  void v8_attach(struct State *pst, const uint8_t *p, size_t n);
41
41
  void v8_call(struct State *pst, const uint8_t *p, size_t n);
42
+ void v8_call_await(struct State *pst, const uint8_t *p, size_t n);
42
43
  void v8_eval(struct State *pst, const uint8_t *p, size_t n);
44
+ void v8_eval_await(struct State *pst, const uint8_t *p, size_t n);
43
45
  void v8_heap_stats(struct State *pst);
44
46
  void v8_heap_snapshot(struct State *pst);
45
47
  void v8_perform_microtask_checkpoint(struct State *pst);
@@ -47,7 +49,9 @@ void v8_pump_message_loop(struct State *pst);
47
49
  void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
48
50
  void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
49
51
  void v8_low_memory_notification(struct State *pst);
50
- void v8_terminate_execution(struct State *pst); // called from ruby or watchdog thread
52
+ void v8_terminate_execution(struct State *pst); // called from ruby thread
53
+ void v8_terminate_watchdog(struct State *pst); // called from watchdog thread
54
+ void v8_cancel_watchdog_termination(struct State *pst); // called from v8 thread
51
55
  void v8_cancel_terminate_execution(struct State *pst); // called from ruby thread
52
56
  void v8_single_threaded_enter(struct State *pst, struct Context *c, void (*f)(struct Context *c));
53
57
  void v8_single_threaded_dispose(struct State *pst);
@@ -243,33 +243,38 @@ static void ser_num(Ser *s, double v)
243
243
  }
244
244
  }
245
245
 
246
- // ser_bigint: |n| is in bytes, not quadwords
247
- static void ser_bigint(Ser *s, const uint64_t *p, size_t n, int sign)
246
+ // ser_bigint: |p| points to |n| bytes, interpreted as little-endian
247
+ // 64-bit words. Keep the interface byte-oriented so callers don't need to
248
+ // expose a concrete word type.
249
+ static void ser_bigint(Ser *s, const void *p, size_t n, int sign)
248
250
  {
251
+ const uint8_t *bytes;
252
+
249
253
  if (*s->err)
250
254
  return;
251
255
  if (n % 8) {
252
256
  snprintf(s->err, sizeof(s->err), "bad bigint");
253
257
  return;
254
258
  }
259
+ bytes = p;
255
260
  w_byte(s, 'Z');
256
261
  // chop off high all-zero words
257
- n /= 8;
258
- while (n--)
259
- if (p[n])
260
- break;
261
- if (n == (size_t)-1) {
262
+ while (n > 0 && bytes[n-1] == 0)
263
+ n--;
264
+ if (n == 0) {
262
265
  w_byte(s, 0); // normalized zero
263
266
  } else {
264
- n = 8*n + 8;
267
+ n = (n + 7) & ~(size_t)7;
265
268
  w_varint(s, 2*n + (sign < 0));
266
- w(s, p, n);
269
+ w(s, bytes, n);
267
270
  }
268
271
  }
269
272
 
270
273
  static void ser_int(Ser *s, int64_t v)
271
274
  {
275
+ uint8_t bytes[8];
272
276
  uint64_t t;
277
+ size_t i;
273
278
  int sign;
274
279
 
275
280
  if (*s->err)
@@ -279,8 +284,10 @@ static void ser_int(Ser *s, int64_t v)
279
284
  if (v <= INT64_MAX/1024)
280
285
  return ser_num(s, v);
281
286
  t = v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v;
287
+ for (i = 0; i < sizeof(bytes); i++)
288
+ bytes[i] = t >> (8*i);
282
289
  sign = v < 0 ? -1 : 1;
283
- ser_bigint(s, &t, sizeof(t), sign);
290
+ ser_bigint(s, bytes, sizeof(bytes), sign);
284
291
  } else {
285
292
  w_byte(s, 'I');
286
293
  w_zigzag(s, v);