rusty_racer 0.1.6 → 0.1.8
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.
- checksums.yaml +4 -4
- data/README.md +23 -15
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +93 -24
- data/ext/rusty_racer/src/marshal.rs +54 -6
- data/ext/rusty_racer/src/stack.rs +3 -0
- data/lib/rusty_racer/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af5578c6ac8ba4eb6d367097be2faef786fdc0cb077ac5c76c14f1ec50106d41
|
|
4
|
+
data.tar.gz: 5cc05af64db35a3202c3fd72d2b0809e621ee7fa59c3db4e913ea537d835c438
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab88c79367db640e4144c0844a2ffcc0168f353de01ad7c225902c756d350f7a48188d9a3c0890ec6da2a61dc83e6a2484e519ccb082bb171d4fa795f89a4e7f
|
|
7
|
+
data.tar.gz: da6ca243ceb13b6d353ce15c6b59e725d617c0712e146a13017f2bbb2d7715dad0705849c4ce36b1b941b0dcb1a2a1750390d557b80c3e7a196809a66a5d4efa
|
data/README.md
CHANGED
|
@@ -15,7 +15,10 @@ Embed [V8](https://v8.dev/) in Ruby, built on [rusty_v8](https://crates.io/crate
|
|
|
15
15
|
not just classic scripts.
|
|
16
16
|
- **Faithful value marshalling**: `BigInt`, `Date`, `Map`, `Set`, typed binary
|
|
17
17
|
(`Uint8Array`/`ArrayBuffer` ↔ binary `String`), and shared/cyclic object
|
|
18
|
-
graphs all round-trip — no lossy JSON hop.
|
|
18
|
+
graphs all round-trip — no lossy JSON hop. A JS `Map` surfaces in Ruby as a
|
|
19
|
+
`RustyRacer::JSMap` (a `Hash` subclass — it reads like a `Hash` but keeps its
|
|
20
|
+
non-string keys) and marshals back to a JS `Map`; a plain `Hash` still maps to a
|
|
21
|
+
JS object.
|
|
19
22
|
- **In-thread execution** — V8 runs on the calling Ruby thread, with no dedicated
|
|
20
23
|
V8 thread and no per-op thread hop; fast when you run many small ops.
|
|
21
24
|
- **Drop-in [ExecJS](#execjs) runtime** — any ExecJS consumer switches with no
|
|
@@ -58,7 +61,7 @@ ctx.eval("1 + 1") # => 2
|
|
|
58
61
|
ctx.eval("({a: 1, b: [true, 'x']})") # => {"a"=>1, "b"=>[true, "x"]}
|
|
59
62
|
|
|
60
63
|
# Call a JS function with marshalled args (BigInt/Date/Map/Set/shared refs
|
|
61
|
-
# all round-trip faithfully).
|
|
64
|
+
# all round-trip faithfully; a JS Map surfaces as a RustyRacer::JSMap).
|
|
62
65
|
ctx.eval("function add(a, b) { return a + b }")
|
|
63
66
|
ctx.call("add", 20, 22) # => 42
|
|
64
67
|
ctx.call_void("doSideEffect") # runs it; never marshals the return
|
|
@@ -154,19 +157,24 @@ Also available:
|
|
|
154
157
|
isolate's heap. All realms are mutually same-origin (with a host namespace,
|
|
155
158
|
`NS.contextGlobal(id)` reaches another realm's `globalThis`, like a same-origin
|
|
156
159
|
`iframe.contentWindow`), so this is **not** an isolation boundary.
|
|
157
|
-
- **Zero-copy buffer transfer between isolates** — with a host namespace,
|
|
158
|
-
`NS
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
- **Zero-copy buffer transfer/share between isolates** — with a host namespace, the
|
|
161
|
+
`NS` object exposes the engine primitive for implementing `postMessage`
|
|
162
|
+
transferables and `SharedArrayBuffer` sharing, both with no byte copy:
|
|
163
|
+
- **Transfer** (the transfer list): `NS.transferOut(arrayBufferOrView)` detaches
|
|
164
|
+
a buffer (its `byteLength` → 0) and returns an integer token; `NS.transferIn(token)`
|
|
165
|
+
rebuilds an `ArrayBuffer` over the **same** memory in another isolate.
|
|
166
|
+
- **Share** (a `SharedArrayBuffer`): `NS.shareOut(sharedArrayBuffer)` returns a
|
|
167
|
+
token *without* detaching — the source stays live; `NS.shareIn(token)` rebuilds
|
|
168
|
+
a `SharedArrayBuffer` over the same memory, so both isolates see each other's
|
|
169
|
+
writes and `Atomics` work across them (including across threads).
|
|
170
|
+
|
|
171
|
+
The backing store is heap-external and atomic-refcounted, so a token stays
|
|
172
|
+
importable even after the exporting isolate is disposed. `transferOut`/`shareOut`
|
|
173
|
+
return `0` for an unsuitable argument (so the caller can fall back to a copy);
|
|
174
|
+
`transferIn`/`shareIn` return `undefined` for an unknown or wrong-kind token;
|
|
175
|
+
`NS.transferDrop(token)` releases an exported buffer that is never imported.
|
|
176
|
+
`RustyRacer.pending_transfer_count` reports exported-but-not-yet-imported buffers
|
|
177
|
+
so dropped messages don't leak silently.
|
|
170
178
|
- **`Isolate#perform_microtask_checkpoint`** — manual microtask drain. The default
|
|
171
179
|
`microtasks: :auto` also drains at the end of each outermost eval/call/evaluate;
|
|
172
180
|
`microtasks: :explicit` leaves it fully manual. There is no event loop or timers
|
data/ext/rusty_racer/Cargo.toml
CHANGED
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -649,23 +649,33 @@ static LEAKED_ISOLATES: std::sync::atomic::AtomicUsize = std::sync::atomic::Atom
|
|
|
649
649
|
|
|
650
650
|
// ── zero-copy ArrayBuffer transfer registry ────────────────────────────────
|
|
651
651
|
//
|
|
652
|
-
// A SharedRef<BackingStore> parked here so a
|
|
653
|
-
//
|
|
652
|
+
// A SharedRef<BackingStore> parked here so a buffer can cross from one isolate to
|
|
653
|
+
// another with NO byte copy. A backing store is V8's own heap-external,
|
|
654
654
|
// atomic-refcounted allocation, designed to be co-owned by live ArrayBuffers "even
|
|
655
655
|
// across isolates" (rusty_v8 ArrayBuffer docs). The embedder (capybara-simulated's
|
|
656
|
-
// cross-isolate postMessage transport) exports a buffer from the source realm
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
656
|
+
// cross-isolate postMessage transport) exports a buffer from the source realm,
|
|
657
|
+
// ships the returned integer token through its normal message plumbing, and
|
|
658
|
+
// rebuilds a buffer over the SAME memory in the destination realm — replacing the
|
|
659
|
+
// prior copy-out/copy-in of the bytes. Two flavours share this registry:
|
|
660
|
+
// - TRANSFER (NS.transferOut/transferIn): an ArrayBuffer; the source is detached
|
|
661
|
+
// (moved). The recipient gets an ArrayBuffer.
|
|
662
|
+
// - SHARE (NS.shareOut/shareIn): a SharedArrayBuffer; the source stays live and
|
|
663
|
+
// both realms see each other's writes (Atomics work across them). The
|
|
664
|
+
// recipient gets a SharedArrayBuffer.
|
|
665
|
+
// `shared` records which, so the matching import rebuilds the right type and a
|
|
666
|
+
// crossed import (transferIn of a share token, or vice versa) is refused.
|
|
660
667
|
//
|
|
661
668
|
// SharedRef<BackingStore> is NOT auto-Send (BackingStore is Send but not Sync, so
|
|
662
|
-
// SharedPtrBase's `T: Sync` Send-bound is unmet). It rides in this
|
|
669
|
+
// SharedPtrBase's `T: Sync` Send-bound is unmet). It rides in this struct with a
|
|
663
670
|
// manual Send for the same reason as SendIso/IsoPtr: the registry only ever MOVES
|
|
664
671
|
// the handle between owner threads (insert on the exporter's thread, remove on the
|
|
665
672
|
// importer's) under the Mutex, never sharing &handle concurrently — and shared_ptr's
|
|
666
673
|
// refcount is atomic, so dropping it on either thread is sound. No Sync impl: the
|
|
667
674
|
// handle is never aliased across threads.
|
|
668
|
-
struct SendBackingStore
|
|
675
|
+
struct SendBackingStore {
|
|
676
|
+
store: v8::SharedRef<v8::BackingStore>,
|
|
677
|
+
shared: bool,
|
|
678
|
+
}
|
|
669
679
|
unsafe impl Send for SendBackingStore {}
|
|
670
680
|
|
|
671
681
|
// Exported-but-not-yet-imported backing stores, keyed by token. A GLOBAL (not
|
|
@@ -1363,10 +1373,9 @@ fn set_promise_reject_handler(
|
|
|
1363
1373
|
// loses access. A view transfers its whole underlying ArrayBuffer.
|
|
1364
1374
|
//
|
|
1365
1375
|
// Returns 0 (copy fallback) for: a non-buffer argument; a non-detachable buffer
|
|
1366
|
-
// (WebAssembly/asm.js memory); and a SharedArrayBuffer or SAB-backed view — a
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
// isolates would be a separate feature.
|
|
1376
|
+
// (WebAssembly/asm.js memory); and a SharedArrayBuffer or SAB-backed view — a SAB
|
|
1377
|
+
// is *shared*, not transferred (detaching it would be a category error), so it
|
|
1378
|
+
// belongs to NS.shareOut, not here.
|
|
1370
1379
|
fn transfer_out(
|
|
1371
1380
|
scope: &mut v8::PinScope<'_, '_>,
|
|
1372
1381
|
args: v8::FunctionCallbackArguments<'_>,
|
|
@@ -1404,12 +1413,36 @@ fn transfer_out(
|
|
|
1404
1413
|
transfer_registry()
|
|
1405
1414
|
.lock()
|
|
1406
1415
|
.unwrap()
|
|
1407
|
-
.insert(token, SendBackingStore
|
|
1416
|
+
.insert(token, SendBackingStore { store, shared: false });
|
|
1417
|
+
rv.set(v8::Number::new(scope, token as f64).into());
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// NS.shareOut(sharedArrayBuffer) -> a positive integer token, or 0 for a
|
|
1421
|
+
// non-SharedArrayBuffer argument (so the caller falls back to a copy). Unlike
|
|
1422
|
+
// transferOut this does NOT detach: a SAB is *shared*, so the source realm keeps
|
|
1423
|
+
// its live SAB and the recipient's shareIn'd SAB sees the same memory (Atomics
|
|
1424
|
+
// work across them). Takes a bare SAB; for a SAB-backed view the caller passes
|
|
1425
|
+
// view.buffer.
|
|
1426
|
+
fn share_out(
|
|
1427
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
1428
|
+
args: v8::FunctionCallbackArguments<'_>,
|
|
1429
|
+
mut rv: v8::ReturnValue<'_, v8::Value>,
|
|
1430
|
+
) {
|
|
1431
|
+
let Ok(sab) = v8::Local::<v8::SharedArrayBuffer>::try_from(args.get(0)) else {
|
|
1432
|
+
rv.set(v8::Number::new(scope, 0.0).into());
|
|
1433
|
+
return;
|
|
1434
|
+
};
|
|
1435
|
+
let store = sab.get_backing_store();
|
|
1436
|
+
let token = NEXT_TRANSFER_TOKEN.fetch_add(1, Ordering::Relaxed);
|
|
1437
|
+
transfer_registry()
|
|
1438
|
+
.lock()
|
|
1439
|
+
.unwrap()
|
|
1440
|
+
.insert(token, SendBackingStore { store, shared: true });
|
|
1408
1441
|
rv.set(v8::Number::new(scope, token as f64).into());
|
|
1409
1442
|
}
|
|
1410
1443
|
|
|
1411
1444
|
// Parse a transfer-token argument: a finite, integral JS number >= 1 (0 is the
|
|
1412
|
-
// "not transferable" sentinel
|
|
1445
|
+
// "not transferable" sentinel the *Out fns never issue, and tokens are f64-exact
|
|
1413
1446
|
// only up to 2^53). Rejects NaN / Infinity / fractional / negative / out-of-range
|
|
1414
1447
|
// values — which a bare `as u64` would truncate or saturate into a COLLISION with
|
|
1415
1448
|
// a live token, stealing an unrelated transfer — so a malformed token instead
|
|
@@ -1422,10 +1455,23 @@ fn transfer_token(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>)
|
|
|
1422
1455
|
.then_some(n as u64)
|
|
1423
1456
|
}
|
|
1424
1457
|
|
|
1458
|
+
// Consume a token whose kind matches |want_shared| (a transfer token for
|
|
1459
|
+
// transferIn, a share token for shareIn). Peeks the kind under the lock and only
|
|
1460
|
+
// removes on a match, so a crossed import (transferIn of a share token, or vice
|
|
1461
|
+
// versa) leaves the entry intact and reads as undefined instead of rebuilding the
|
|
1462
|
+
// wrong buffer type over the memory.
|
|
1463
|
+
fn take_store(token: u64, want_shared: bool) -> Option<v8::SharedRef<v8::BackingStore>> {
|
|
1464
|
+
let mut reg = transfer_registry().lock().unwrap();
|
|
1465
|
+
match reg.get(&token) {
|
|
1466
|
+
Some(e) if e.shared == want_shared => reg.remove(&token).map(|e| e.store),
|
|
1467
|
+
_ => None,
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1425
1471
|
// NS.transferIn(token) -> a fresh ArrayBuffer over the transferred backing store
|
|
1426
|
-
// (no byte copy), or undefined for an unknown token (already imported,
|
|
1427
|
-
// message was dropped). Consumes the token: the new
|
|
1428
|
-
// the registry's reference is released.
|
|
1472
|
+
// (no byte copy), or undefined for an unknown/crossed token (already imported, the
|
|
1473
|
+
// message was dropped, or it was a share token). Consumes the token: the new
|
|
1474
|
+
// buffer co-owns the store, so the registry's reference is released.
|
|
1429
1475
|
fn transfer_in(
|
|
1430
1476
|
scope: &mut v8::PinScope<'_, '_>,
|
|
1431
1477
|
args: v8::FunctionCallbackArguments<'_>,
|
|
@@ -1434,16 +1480,30 @@ fn transfer_in(
|
|
|
1434
1480
|
let Some(token) = transfer_token(scope, args.get(0)) else {
|
|
1435
1481
|
return;
|
|
1436
1482
|
};
|
|
1437
|
-
let
|
|
1438
|
-
if let Some(SendBackingStore(store)) = entry {
|
|
1483
|
+
if let Some(store) = take_store(token, false) {
|
|
1439
1484
|
rv.set(v8::ArrayBuffer::with_backing_store(scope, &store).into());
|
|
1440
1485
|
}
|
|
1441
1486
|
}
|
|
1442
1487
|
|
|
1443
|
-
// NS.
|
|
1444
|
-
//
|
|
1445
|
-
|
|
1446
|
-
|
|
1488
|
+
// NS.shareIn(token) -> a fresh SharedArrayBuffer over the shared backing store
|
|
1489
|
+
// (same memory as the source's SAB), or undefined for an unknown/crossed token.
|
|
1490
|
+
fn share_in(
|
|
1491
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
1492
|
+
args: v8::FunctionCallbackArguments<'_>,
|
|
1493
|
+
mut rv: v8::ReturnValue<'_, v8::Value>,
|
|
1494
|
+
) {
|
|
1495
|
+
let Some(token) = transfer_token(scope, args.get(0)) else {
|
|
1496
|
+
return;
|
|
1497
|
+
};
|
|
1498
|
+
if let Some(store) = take_store(token, true) {
|
|
1499
|
+
rv.set(v8::SharedArrayBuffer::with_backing_store(scope, &store).into());
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// NS.transferDrop(token): release a parked backing store (transfer OR share)
|
|
1504
|
+
// WITHOUT importing it — for when the embedder discards a message whose buffer was
|
|
1505
|
+
// exported. A no-op for an unknown token. Without this, an exported-but-never-
|
|
1506
|
+
// imported buffer would pin its memory until process exit.
|
|
1447
1507
|
fn transfer_drop(
|
|
1448
1508
|
scope: &mut v8::PinScope<'_, '_>,
|
|
1449
1509
|
args: v8::FunctionCallbackArguments<'_>,
|
|
@@ -1570,7 +1630,7 @@ fn install_host_namespace(
|
|
|
1570
1630
|
let context = v8::Local::new(scope, ctx);
|
|
1571
1631
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
1572
1632
|
let ns = v8::Object::new(scope);
|
|
1573
|
-
let members: [(&str, Option<v8::Local<v8::Function>>);
|
|
1633
|
+
let members: [(&str, Option<v8::Local<v8::Function>>); 9] = [
|
|
1574
1634
|
("drainMicrotasks", v8::Function::new(scope, drain_microtasks)),
|
|
1575
1635
|
("contextGlobal", v8::Function::new(scope, context_global)),
|
|
1576
1636
|
("contextOf", v8::Function::new(scope, context_of)),
|
|
@@ -1580,6 +1640,8 @@ fn install_host_namespace(
|
|
|
1580
1640
|
),
|
|
1581
1641
|
("transferOut", v8::Function::new(scope, transfer_out)),
|
|
1582
1642
|
("transferIn", v8::Function::new(scope, transfer_in)),
|
|
1643
|
+
("shareOut", v8::Function::new(scope, share_out)),
|
|
1644
|
+
("shareIn", v8::Function::new(scope, share_in)),
|
|
1583
1645
|
("transferDrop", v8::Function::new(scope, transfer_drop)),
|
|
1584
1646
|
];
|
|
1585
1647
|
for (member, function) in members {
|
|
@@ -2957,6 +3019,13 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
2957
3019
|
jsmodule.define_method("dispose", method!(JsModule::dispose, 0))?;
|
|
2958
3020
|
jsmodule.define_method("disposed?", method!(JsModule::disposed, 0))?;
|
|
2959
3021
|
|
|
3022
|
+
// A JS Map marshals to this Hash subclass (not a plain Hash) so the reverse
|
|
3023
|
+
// direction can tell it apart from a JS Object — also a Hash — and rebuild a
|
|
3024
|
+
// JS Map instead of a plain object. It behaves exactly like a Hash (so
|
|
3025
|
+
// `assert_kind_of Hash` / `h[k]` still work); users may also construct one to
|
|
3026
|
+
// hand a JS Map to JS. See marshal.rs.
|
|
3027
|
+
module.define_class("JSMap", ruby.class_object().const_get::<_, magnus::RClass>("Hash")?)?;
|
|
3028
|
+
|
|
2960
3029
|
let platform = module.define_module("Platform")?;
|
|
2961
3030
|
platform.define_singleton_method("set_flags!", function!(platform_set_flags, -1))?;
|
|
2962
3031
|
|
|
@@ -48,8 +48,11 @@ pub(crate) enum JsVal {
|
|
|
48
48
|
Array { id: u32, items: Vec<JsVal> },
|
|
49
49
|
// JS object / Ruby Hash with string keys. Insertion order preserved.
|
|
50
50
|
Obj { id: u32, entries: Vec<(String, JsVal)> },
|
|
51
|
-
// JS Map <-> Ruby Hash. Keys are arbitrary
|
|
52
|
-
// this is distinct from Obj. Insertion order
|
|
51
|
+
// JS Map <-> Ruby RustyRacer::JSMap (a Hash subclass). Keys are arbitrary
|
|
52
|
+
// values (not just strings), so this is distinct from Obj. Insertion order
|
|
53
|
+
// preserved. The JSMap subclass is what lets a Map round-trip: a plain Ruby
|
|
54
|
+
// Hash marshals to Obj (a JS Object), a JSMap to Map — the return leg tells
|
|
55
|
+
// them apart by class (Ruby has no native Map type).
|
|
53
56
|
Map { id: u32, pairs: Vec<(JsVal, JsVal)> },
|
|
54
57
|
// JS Set <-> Ruby Set (stdlib).
|
|
55
58
|
Set { id: u32, items: Vec<JsVal> },
|
|
@@ -431,6 +434,15 @@ pub(crate) fn jsval_to_ruby(ruby: &Ruby, val: &JsVal) -> Result<Value, Error> {
|
|
|
431
434
|
// reference. This invariant is load-bearing: do NOT refactor an arm to stash a
|
|
432
435
|
// value in `built` without keeping it rooted by a live local until it's grafted.
|
|
433
436
|
|
|
437
|
+
// The RustyRacer::JSMap class (a Hash subclass), defined in init. A JS Map
|
|
438
|
+
// marshals to an instance of it so the reverse direction can distinguish it from
|
|
439
|
+
// a plain Hash (a JS Object) and rebuild a JS Map. Errors only if init didn't run.
|
|
440
|
+
fn js_map_class(ruby: &Ruby) -> Result<magnus::RClass, Error> {
|
|
441
|
+
ruby.class_object()
|
|
442
|
+
.const_get::<_, magnus::RModule>("RustyRacer")?
|
|
443
|
+
.const_get::<_, magnus::RClass>("JSMap")
|
|
444
|
+
}
|
|
445
|
+
|
|
434
446
|
fn jsval_to_ruby_d(
|
|
435
447
|
ruby: &Ruby,
|
|
436
448
|
val: &JsVal,
|
|
@@ -494,16 +506,27 @@ fn jsval_to_ruby_d(
|
|
|
494
506
|
}
|
|
495
507
|
h.as_value()
|
|
496
508
|
}
|
|
497
|
-
// JS Map ->
|
|
509
|
+
// JS Map -> RustyRacer::JSMap, a Hash subclass: arbitrary marshalled keys
|
|
510
|
+
// (not just strings) AND distinguishable from a JS Object (a plain Hash),
|
|
511
|
+
// so ruby_to_jsval can rebuild a JS Map rather than a plain object. Build
|
|
512
|
+
// empty then aset so a cyclic Map (a value referring back to the Map)
|
|
513
|
+
// resolves through the Ref table.
|
|
514
|
+
//
|
|
515
|
+
// LIMITATION: keys collapse by Ruby Hash equality (eql?/hash), but a JS
|
|
516
|
+
// Map keys by identity (SameValueZero). Primitive keys (string/number/
|
|
517
|
+
// bool/bigint) round-trip exactly; two distinct-but-structurally-equal
|
|
518
|
+
// OBJECT keys (e.g. `{}` and `{}`) become ONE entry — inherent to a Hash
|
|
519
|
+
// representation, and object identity can't survive copy-marshalling anyway.
|
|
498
520
|
JsVal::Map { id, pairs } => {
|
|
499
|
-
let
|
|
500
|
-
built.insert(*id,
|
|
521
|
+
let map_obj: Value = js_map_class(ruby)?.funcall("new", ())?;
|
|
522
|
+
built.insert(*id, map_obj);
|
|
523
|
+
let h = RHash::from_value(map_obj).expect("JSMap is a Hash");
|
|
501
524
|
for (k, v) in pairs {
|
|
502
525
|
let kk = jsval_to_ruby_d(ruby, k, built)?;
|
|
503
526
|
let vv = jsval_to_ruby_d(ruby, v, built)?;
|
|
504
527
|
let _ = h.aset(kk, vv);
|
|
505
528
|
}
|
|
506
|
-
|
|
529
|
+
map_obj
|
|
507
530
|
}
|
|
508
531
|
// JS Set -> Ruby Set (stdlib); build empty then add so a cyclic Set
|
|
509
532
|
// (a Set containing itself) resolves through the Ref table.
|
|
@@ -726,6 +749,31 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
726
749
|
}
|
|
727
750
|
return Ok(JsVal::Array { id, items });
|
|
728
751
|
}
|
|
752
|
+
// RustyRacer::JSMap (a Hash subclass) -> JS Map, preserving arbitrary keys as
|
|
753
|
+
// real JS values (NOT string-coerced like a plain Hash -> JS Object). Must come
|
|
754
|
+
// BEFORE the Hash branch, since a JSMap IS-A Hash. This is what makes a JS Map
|
|
755
|
+
// round-trip (JS Map -> JSMap -> JS Map) instead of degrading to an object.
|
|
756
|
+
if let Ok(js_map) = js_map_class(&ruby) {
|
|
757
|
+
if val.is_kind_of(js_map) {
|
|
758
|
+
let id = match rb_container_id(seen, val, depth)? {
|
|
759
|
+
RbId::New(id) => id,
|
|
760
|
+
RbId::Reuse(jv) => return Ok(jv),
|
|
761
|
+
};
|
|
762
|
+
let hash = RHash::from_value(val).expect("JSMap is a Hash");
|
|
763
|
+
let pairs = RefCell::new(Vec::new());
|
|
764
|
+
hash.foreach(|k: Value, v: Value| {
|
|
765
|
+
pairs.borrow_mut().push((
|
|
766
|
+
ruby_to_jsval_d(k, seen, depth + 1)?,
|
|
767
|
+
ruby_to_jsval_d(v, seen, depth + 1)?,
|
|
768
|
+
));
|
|
769
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
770
|
+
})?;
|
|
771
|
+
return Ok(JsVal::Map {
|
|
772
|
+
id,
|
|
773
|
+
pairs: pairs.into_inner(),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
729
777
|
if let Ok(hash) = RHash::try_convert(val) {
|
|
730
778
|
let id = match rb_container_id(seen, val, depth)? {
|
|
731
779
|
RbId::New(id) => id,
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// STACK_DEBUG (set at init); everything else is private to this module.
|
|
7
7
|
|
|
8
8
|
use std::ffi::c_void;
|
|
9
|
+
// Only native_stack_bounds (linux) needs it; gated so non-linux builds (macOS)
|
|
10
|
+
// don't warn it unused.
|
|
11
|
+
#[cfg(target_os = "linux")]
|
|
9
12
|
use std::ptr::null_mut;
|
|
10
13
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
11
14
|
|
data/lib/rusty_racer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rusty_racer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|
|
@@ -54,7 +54,7 @@ metadata:
|
|
|
54
54
|
source_code_uri: https://github.com/ursm/rusty_racer
|
|
55
55
|
bug_tracker_uri: https://github.com/ursm/rusty_racer/issues
|
|
56
56
|
rubygems_mfa_required: 'true'
|
|
57
|
-
post_install_message:
|
|
57
|
+
post_install_message:
|
|
58
58
|
rdoc_options: []
|
|
59
59
|
require_paths:
|
|
60
60
|
- lib
|
|
@@ -70,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
70
|
version: '0'
|
|
71
71
|
requirements: []
|
|
72
72
|
rubygems_version: 3.5.22
|
|
73
|
-
signing_key:
|
|
73
|
+
signing_key:
|
|
74
74
|
specification_version: 4
|
|
75
75
|
summary: Embed V8 in Ruby via rusty_v8 + Magnus (rb-sys)
|
|
76
76
|
test_files: []
|