rusty_racer 0.2.5 → 0.2.7

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.
@@ -93,8 +93,8 @@ use watchdog::{
93
93
  // SAFETY of the manual Send: the !Send contents (and BoxValue's drop, which
94
94
  // calls rb_gc_unregister_address) only run under the GVL. Two conventions
95
95
  // keep that true — breaking either is silent UB, so don't:
96
- // - Arc<Core> lives ONLY in the four TypedData wrappers (Isolate, Context,
97
- // Module, Script). Never clone it into the V8/watchdog threads, or the
96
+ // - Arc<Core> lives ONLY in the five TypedData wrappers (Isolate, Context,
97
+ // Module, Script, Function). Never clone it into the V8/watchdog threads, or the
98
98
  // last drop could run off a Ruby thread.
99
99
  // - the wrappers must not set free_immediately: their dfree (and so Core's
100
100
  // drop) has to run outside the GC sweep, where Ruby APIs are forbidden.
@@ -293,6 +293,8 @@ enum VmError {
293
293
  },
294
294
  Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
295
295
  OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
296
+ // A handle whose realm was reset or disposed under it -> RustyRacer::DisposedError
297
+ Disposed(String),
296
298
  }
297
299
 
298
300
  // V8's near-heap-limit callback (registered on EVERY isolate). V8 calls this,
@@ -869,7 +871,8 @@ fn take_thrown<'s>(
869
871
 
870
872
  // ---------------------------------------------------------------------------
871
873
  // ES modules: V8's raw compile/instantiate/evaluate steps, with the embedder
872
- // owning the url->Module registry (MODULES) and the resolve policy.
874
+ // owning the url->Module registry and the resolve policy (ModuleReg is only the
875
+ // binding's id and url bookkeeping).
873
876
  // ---------------------------------------------------------------------------
874
877
  fn module_origin<'s>(scope: &v8::PinScope<'s, '_>, url: &str) -> v8::ScriptOrigin<'s> {
875
878
  let name = v8::String::new(scope, url).unwrap();
@@ -888,18 +891,47 @@ fn module_origin<'s>(scope: &v8::PinScope<'s, '_>, url: &str) -> v8::ScriptOrigi
888
891
  )
889
892
  }
890
893
 
891
- // Registry for the thin compile_module/instantiate API: each compiled module is
892
- // addressed by an id, with its url kept for the resolve round-trip and a
893
- // hash bucket to map a referrer Local<Module> back to its id.
894
+ // Registry for the thin compile_module/instantiate API.
894
895
  #[derive(Default)]
895
896
  struct ModuleReg {
896
- // id -> (module, url, owning context id). The context id is needed because
897
- // a module is bound to the v8::Context it was compiled in.
898
- by_id: HashMap<i32, (v8::Global<v8::Module>, String, i32)>,
899
- by_hash: HashMap<i32, Vec<(v8::Global<v8::Module>, i32)>>,
897
+ // id -> (module, owning context id): what a Module wrapper's ops address.
898
+ // The context id is needed because a module is bound to the v8::Context it
899
+ // was compiled in. Strong, and gone once the wrapper is disposed or collected
900
+ // (Core.released).
901
+ by_id: HashMap<i32, (v8::Global<v8::Module>, i32)>,
902
+ // identity hash -> (module, url): a Local<Module> V8 hands a callback
903
+ // (import.meta, a referrer being linked) back to the url it was compiled
904
+ // with. WEAK, so a module V8 still keeps — a dependency linked into a live
905
+ // graph, a function of a disposed realm's module another realm holds —
906
+ // answers whether its wrapper (or its realm) is still around or not, and
907
+ // one V8 collected is pruned instead of pinned (`add_url`).
908
+ urls: HashMap<i32, Vec<(v8::Weak<v8::Module>, String)>>,
909
+ // `add_url`'s amortization: entries added since the last prune, and the
910
+ // entries that prune left.
911
+ added_since_prune: usize,
912
+ kept_at_prune: usize,
900
913
  next_id: i32,
901
914
  }
902
915
 
916
+ impl ModuleReg {
917
+ // Index |module|'s url, first pruning the entries of collected modules once
918
+ // as many have been added since the last prune as it left — a scan
919
+ // amortized O(1) per compile, keeping the index within about twice the
920
+ // modules alive.
921
+ fn add_url(&mut self, hash: i32, module: v8::Weak<v8::Module>, url: String) {
922
+ if self.added_since_prune > self.kept_at_prune {
923
+ self.urls.retain(|_, bucket| {
924
+ bucket.retain(|(module, _)| !module.is_empty());
925
+ !bucket.is_empty()
926
+ });
927
+ self.kept_at_prune = self.urls.values().map(Vec::len).sum();
928
+ self.added_since_prune = 0;
929
+ }
930
+ self.added_since_prune += 1;
931
+ self.urls.entry(hash).or_default().push((module, url));
932
+ }
933
+ }
934
+
903
935
  // Classic compiled scripts: id -> (unbound script, owning context id). An
904
936
  // UnboundScript is context-independent, but we run it in the context it was
905
937
  // compiled in (and reset/dispose of that context drops it).
@@ -909,6 +941,39 @@ struct ScriptReg {
909
941
  next_id: i32,
910
942
  }
911
943
 
944
+ // Context#function's handles: id -> the function its path resolved to, the
945
+ // holder it was read off (its `this`), and the realm it was resolved in. The
946
+ // Globals live HERE, not in the Ruby wrapper, so a reset or disposed realm drops
947
+ // them (drop_context_artifacts) whatever wrappers Ruby still holds — a handle
948
+ // must not keep its realm's v8::Context alive — and so they are only ever
949
+ // dropped on the owner thread with the isolate entered, which a wrapper's GC
950
+ // free can't promise. A wrapper that is disposed or collected queues its id on
951
+ // Core.released instead, and the next outermost request boundary removes it
952
+ // here. Ids are never reused, so a stale id can only miss.
953
+ //
954
+ // A path may lead into ANOTHER realm (`frames[0].fn`): the entry belongs to the
955
+ // realm it was resolved in, so it holds that other realm's function until this
956
+ // one is reset or the handle is released — as any reference this realm keeps
957
+ // to it would.
958
+ #[derive(Default)]
959
+ struct FunctionReg {
960
+ by_id: HashMap<u64, FunctionEntry>,
961
+ next_id: u64,
962
+ }
963
+
964
+ struct FunctionEntry {
965
+ function: v8::Global<v8::Function>,
966
+ receiver: v8::Global<v8::Value>,
967
+ context_id: i32,
968
+ }
969
+
970
+ // A handle whose Ruby wrapper let go of it, queued on Core.released.
971
+ enum Released {
972
+ Function(u64),
973
+ Module(i32),
974
+ Script(i32),
975
+ }
976
+
912
977
  // The V8 thread's realm registry: the main context (id 0, swappable by reset),
913
978
  // the extra realms from create_context, and the host-namespace name to
914
979
  // re-install on fresh realms. Thread-local (like MODULES/SCRIPTS) so
@@ -993,6 +1058,7 @@ struct IsolateState {
993
1058
  realms: V8State,
994
1059
  modules: ModuleReg,
995
1060
  scripts: ScriptReg,
1061
+ functions: FunctionReg,
996
1062
  active_realms: Vec<i32>,
997
1063
  instantiating: bool,
998
1064
  watchdog_fired: bool,
@@ -1046,6 +1112,7 @@ impl IsolateState {
1046
1112
  },
1047
1113
  modules: ModuleReg::default(),
1048
1114
  scripts: ScriptReg::default(),
1115
+ functions: FunctionReg::default(),
1049
1116
  active_realms: Vec::new(),
1050
1117
  instantiating: false,
1051
1118
  watchdog_fired: false,
@@ -1204,23 +1271,19 @@ fn auto_drain(scope: &mut v8::PinScope<'_, '_>, outermost: bool) {
1204
1271
  }
1205
1272
  }
1206
1273
 
1207
- // Drop every module AND script compiled in `context_id` (its v8::Context is
1208
- // going away — on reset or dispose — so those handles are now dead).
1274
+ // Drop every module, script and function handle of `context_id` (its
1275
+ // v8::Context is going away — on reset or dispose — so those handles are now
1276
+ // dead, and a function's Globals would otherwise keep that context alive).
1209
1277
  fn drop_context_artifacts(state: &mut IsolateState, context_id: i32) {
1210
1278
  let m = &mut state.modules;
1211
- let dead: Vec<i32> = m
1212
- .by_id
1213
- .iter()
1214
- .filter(|(_, (_, _, cid))| *cid == context_id)
1215
- .map(|(id, _)| *id)
1216
- .collect();
1217
- for id in dead {
1218
- m.by_id.remove(&id);
1219
- for bucket in m.by_hash.values_mut() {
1220
- bucket.retain(|(_, mid)| *mid != id);
1221
- }
1222
- }
1279
+ // (Not its url index: weak, it pins nothing, and a module of this realm that
1280
+ // another realm still reaches keeps its import.meta.url.)
1281
+ m.by_id.retain(|_, (_, cid)| *cid != context_id);
1223
1282
  state.scripts.by_id.retain(|_, (_, cid)| *cid != context_id);
1283
+ state
1284
+ .functions
1285
+ .by_id
1286
+ .retain(|_, entry| entry.context_id != context_id);
1224
1287
  // A promise-reject recorder created in this context is unusable now.
1225
1288
  if state
1226
1289
  .realms
@@ -1244,24 +1307,21 @@ fn script_handle(
1244
1307
  .map(|(g, cid)| (g.clone(), *cid))
1245
1308
  }
1246
1309
 
1247
- // A registered module's url (the filename it was compiled with), by identity
1248
- // against MODULES — the reverse of the id lookup. Used to give a referrer its
1249
- // url for the resolve round-trip and to fill import.meta.url.
1310
+ // A compiled module's url (the filename it was compiled with), by identity in
1311
+ // the weak url index (ModuleReg.urls) — for as long as V8 keeps the module. Used
1312
+ // to give a referrer its url for the resolve round-trip and to fill
1313
+ // import.meta.url.
1250
1314
  fn module_url(scope: &mut v8::PinScope<'_, '_>, module: v8::Local<v8::Module>) -> Option<String> {
1251
1315
  let hash = module.get_identity_hash().get();
1252
- // Snapshot the hash bucket (cloned globals) so the scope is free for the
1253
- // Local comparison below: istate! borrows the scope and v8::Local::new
1254
- // needs it too, so they can't overlap in one expression.
1255
- let bucket = istate!(scope).modules.by_hash.get(&hash)?.clone();
1256
- let id = bucket
1257
- .iter()
1258
- .find(|(g, _)| v8::Local::new(&*scope, g) == module)
1259
- .map(|(_, id)| *id)?;
1260
- istate!(scope)
1316
+ // A shared borrow of the slot beside the shared one to_local takes.
1317
+ scope
1318
+ .get_slot::<IsolateState>()?
1261
1319
  .modules
1262
- .by_id
1263
- .get(&id)
1264
- .map(|(_, u, _)| u.clone())
1320
+ .urls
1321
+ .get(&hash)?
1322
+ .iter()
1323
+ .find(|(weak, _)| weak.to_local(scope) == Some(module))
1324
+ .map(|(_, url)| url.clone())
1265
1325
  }
1266
1326
 
1267
1327
  // V8 calls this the first time a module reads import.meta. Fills in
@@ -1385,7 +1445,7 @@ fn resolve_imported<'s>(
1385
1445
  .modules
1386
1446
  .by_id
1387
1447
  .get(&dep_id)
1388
- .map(|(g, _, cid)| (g.clone(), *cid));
1448
+ .map(|(g, cid)| (g.clone(), *cid));
1389
1449
  let g = match (here, found) {
1390
1450
  (Some(here), Some((g, cid))) if cid == here => g,
1391
1451
  (Some(_), Some(_)) => {
@@ -1462,8 +1522,8 @@ fn dynamic_import_cb<'s>(
1462
1522
  .modules
1463
1523
  .by_id
1464
1524
  .get(&id)
1465
- .filter(|(_, _, cid)| Some(*cid) == here)
1466
- .map(|(g, _, _)| g.clone());
1525
+ .filter(|(_, cid)| Some(*cid) == here)
1526
+ .map(|(g, _)| g.clone());
1467
1527
  match g {
1468
1528
  Some(g) => {
1469
1529
  let module = v8::Local::new(scope, &g);
@@ -1644,7 +1704,7 @@ fn module_handle(state: &IsolateState, module_id: i32) -> Option<(v8::Global<v8:
1644
1704
  .modules
1645
1705
  .by_id
1646
1706
  .get(&module_id)
1647
- .map(|(g, _, cid)| (g.clone(), *cid))
1707
+ .map(|(g, cid)| (g.clone(), *cid))
1648
1708
  }
1649
1709
 
1650
1710
  // ---------------------------------------------------------------------------
@@ -1710,6 +1770,14 @@ struct Core {
1710
1770
  // reach it through Core (via the slot's core_ptr) while a &Core method also
1711
1771
  // holds it. reset/dispose releases the slots and recycles them.
1712
1772
  procs: Mutex<ProcTable>,
1773
+ // The handles (Function, Module, Script) whose Ruby wrappers were disposed or
1774
+ // collected, for the next outermost request boundary to remove from their
1775
+ // registries (`forget_released_handles`). A wrapper's free may run on any
1776
+ // Ruby thread, outside any op, so all it can do is queue its id here.
1777
+ released: Mutex<Vec<Released>>,
1778
+ // Set with each push there, so the request boundary looks at the lock only
1779
+ // when there is something under it.
1780
+ handles_released: AtomicBool,
1713
1781
  // Default per-eval/call timeout (ms); 0 = none. eval(timeout_ms:)'s explicit
1714
1782
  // value overrides it. Guards against an in-V8 infinite loop without a watchdog.
1715
1783
  default_timeout_ms: u64,
@@ -1779,10 +1847,22 @@ struct Script {
1779
1847
  cache_rejected: bool,
1780
1848
  }
1781
1849
 
1782
- // The four wrappers that hold an Arc<Core> all mark the isolate's roots, because any
1850
+ // Context#function result: a JS function resolved once, with its `this`, kept in
1851
+ // FunctionReg by id — the realm it belongs to rides along so a call enters it
1852
+ // without a lookup.
1853
+ #[derive(TypedData)]
1854
+ #[magnus(class = "RustyRacer::Function", mark)]
1855
+ struct JsFunction {
1856
+ core: Arc<Core>,
1857
+ context_id: i32,
1858
+ function_id: u64,
1859
+ disposed: AtomicBool,
1860
+ }
1861
+
1862
+ // The five wrappers that hold an Arc<Core> all mark the isolate's roots, because any
1783
1863
  // one of them may be the last one reachable: Ruby code is free to keep only the
1784
1864
  // Context a `Isolate#context` handed it, or only a Module, and the host fns must
1785
- // stay callable through it. Marking the same array four times costs nothing (the GC
1865
+ // stay callable through it. Marking the same array five times costs nothing (the GC
1786
1866
  // stops at an already-marked object); missing it once would free a live proc.
1787
1867
  macro_rules! marks_core_roots {
1788
1868
  ($t:ty) => {
@@ -1797,6 +1877,7 @@ marks_core_roots!(Isolate);
1797
1877
  marks_core_roots!(Context);
1798
1878
  marks_core_roots!(JsModule);
1799
1879
  marks_core_roots!(Script);
1880
+ marks_core_roots!(JsFunction);
1800
1881
 
1801
1882
  // Set true once V8 is initialized; Platform.set_flags! refuses after that
1802
1883
  // (flags must be set before V8::initialize), like mini_racer's
@@ -2323,12 +2404,12 @@ fn install_host_namespace(
2323
2404
  ];
2324
2405
  for (member, function) in members {
2325
2406
  if let (Some(f), Some(k)) = (function, v8::String::new(scope, member)) {
2326
- ns.set(scope, k.into(), f.into());
2407
+ ns.create_data_property(scope, k.into(), f.into());
2327
2408
  }
2328
2409
  }
2329
2410
  if let Some(key) = v8::String::new(scope, name) {
2330
2411
  let global = context.global(scope);
2331
- global.set(scope, key.into(), ns.into());
2412
+ global.create_data_property(scope, key.into(), ns.into());
2332
2413
  }
2333
2414
  }
2334
2415
 
@@ -2428,7 +2509,7 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
2428
2509
  )
2429
2510
  {
2430
2511
  err = Some(match e {
2431
- VmError::Parse(m, _) | VmError::Runtime(m) => m,
2512
+ VmError::Parse(m, _) | VmError::Runtime(m) | VmError::Disposed(m) => m,
2432
2513
  VmError::JsError { message, .. } => message,
2433
2514
  VmError::Terminated => "snapshot code was terminated".to_string(),
2434
2515
  // Unreachable: the snapshot-creator is a separate isolate
@@ -2565,6 +2646,8 @@ impl Isolate {
2565
2646
  depth: std::sync::atomic::AtomicU32::new(0),
2566
2647
  roots: Opaque::from(roots),
2567
2648
  procs: Mutex::new(ProcTable::default()),
2649
+ released: Mutex::new(Vec::new()),
2650
+ handles_released: AtomicBool::new(false),
2568
2651
  default_timeout_ms: timeout_ms,
2569
2652
  memory_limit,
2570
2653
  watchdog,
@@ -2890,7 +2973,7 @@ impl Core {
2890
2973
  }
2891
2974
 
2892
2975
  // Show the GC everything this isolate holds. Called from EVERY wrapper's mark
2893
- // (Isolate, Context, Module, Script all keep an Arc<Core>, and any one of them
2976
+ // (Isolate, Context, Module, Script, Function all keep an Arc<Core>, and any one of them
2894
2977
  // may be the last reachable): a wrapper that forgot to would leave a live
2895
2978
  // isolate's host fns unmarked. `mark` PINS — an Array marks its own elements
2896
2979
  // MOVABLE, and the copies Rust takes out of it (call_proc, root_proc) would go
@@ -2975,18 +3058,47 @@ impl Core {
2975
3058
  "call requires a function name",
2976
3059
  ));
2977
3060
  };
2978
- let name = String::try_convert(*name)?;
2979
- let jsargs: Vec<JsVal> = call_args
2980
- .iter()
2981
- .map(|v| ruby_to_jsval(*v))
2982
- .collect::<Result<_, _>>()?;
2983
-
2984
3061
  let reply = self.run(
2985
3062
  ruby,
2986
3063
  Request::Call {
2987
3064
  context_id,
2988
- name,
2989
- args: jsargs,
3065
+ name: String::try_convert(*name)?,
3066
+ args: js_args(call_args)?,
3067
+ void,
3068
+ timeout_ms: self.default_timeout_ms,
3069
+ },
3070
+ )?;
3071
+ self.reply_value(ruby, reply)
3072
+ }
3073
+
3074
+ // Context#function: resolve |path| in the realm once; the handle's id.
3075
+ fn resolve_function(&self, ruby: &Ruby, context_id: i32, path: String) -> Result<u64, Error> {
3076
+ let reply = self.run(
3077
+ ruby,
3078
+ Request::ResolveFunction {
3079
+ context_id,
3080
+ path,
3081
+ timeout_ms: self.default_timeout_ms,
3082
+ },
3083
+ )?;
3084
+ u64::try_convert(self.reply_value(ruby, reply)?)
3085
+ }
3086
+
3087
+ // Function#call (and call_void): Context#call's invocation, of a resolved handle.
3088
+ fn call_function(
3089
+ &self,
3090
+ ruby: &Ruby,
3091
+ context_id: i32,
3092
+ function_id: u64,
3093
+ args: &[Value],
3094
+ void: bool,
3095
+ ) -> Result<Value, Error> {
3096
+ let reply = self.run(
3097
+ ruby,
3098
+ Request::CallFunction {
3099
+ context_id,
3100
+ function_id,
3101
+ args: js_args(args)?,
2990
3102
  void,
2991
3103
  timeout_ms: self.default_timeout_ms,
2992
3104
  },
@@ -2994,6 +3106,13 @@ impl Core {
2994
3106
  self.reply_value(ruby, reply)
2995
3107
  }
2996
3108
 
3109
+ // A wrapper let go of its handle (see `released`). Takes only this lock,
3110
+ // briefly, and allocates no Ruby object — it runs from a GC free.
3111
+ fn release(&self, handle: Released) {
3112
+ self.released.lock().unwrap().push(handle);
3113
+ self.handles_released.store(true, Ordering::Relaxed);
3114
+ }
3115
+
2997
3116
  fn drain_microtasks(&self, ruby: &Ruby) -> Result<Value, Error> {
2998
3117
  let reply = self.run(
2999
3118
  ruby,
@@ -3302,11 +3421,6 @@ impl Core {
3302
3421
  self.reply_value(ruby, reply)
3303
3422
  }
3304
3423
 
3305
- fn dispose_module(&self, ruby: &Ruby, module_id: i32) -> Result<(), Error> {
3306
- let reply = self.run(ruby, Request::DisposeModule { module_id })?;
3307
- self.reply_value(ruby, reply).map(|_| ())
3308
- }
3309
-
3310
3424
  // Classic script: compile, run, dispose.
3311
3425
  #[allow(clippy::too_many_arguments)]
3312
3426
  fn compile_script(
@@ -3352,11 +3466,6 @@ impl Core {
3352
3466
  self.reply_value(ruby, reply)
3353
3467
  }
3354
3468
 
3355
- fn dispose_script(&self, ruby: &Ruby, script_id: i32) -> Result<(), Error> {
3356
- let reply = self.run(ruby, Request::DisposeScript { script_id })?;
3357
- self.reply_value(ruby, reply).map(|_| ())
3358
- }
3359
-
3360
3469
  // Serialize a fresh bytecode cache from a compiled handle's current state
3361
3470
  // (Script#/Module#create_code_cache). None = V8 couldn't produce one (or the
3362
3471
  // realm is gone).
@@ -3427,6 +3536,7 @@ impl Core {
3427
3536
  st.realms = V8State::default();
3428
3537
  st.modules = ModuleReg::default();
3429
3538
  st.scripts = ScriptReg::default();
3539
+ st.functions = FunctionReg::default();
3430
3540
  st.instantiate_resolve = None;
3431
3541
  }
3432
3542
  // Unregister the near-heap-limit callback before disposal: dropping the box
@@ -3551,6 +3661,11 @@ fn pending_transfer_count() -> usize {
3551
3661
  transfer_registry().lock().unwrap().len()
3552
3662
  }
3553
3663
 
3664
+ // A call's Ruby arguments, marshalled for JS.
3665
+ fn js_args(args: &[Value]) -> Result<Vec<JsVal>, Error> {
3666
+ args.iter().map(|v| ruby_to_jsval(*v)).collect()
3667
+ }
3668
+
3554
3669
  // Thin magnus-method wrappers.
3555
3670
  // Isolate = the VM and its isolate-level operations; it hands out Contexts.
3556
3671
  impl Isolate {
@@ -3657,6 +3772,18 @@ impl Context {
3657
3772
  rb_self.check_live(ruby)?;
3658
3773
  rb_self.core.call(ruby, rb_self.id, args, true)
3659
3774
  }
3775
+ // function(path) -> Function: what `call(path, ...)` would call, resolved
3776
+ // now, once — its holder kept as its `this` — so each call skips the path walk.
3777
+ fn function(ruby: &Ruby, rb_self: &Self, path: String) -> Result<JsFunction, Error> {
3778
+ rb_self.check_live(ruby)?;
3779
+ let function_id = rb_self.core.resolve_function(ruby, rb_self.id, path)?;
3780
+ Ok(JsFunction {
3781
+ core: rb_self.core.clone(),
3782
+ context_id: rb_self.id,
3783
+ function_id,
3784
+ disposed: AtomicBool::new(false),
3785
+ })
3786
+ }
3660
3787
  fn attach(ruby: &Ruby, rb_self: &Self, name: String, proc: Proc) -> Result<Value, Error> {
3661
3788
  rb_self.check_live(ruby)?;
3662
3789
  rb_self.core.attach(ruby, rb_self.id, name, proc)
@@ -3885,12 +4012,12 @@ impl JsModule {
3885
4012
  let bytes = rb_self.core.module_code_cache(ruby, rb_self.module_id)?;
3886
4013
  Ok(code_cache_value(ruby, bytes.as_ref()))
3887
4014
  }
3888
- fn dispose(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
3889
- if rb_self.disposed.swap(true, Ordering::SeqCst) {
3890
- return Ok(());
4015
+ // Let go of the module now rather than when this wrapper is collected (see
4016
+ // JsFunction::dispose). Its url still answers while V8 keeps the module.
4017
+ fn dispose(&self) {
4018
+ if !self.disposed.swap(true, Ordering::SeqCst) {
4019
+ self.core.release(Released::Module(self.module_id));
3891
4020
  }
3892
- let _ = rb_self.core.dispose_module(ruby, rb_self.module_id);
3893
- Ok(())
3894
4021
  }
3895
4022
  fn disposed(&self) -> bool {
3896
4023
  self.disposed.load(Ordering::SeqCst)
@@ -3936,18 +4063,72 @@ impl Script {
3936
4063
  let bytes = rb_self.core.script_code_cache(ruby, rb_self.script_id)?;
3937
4064
  Ok(code_cache_value(ruby, bytes.as_ref()))
3938
4065
  }
3939
- fn dispose(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
3940
- if rb_self.disposed.swap(true, Ordering::SeqCst) {
3941
- return Ok(());
4066
+ // Let go of the script now rather than when this wrapper is collected (see
4067
+ // JsFunction::dispose).
4068
+ fn dispose(&self) {
4069
+ if !self.disposed.swap(true, Ordering::SeqCst) {
4070
+ self.core.release(Released::Script(self.script_id));
3942
4071
  }
3943
- let _ = rb_self.core.dispose_script(ruby, rb_self.script_id);
3944
- Ok(())
3945
4072
  }
3946
4073
  fn disposed(&self) -> bool {
3947
4074
  self.disposed.load(Ordering::SeqCst)
3948
4075
  }
3949
4076
  }
3950
4077
 
4078
+ impl JsFunction {
4079
+ fn check_live(&self, ruby: &Ruby) -> Result<(), Error> {
4080
+ // Its realm's reset or dispose is found by the call itself (the handle is
4081
+ // gone from FunctionReg): that is not something this wrapper can see.
4082
+ if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
4083
+ return Err(Error::new(
4084
+ err_class(ruby, "DisposedError"),
4085
+ "disposed function",
4086
+ ));
4087
+ }
4088
+ Ok(())
4089
+ }
4090
+ fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
4091
+ rb_self.check_live(ruby)?;
4092
+ rb_self
4093
+ .core
4094
+ .call_function(ruby, rb_self.context_id, rb_self.function_id, args, false)
4095
+ }
4096
+ fn call_void(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
4097
+ rb_self.check_live(ruby)?;
4098
+ rb_self
4099
+ .core
4100
+ .call_function(ruby, rb_self.context_id, rb_self.function_id, args, true)
4101
+ }
4102
+ // Let go of the function now rather than when this wrapper is collected. Any
4103
+ // thread, any time: the Globals go at the end of the isolate's next op.
4104
+ fn dispose(&self) {
4105
+ if !self.disposed.swap(true, Ordering::SeqCst) {
4106
+ self.core.release(Released::Function(self.function_id));
4107
+ }
4108
+ }
4109
+ fn disposed(&self) -> bool {
4110
+ self.disposed.load(Ordering::SeqCst) || self.core.is_disposed()
4111
+ }
4112
+ }
4113
+
4114
+ impl Drop for JsFunction {
4115
+ fn drop(&mut self) {
4116
+ self.dispose();
4117
+ }
4118
+ }
4119
+
4120
+ impl Drop for JsModule {
4121
+ fn drop(&mut self) {
4122
+ self.dispose();
4123
+ }
4124
+ }
4125
+
4126
+ impl Drop for Script {
4127
+ fn drop(&mut self) {
4128
+ self.dispose();
4129
+ }
4130
+ }
4131
+
3951
4132
  // A bytecode cache as a binary (ASCII-8BIT) Ruby String, or nil when there's
3952
4133
  // none — the shared return shape of #cached_data and #create_code_cache.
3953
4134
  fn code_cache_value(ruby: &Ruby, bytes: Option<&Vec<u8>>) -> Value {
@@ -4015,6 +4196,7 @@ fn vm_err(ruby: &Ruby, e: VmError) -> Error {
4015
4196
  err_class(ruby, "V8OutOfMemoryError"),
4016
4197
  "JavaScript exceeded the isolate memory_limit",
4017
4198
  ),
4199
+ VmError::Disposed(m) => Error::new(err_class(ruby, "DisposedError"), m),
4018
4200
  }
4019
4201
  }
4020
4202
 
@@ -4197,6 +4379,7 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
4197
4379
  context.define_method("_eval", method!(Context::eval, 4))?;
4198
4380
  context.define_method("call", method!(Context::call, -1))?;
4199
4381
  context.define_method("call_void", method!(Context::call_void, -1))?;
4382
+ context.define_method("function", method!(Context::function, 1))?;
4200
4383
  context.define_method("attach", method!(Context::attach, 2))?;
4201
4384
  context.define_method("attach_many", method!(Context::attach_many, 1))?;
4202
4385
  context.define_method("reset", method!(Context::reset, 0))?;
@@ -4217,6 +4400,13 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
4217
4400
  script.define_method("dispose", method!(Script::dispose, 0))?;
4218
4401
  script.define_method("disposed?", method!(Script::disposed, 0))?;
4219
4402
 
4403
+ // A resolved function: Context#function -> #call / #call_void.
4404
+ let function = module.define_class("Function", ruby.class_object())?;
4405
+ function.define_method("call", method!(JsFunction::call, -1))?;
4406
+ function.define_method("call_void", method!(JsFunction::call_void, -1))?;
4407
+ function.define_method("dispose", method!(JsFunction::dispose, 0))?;
4408
+ function.define_method("disposed?", method!(JsFunction::disposed, 0))?;
4409
+
4220
4410
  // V8 startup blob: Snapshot.new(code) -> Isolate.new(snapshot:).
4221
4411
  let snapshot = module.define_class("Snapshot", ruby.class_object())?;
4222
4412
  snapshot.define_singleton_method("new", function!(Snapshot::new, -1))?;