nosj 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/Cargo.lock +375 -0
- data/Cargo.toml +27 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +13 -0
- data/README.md +214 -0
- data/ext/nosj/Cargo.toml +25 -0
- data/ext/nosj/extconf.rb +6 -0
- data/ext/nosj/src/gen/errors.rs +61 -0
- data/ext/nosj/src/gen/hash_iter.rs +53 -0
- data/ext/nosj/src/gen/keys.rs +58 -0
- data/ext/nosj/src/gen/mod.rs +171 -0
- data/ext/nosj/src/gen/opts.rs +162 -0
- data/ext/nosj/src/gen/ruby.rs +100 -0
- data/ext/nosj/src/gen/walker.rs +638 -0
- data/ext/nosj/src/lib.rs +56 -0
- data/ext/nosj/src/parse.rs +211 -0
- data/ext/nosj/src/pointer.rs +190 -0
- data/ext/nosj/src/sink.rs +370 -0
- data/ext/nosj/src/state.rs +89 -0
- data/lib/nosj/json.rb +124 -0
- data/lib/nosj/multi_json.rb +51 -0
- data/lib/nosj/native.rb +11 -0
- data/lib/nosj/version.rb +6 -0
- data/lib/nosj.rb +193 -0
- data/sig/nosj.rbs +61 -0
- metadata +92 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
//! The nosj sinks: `RubyValueSink` builds Ruby VALUEs directly during
|
|
2
|
+
//! the parse (with interned-key caches and gem-compatible option
|
|
3
|
+
//! handling); `NullSink` powers `NOSJ.valid?` by discarding every
|
|
4
|
+
//! event. Raw VALUE construction helpers live here too.
|
|
5
|
+
|
|
6
|
+
use ahash::AHashMap;
|
|
7
|
+
|
|
8
|
+
use crate::state::VStackShadow;
|
|
9
|
+
|
|
10
|
+
/// Sanity ceiling for pending values (memory bomb guard), not a design limit.
|
|
11
|
+
const SINK_STACK_MAX: usize = 1 << 26;
|
|
12
|
+
|
|
13
|
+
/// The JSON gem's default nesting limit; matching it is part of drop-in
|
|
14
|
+
/// compatibility (gem raises NestingError past 100 levels).
|
|
15
|
+
pub(crate) const MAX_NESTING: usize = 100;
|
|
16
|
+
|
|
17
|
+
const KEY_CACHE_CAP: usize = 2048;
|
|
18
|
+
|
|
19
|
+
/// Why a sink stopped the drive; mapped onto the gem's exceptions in
|
|
20
|
+
/// [`crate::parse::finish_drive`].
|
|
21
|
+
pub(crate) enum SinkAbort {
|
|
22
|
+
Overflow,
|
|
23
|
+
BadBigint,
|
|
24
|
+
TooDeep,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// `RB_INT2FIX` ported from Ruby's public inline headers: fixnums are
|
|
28
|
+
/// `(i << 1) + 1` for `|i| <= LONG_MAX / 2`, and the range is defined by
|
|
29
|
+
/// the C `long`, which is 32-bit on Windows (LLP64): fixnums there hold
|
|
30
|
+
/// only 31 bits, and tagging anything wider crashes Ruby with
|
|
31
|
+
/// "Unnormalized Fixnum value". C extensions get the check inlined by the
|
|
32
|
+
/// header; going through the extern `rb_ll2inum` costs an FFI call per
|
|
33
|
+
/// integer. Non-fixable values still take the call.
|
|
34
|
+
// The widening is an identity on LP64 hosts (clippy flags it there) but
|
|
35
|
+
// required on Windows, where c_long is 32-bit.
|
|
36
|
+
#[allow(clippy::unnecessary_cast)]
|
|
37
|
+
#[inline(always)]
|
|
38
|
+
fn int_to_raw(i: i64) -> rb_sys::VALUE {
|
|
39
|
+
const FIXABLE_MIN: i64 = (std::os::raw::c_long::MIN / 2) as i64;
|
|
40
|
+
const FIXABLE_MAX: i64 = (std::os::raw::c_long::MAX / 2) as i64;
|
|
41
|
+
if (FIXABLE_MIN..=FIXABLE_MAX).contains(&i) {
|
|
42
|
+
((i as u64) << 1).wrapping_add(1) as rb_sys::VALUE
|
|
43
|
+
} else {
|
|
44
|
+
unsafe { rb_sys::rb_ll2inum(i) }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[inline(always)]
|
|
49
|
+
fn str_to_raw(s: &str) -> rb_sys::VALUE {
|
|
50
|
+
unsafe {
|
|
51
|
+
rb_sys::rb_utf8_str_new(
|
|
52
|
+
s.as_ptr() as *const std::os::raw::c_char,
|
|
53
|
+
s.len() as std::os::raw::c_long,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[inline(always)]
|
|
59
|
+
fn interned_str_raw(s: &str) -> rb_sys::VALUE {
|
|
60
|
+
unsafe {
|
|
61
|
+
rb_sys::rb_enc_interned_str(
|
|
62
|
+
s.as_ptr() as *const std::os::raw::c_char,
|
|
63
|
+
s.len() as std::os::raw::c_long,
|
|
64
|
+
rb_sys::rb_utf8_encoding(),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Interned-key cache with epoch eviction: when the cap is reached the whole
|
|
70
|
+
/// cache is cleared (hot keys repopulate immediately). Without this, one
|
|
71
|
+
/// document with many unique keys (citm's numeric id maps) permanently fills
|
|
72
|
+
/// the cache and every later document pays full interning per key, measured
|
|
73
|
+
/// as a 1.37x regression on twitter parsed after citm in the same process.
|
|
74
|
+
#[inline(always)]
|
|
75
|
+
fn intern_key_cached(
|
|
76
|
+
key: &str,
|
|
77
|
+
map: &mut AHashMap<Box<str>, rb_sys::VALUE>,
|
|
78
|
+
shadow: &mut VStackShadow,
|
|
79
|
+
) -> rb_sys::VALUE {
|
|
80
|
+
if key.is_empty() || key.len() > 64 {
|
|
81
|
+
return interned_str_raw(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if let Some(&v) = map.get(key) {
|
|
85
|
+
return v;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let raw = interned_str_raw(key);
|
|
89
|
+
if map.len() >= KEY_CACHE_CAP {
|
|
90
|
+
map.clear();
|
|
91
|
+
shadow.values.clear();
|
|
92
|
+
}
|
|
93
|
+
map.insert(Box::from(key), raw);
|
|
94
|
+
shadow.values.push(raw);
|
|
95
|
+
raw
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Symbol-mode key cache. Static symbols from `rb_intern3` are permanent, so
|
|
99
|
+
/// no GC shadow is needed; epoch-cleared at capacity like the string cache.
|
|
100
|
+
#[inline(always)]
|
|
101
|
+
fn intern_symbol_cached(key: &str, map: &mut AHashMap<Box<str>, rb_sys::VALUE>) -> rb_sys::VALUE {
|
|
102
|
+
#[inline(always)]
|
|
103
|
+
fn intern(key: &str) -> rb_sys::VALUE {
|
|
104
|
+
unsafe {
|
|
105
|
+
rb_sys::rb_id2sym(rb_sys::rb_intern3(
|
|
106
|
+
key.as_ptr() as *const std::os::raw::c_char,
|
|
107
|
+
key.len() as std::os::raw::c_long,
|
|
108
|
+
rb_sys::rb_utf8_encoding(),
|
|
109
|
+
))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if key.is_empty() || key.len() > 64 {
|
|
114
|
+
return intern(key);
|
|
115
|
+
}
|
|
116
|
+
if let Some(&v) = map.get(key) {
|
|
117
|
+
return v;
|
|
118
|
+
}
|
|
119
|
+
let raw = intern(key);
|
|
120
|
+
if map.len() >= KEY_CACHE_CAP {
|
|
121
|
+
map.clear();
|
|
122
|
+
}
|
|
123
|
+
map.insert(Box::from(key), raw);
|
|
124
|
+
raw
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// nosj::Sink building Ruby VALUEs on the heap value stack. Pending
|
|
128
|
+
/// VALUEs are kept alive by the pinned TypedData wrapper's precise dmark.
|
|
129
|
+
pub(crate) struct RubyValueSink<'a> {
|
|
130
|
+
pub(crate) stack: &'a mut Vec<rb_sys::VALUE>,
|
|
131
|
+
pub(crate) keys: &'a mut AHashMap<Box<str>, rb_sys::VALUE>,
|
|
132
|
+
pub(crate) key_shadow: &'a mut VStackShadow,
|
|
133
|
+
pub(crate) depth: usize,
|
|
134
|
+
/// JSON.parse-compatible options; defaults keep the fast paths.
|
|
135
|
+
pub(crate) symbolize: bool,
|
|
136
|
+
pub(crate) freeze: bool,
|
|
137
|
+
pub(crate) max_nesting: usize,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Tried and rejected (2026-07-10): a jiter-style cache of repeated VALUE
|
|
141
|
+
// strings (rb_str_dup of a canonical copy, gated to heap lengths 40..=200
|
|
142
|
+
// where the dup shares its buffer copy-on-write). Measured twitter parse
|
|
143
|
+
// 0.95x vs 0.90x without: Ruby must dup for mutability where Python only
|
|
144
|
+
// INCREFs, and the hash+insert overhead exceeds the CoW savings.
|
|
145
|
+
|
|
146
|
+
impl RubyValueSink<'_> {
|
|
147
|
+
#[inline(always)]
|
|
148
|
+
fn push_raw(&mut self, raw: rb_sys::VALUE) -> Result<(), SinkAbort> {
|
|
149
|
+
if self.stack.len() >= SINK_STACK_MAX {
|
|
150
|
+
return Err(SinkAbort::Overflow);
|
|
151
|
+
}
|
|
152
|
+
self.stack.push(raw);
|
|
153
|
+
Ok(())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[inline(always)]
|
|
157
|
+
fn enter_container(&mut self) -> Result<(), SinkAbort> {
|
|
158
|
+
self.depth += 1;
|
|
159
|
+
if self.depth > self.max_nesting {
|
|
160
|
+
return Err(SinkAbort::TooDeep);
|
|
161
|
+
}
|
|
162
|
+
Ok(())
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
impl nosj::Sink for RubyValueSink<'_> {
|
|
167
|
+
type Error = SinkAbort;
|
|
168
|
+
|
|
169
|
+
#[inline(always)]
|
|
170
|
+
fn null(&mut self) -> Result<(), SinkAbort> {
|
|
171
|
+
self.push_raw(rb_sys::special_consts::Qnil as rb_sys::VALUE)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[inline(always)]
|
|
175
|
+
fn boolean(&mut self, value: bool) -> Result<(), SinkAbort> {
|
|
176
|
+
self.push_raw(if value {
|
|
177
|
+
rb_sys::special_consts::Qtrue as rb_sys::VALUE
|
|
178
|
+
} else {
|
|
179
|
+
rb_sys::special_consts::Qfalse as rb_sys::VALUE
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[inline(always)]
|
|
184
|
+
fn int(&mut self, value: i64) -> Result<(), SinkAbort> {
|
|
185
|
+
self.push_raw(int_to_raw(value))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[inline(always)]
|
|
189
|
+
fn float(&mut self, value: f64) -> Result<(), SinkAbort> {
|
|
190
|
+
self.push_raw(unsafe { rb_sys::rb_float_new(value) })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[inline(always)]
|
|
194
|
+
fn big_int(&mut self, digits: &str) -> Result<(), SinkAbort> {
|
|
195
|
+
let c = std::ffi::CString::new(digits).map_err(|_| SinkAbort::BadBigint)?;
|
|
196
|
+
self.push_raw(unsafe { rb_sys::rb_cstr2inum(c.as_ptr(), 10) })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[inline(always)]
|
|
200
|
+
fn str(&mut self, value: &str) -> Result<(), SinkAbort> {
|
|
201
|
+
let raw = if self.freeze {
|
|
202
|
+
// Gem parity: freeze mode dedupes strings via the fstring table.
|
|
203
|
+
interned_str_raw(value)
|
|
204
|
+
} else {
|
|
205
|
+
str_to_raw(value)
|
|
206
|
+
};
|
|
207
|
+
self.push_raw(raw)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[inline(always)]
|
|
211
|
+
fn key(&mut self, key: &str) -> Result<(), SinkAbort> {
|
|
212
|
+
let raw = if self.symbolize {
|
|
213
|
+
intern_symbol_cached(key, self.keys)
|
|
214
|
+
} else {
|
|
215
|
+
intern_key_cached(key, self.keys, self.key_shadow)
|
|
216
|
+
};
|
|
217
|
+
self.push_raw(raw)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Lone-low-surrogate content: gem parity is a UTF-8-encoded Ruby string
|
|
221
|
+
/// carrying the raw WTF-8 bytes (broken coderange, like the gem's).
|
|
222
|
+
#[inline(always)]
|
|
223
|
+
fn str_bytes(&mut self, value: &[u8]) -> Result<(), SinkAbort> {
|
|
224
|
+
let raw = unsafe {
|
|
225
|
+
let s = rb_sys::rb_utf8_str_new(
|
|
226
|
+
value.as_ptr() as *const std::os::raw::c_char,
|
|
227
|
+
value.len() as std::os::raw::c_long,
|
|
228
|
+
);
|
|
229
|
+
if self.freeze {
|
|
230
|
+
rb_sys::rb_str_freeze(s)
|
|
231
|
+
} else {
|
|
232
|
+
s
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
self.push_raw(raw)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[inline(always)]
|
|
239
|
+
fn key_bytes(&mut self, key: &[u8]) -> Result<(), SinkAbort> {
|
|
240
|
+
// Interning is skipped: these keys are pathological, not hot.
|
|
241
|
+
let raw = unsafe {
|
|
242
|
+
rb_sys::rb_utf8_str_new(
|
|
243
|
+
key.as_ptr() as *const std::os::raw::c_char,
|
|
244
|
+
key.len() as std::os::raw::c_long,
|
|
245
|
+
)
|
|
246
|
+
};
|
|
247
|
+
let frozen = unsafe { rb_sys::rb_str_freeze(raw) };
|
|
248
|
+
self.push_raw(frozen)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[inline(always)]
|
|
252
|
+
fn begin_array(&mut self) -> Result<(), SinkAbort> {
|
|
253
|
+
self.enter_container()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[inline(always)]
|
|
257
|
+
fn begin_object(&mut self) -> Result<(), SinkAbort> {
|
|
258
|
+
self.enter_container()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[inline(always)]
|
|
262
|
+
fn mark(&self) -> usize {
|
|
263
|
+
self.stack.len()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Note: eager arrays (begin_array allocating + array_checkpoint spilling)
|
|
267
|
+
// were measured SLOWER here; rb_ary_new + rb_ary_cat growth per small
|
|
268
|
+
// array loses to one exact-size rb_ary_new_from_values, and the hoped-for
|
|
269
|
+
// GC-marking savings didn't materialize (sweep/free dominates, not
|
|
270
|
+
// pending-stack marking). The default no-op hooks stay available for
|
|
271
|
+
// sinks where the trade differs.
|
|
272
|
+
#[inline(always)]
|
|
273
|
+
fn end_array(&mut self, mark: usize, _len: usize) -> Result<(), SinkAbort> {
|
|
274
|
+
self.depth -= 1;
|
|
275
|
+
let n = self.stack.len() - mark;
|
|
276
|
+
let raw = unsafe {
|
|
277
|
+
let a = rb_sys::rb_ary_new_from_values(
|
|
278
|
+
n as std::os::raw::c_long,
|
|
279
|
+
self.stack.as_ptr().add(mark),
|
|
280
|
+
);
|
|
281
|
+
if self.freeze {
|
|
282
|
+
rb_sys::rb_obj_freeze(a);
|
|
283
|
+
}
|
|
284
|
+
a
|
|
285
|
+
};
|
|
286
|
+
self.stack.truncate(mark);
|
|
287
|
+
self.push_raw(raw)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#[inline(always)]
|
|
291
|
+
fn end_object(&mut self, mark: usize, pairs: usize) -> Result<(), SinkAbort> {
|
|
292
|
+
self.depth -= 1;
|
|
293
|
+
let n = self.stack.len() - mark;
|
|
294
|
+
let hash_raw = unsafe { rb_sys::rb_hash_new_capa(pairs as std::os::raw::c_long) };
|
|
295
|
+
unsafe {
|
|
296
|
+
rb_sys::rb_hash_bulk_insert(
|
|
297
|
+
n as std::os::raw::c_long,
|
|
298
|
+
self.stack.as_ptr().add(mark),
|
|
299
|
+
hash_raw,
|
|
300
|
+
);
|
|
301
|
+
if self.freeze {
|
|
302
|
+
rb_sys::rb_obj_freeze(hash_raw);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
self.stack.truncate(mark);
|
|
306
|
+
self.push_raw(hash_raw)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Validation-only sink: every event is a no-op except nesting-depth
|
|
311
|
+
/// tracking, so `NOSJ.valid?` runs the full parser (tokenizers,
|
|
312
|
+
/// string decode, number validation) without allocating a single VALUE.
|
|
313
|
+
pub(crate) struct NullSink {
|
|
314
|
+
pub(crate) depth: usize,
|
|
315
|
+
pub(crate) max_nesting: usize,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
impl nosj::Sink for NullSink {
|
|
319
|
+
type Error = SinkAbort;
|
|
320
|
+
|
|
321
|
+
fn null(&mut self) -> Result<(), SinkAbort> {
|
|
322
|
+
Ok(())
|
|
323
|
+
}
|
|
324
|
+
fn boolean(&mut self, _: bool) -> Result<(), SinkAbort> {
|
|
325
|
+
Ok(())
|
|
326
|
+
}
|
|
327
|
+
fn int(&mut self, _: i64) -> Result<(), SinkAbort> {
|
|
328
|
+
Ok(())
|
|
329
|
+
}
|
|
330
|
+
fn float(&mut self, _: f64) -> Result<(), SinkAbort> {
|
|
331
|
+
Ok(())
|
|
332
|
+
}
|
|
333
|
+
fn big_int(&mut self, _: &str) -> Result<(), SinkAbort> {
|
|
334
|
+
Ok(())
|
|
335
|
+
}
|
|
336
|
+
fn str(&mut self, _: &str) -> Result<(), SinkAbort> {
|
|
337
|
+
Ok(())
|
|
338
|
+
}
|
|
339
|
+
fn key(&mut self, _: &str) -> Result<(), SinkAbort> {
|
|
340
|
+
Ok(())
|
|
341
|
+
}
|
|
342
|
+
fn str_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
343
|
+
Ok(())
|
|
344
|
+
}
|
|
345
|
+
fn mark(&self) -> usize {
|
|
346
|
+
0
|
|
347
|
+
}
|
|
348
|
+
fn begin_array(&mut self) -> Result<(), SinkAbort> {
|
|
349
|
+
self.depth += 1;
|
|
350
|
+
if self.depth > self.max_nesting {
|
|
351
|
+
return Err(SinkAbort::TooDeep);
|
|
352
|
+
}
|
|
353
|
+
Ok(())
|
|
354
|
+
}
|
|
355
|
+
fn begin_object(&mut self) -> Result<(), SinkAbort> {
|
|
356
|
+
self.depth += 1;
|
|
357
|
+
if self.depth > self.max_nesting {
|
|
358
|
+
return Err(SinkAbort::TooDeep);
|
|
359
|
+
}
|
|
360
|
+
Ok(())
|
|
361
|
+
}
|
|
362
|
+
fn end_array(&mut self, _: usize, _: usize) -> Result<(), SinkAbort> {
|
|
363
|
+
self.depth -= 1;
|
|
364
|
+
Ok(())
|
|
365
|
+
}
|
|
366
|
+
fn end_object(&mut self, _: usize, _: usize) -> Result<(), SinkAbort> {
|
|
367
|
+
self.depth -= 1;
|
|
368
|
+
Ok(())
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//! Per-thread parser state and the GC-visible value-stack shadow.
|
|
2
|
+
//!
|
|
3
|
+
//! Pending VALUEs live on heap-allocated stacks that Ruby's GC can see
|
|
4
|
+
//! through a wrapped handle object whose magnus-driven mark walks the
|
|
5
|
+
//! live entries (the JSON gem's rvalue_stack design): precise marking
|
|
6
|
+
//! of only `values.len()` slots, growing on demand so huge flat arrays
|
|
7
|
+
//! (mesh.json) never overflow.
|
|
8
|
+
|
|
9
|
+
use ahash::AHashMap;
|
|
10
|
+
use magnus::typed_data::Obj;
|
|
11
|
+
use magnus::{DataTypeFunctions, TypedData};
|
|
12
|
+
use nosj::Buffers;
|
|
13
|
+
use std::cell::RefCell;
|
|
14
|
+
|
|
15
|
+
/// Everything a parse touches, allocated once per thread and reused:
|
|
16
|
+
/// nosj's scratch buffers, the interned-key caches, and the GC-marked
|
|
17
|
+
/// stacks.
|
|
18
|
+
pub(crate) struct PullState {
|
|
19
|
+
pub(crate) bufs: Buffers,
|
|
20
|
+
pub(crate) keys: AHashMap<Box<str>, rb_sys::VALUE>,
|
|
21
|
+
/// Separate cache for symbolize_names mode: symbol and string VALUEs
|
|
22
|
+
/// must never share a map.
|
|
23
|
+
pub(crate) sym_keys: AHashMap<Box<str>, rb_sys::VALUE>,
|
|
24
|
+
/// Leaked once per thread; kept alive + GC-marked via the wrapped
|
|
25
|
+
/// handle.
|
|
26
|
+
pub(crate) vstack: Option<&'static mut VStackShadow>,
|
|
27
|
+
/// Marked shadow holding the cached key VALUEs; keys are kept alive by
|
|
28
|
+
/// this (collectable on epoch clear), NOT by per-key eternal GC pins.
|
|
29
|
+
pub(crate) key_shadow: Option<&'static mut VStackShadow>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
thread_local! {
|
|
33
|
+
pub(crate) static PULL_STATE: RefCell<PullState> = RefCell::new(PullState {
|
|
34
|
+
bufs: Buffers::new(),
|
|
35
|
+
keys: AHashMap::with_capacity(256),
|
|
36
|
+
sym_keys: AHashMap::new(),
|
|
37
|
+
vstack: None,
|
|
38
|
+
key_shadow: None,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// GC-marked holder for pending VALUEs.
|
|
43
|
+
pub(crate) struct VStackShadow {
|
|
44
|
+
pub(crate) values: Vec<rb_sys::VALUE>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Ruby-side handle over a leaked shadow: magnus drives the GC mark
|
|
48
|
+
/// through [`DataTypeFunctions::mark`] (its trampoline, not ours),
|
|
49
|
+
/// pinning every pending VALUE with `rb_gc_mark` semantics. The class
|
|
50
|
+
/// is defined (and made a private constant) at init.
|
|
51
|
+
#[derive(TypedData)]
|
|
52
|
+
#[magnus(class = "NOSJ::ValueStackShadow", mark)]
|
|
53
|
+
pub(crate) struct ShadowHandle(*const VStackShadow);
|
|
54
|
+
|
|
55
|
+
// SAFETY: the pointee is leaked for the process lifetime and each
|
|
56
|
+
// handle stays with the thread that created it; the only cross-thread
|
|
57
|
+
// access is the GC mark read, which runs at safepoints while the
|
|
58
|
+
// owning thread is parked (the same contract the previous
|
|
59
|
+
// rb_data_type_t dmark relied on).
|
|
60
|
+
unsafe impl Send for ShadowHandle {}
|
|
61
|
+
|
|
62
|
+
impl DataTypeFunctions for ShadowHandle {
|
|
63
|
+
fn mark(&self, marker: &magnus::gc::Marker) {
|
|
64
|
+
use magnus::rb_sys::FromRawValue;
|
|
65
|
+
// SAFETY: the shadow is leaked for the process lifetime, and
|
|
66
|
+
// GC marks at safepoints while the owning thread is not
|
|
67
|
+
// mutating the stack (the same contract the previous
|
|
68
|
+
// hand-written dmark relied on). Entries are live VALUEs
|
|
69
|
+
// pushed by the sinks.
|
|
70
|
+
let shadow = unsafe { &*self.0 };
|
|
71
|
+
for &v in &shadow.values {
|
|
72
|
+
marker.mark(unsafe { magnus::Value::from_raw(v) });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Create (once per thread) a leaked, GC-marked VStackShadow.
|
|
78
|
+
pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>) {
|
|
79
|
+
if slot.is_none() {
|
|
80
|
+
let ruby = magnus::Ruby::get().expect("called on a Ruby thread");
|
|
81
|
+
let shadow: &'static mut VStackShadow = Box::leak(Box::new(VStackShadow {
|
|
82
|
+
values: Vec::with_capacity(1024),
|
|
83
|
+
}));
|
|
84
|
+
let ptr = std::ptr::from_mut::<VStackShadow>(shadow).cast_const();
|
|
85
|
+
let handle: Obj<ShadowHandle> = ruby.obj_wrap(ShadowHandle(ptr));
|
|
86
|
+
ruby.gc_register_mark_object(handle);
|
|
87
|
+
*slot = Some(shadow);
|
|
88
|
+
}
|
|
89
|
+
}
|
data/lib/nosj/json.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Drop-in acceleration for the JSON module:
|
|
4
|
+
#
|
|
5
|
+
# require "nosj/json"
|
|
6
|
+
#
|
|
7
|
+
# reroutes JSON.parse, JSON.generate, JSON.pretty_generate and JSON.dump
|
|
8
|
+
# through NOSJ whenever the requested options fall within NOSJ's
|
|
9
|
+
# supported set, and falls back to the original json implementation for
|
|
10
|
+
# everything else (create_additions, object_class/array_class,
|
|
11
|
+
# decimal_class, on_load procs, JSON::State instances, IO arguments).
|
|
12
|
+
# Entry points built on JSON.parse (JSON.load, JSON.parse!,
|
|
13
|
+
# JSON.load_file, JSON.unsafe_load) pick up the fast path automatically
|
|
14
|
+
# and keep their exact legacy behavior when they need unsupported options
|
|
15
|
+
# (JSON.load's create_additions default always takes the fallback).
|
|
16
|
+
#
|
|
17
|
+
# Exceptions from the fast path are re-raised as the JSON classes
|
|
18
|
+
# (JSON::ParserError, JSON::GeneratorError, JSON::NestingError), so
|
|
19
|
+
# existing rescue clauses keep working. Parse error MESSAGES are
|
|
20
|
+
# NOSJ's (byte offsets rather than the gem's phrasing).
|
|
21
|
+
#
|
|
22
|
+
# Not rerouted: obj.to_json (core extensions drive the gem's generator
|
|
23
|
+
# directly), and objects with a custom to_json inside a rerouted
|
|
24
|
+
# generate receive no State argument (documented NOSJ divergence).
|
|
25
|
+
|
|
26
|
+
require "json"
|
|
27
|
+
require "nosj"
|
|
28
|
+
|
|
29
|
+
module NOSJ
|
|
30
|
+
# Implementation detail of `require "nosj/json"`.
|
|
31
|
+
# @private
|
|
32
|
+
module JSONDropIn
|
|
33
|
+
PARSE_OPTS = %i[symbolize_names freeze max_nesting allow_nan
|
|
34
|
+
allow_trailing_comma].freeze
|
|
35
|
+
GENERATE_OPTS = %i[indent space space_before object_nl array_nl
|
|
36
|
+
max_nesting allow_nan ascii_only script_safe
|
|
37
|
+
escape_slash strict depth
|
|
38
|
+
buffer_initial_length].freeze
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# The fast path handles nil or a plain Hash whose every key NOSJ
|
|
43
|
+
# implements; anything else (JSON::State, exotic options, string
|
|
44
|
+
# keys) belongs to the original implementation.
|
|
45
|
+
def supported?(opts, allowed)
|
|
46
|
+
return true if opts.nil?
|
|
47
|
+
return false unless opts.instance_of?(Hash)
|
|
48
|
+
opts.each_key { |k| return false unless allowed.include?(k) }
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse(source, opts)
|
|
53
|
+
NOSJ.parse(source, opts)
|
|
54
|
+
rescue RuntimeError => e
|
|
55
|
+
raise ::JSON::ParserError, e.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def generate(obj, opts, pretty)
|
|
59
|
+
pretty ? NOSJ.pretty_generate(obj, opts) : NOSJ.generate(obj, opts)
|
|
60
|
+
rescue NOSJ::NestingError => e
|
|
61
|
+
raise ::JSON::NestingError, e.message
|
|
62
|
+
rescue NOSJ::GeneratorError => e
|
|
63
|
+
raise ::JSON::GeneratorError, e.message
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Reopened by `require "nosj/json"` to reroute the module functions
|
|
69
|
+
# through NOSJ; behavior is documented on the require and in the
|
|
70
|
+
# README, not here.
|
|
71
|
+
# @private
|
|
72
|
+
module JSON
|
|
73
|
+
class << self
|
|
74
|
+
unless method_defined?(:nosj_original_parse) || private_method_defined?(:nosj_original_parse)
|
|
75
|
+
alias_method :nosj_original_parse, :parse
|
|
76
|
+
alias_method :nosj_original_generate, :generate
|
|
77
|
+
alias_method :nosj_original_pretty_generate, :pretty_generate
|
|
78
|
+
alias_method :nosj_original_dump, :dump
|
|
79
|
+
|
|
80
|
+
def parse(source, opts = nil)
|
|
81
|
+
if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::PARSE_OPTS)
|
|
82
|
+
NOSJ::JSONDropIn.parse(source, opts)
|
|
83
|
+
else
|
|
84
|
+
nosj_original_parse(source, opts)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def generate(obj, opts = nil)
|
|
89
|
+
if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::GENERATE_OPTS)
|
|
90
|
+
NOSJ::JSONDropIn.generate(obj, opts, false)
|
|
91
|
+
else
|
|
92
|
+
nosj_original_generate(obj, opts)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def pretty_generate(obj, opts = nil)
|
|
97
|
+
if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::GENERATE_OPTS)
|
|
98
|
+
NOSJ::JSONDropIn.generate(obj, opts, true)
|
|
99
|
+
else
|
|
100
|
+
nosj_original_pretty_generate(obj, opts)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def dump(obj, an_io = nil, limit = nil, kwargs = nil)
|
|
105
|
+
# Fast path for the common shapes, dump(obj) and dump(obj, opts
|
|
106
|
+
# hash), mirroring the gem: dump defaults merged under the
|
|
107
|
+
# user's options, NestingError surfaced as ArgumentError. IO and
|
|
108
|
+
# limit arguments take the original implementation.
|
|
109
|
+
if limit.nil? && kwargs.nil? && (an_io.nil? || an_io.instance_of?(Hash))
|
|
110
|
+
opts = _dump_default_options
|
|
111
|
+
opts = opts.merge(an_io) if an_io
|
|
112
|
+
if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::GENERATE_OPTS)
|
|
113
|
+
begin
|
|
114
|
+
return NOSJ::JSONDropIn.generate(obj, opts, false)
|
|
115
|
+
rescue ::JSON::NestingError
|
|
116
|
+
raise ArgumentError, "exceed depth limit"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
nosj_original_dump(obj, an_io, limit, kwargs)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# MultiJson adapter:
|
|
4
|
+
#
|
|
5
|
+
# require "nosj/multi_json"
|
|
6
|
+
# MultiJson.use NOSJ::MultiJsonAdapter
|
|
7
|
+
#
|
|
8
|
+
# Anything speaking MultiJson (Faraday middleware and friends) then
|
|
9
|
+
# parses and generates through NOSJ.
|
|
10
|
+
|
|
11
|
+
require "json"
|
|
12
|
+
require "multi_json"
|
|
13
|
+
require "multi_json/adapter"
|
|
14
|
+
require "nosj"
|
|
15
|
+
|
|
16
|
+
module NOSJ
|
|
17
|
+
# MultiJson adapter routing +MultiJson.load+/+dump+ through nosj.
|
|
18
|
+
#
|
|
19
|
+
# multi_json 2.x renamed its namespace MultiJson -> MultiJSON; the
|
|
20
|
+
# adapter inherits from whichever this installation defines.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# require "nosj/multi_json"
|
|
24
|
+
# MultiJson.use NOSJ::MultiJsonAdapter
|
|
25
|
+
class MultiJsonAdapter < (defined?(::MultiJSON) ? ::MultiJSON::Adapter : ::MultiJson::Adapter)
|
|
26
|
+
# MultiJson wraps whatever the adapter's ParseError names.
|
|
27
|
+
ParseError = ::JSON::ParserError
|
|
28
|
+
|
|
29
|
+
SYMBOLIZE = {symbolize_names: true}.freeze
|
|
30
|
+
private_constant :SYMBOLIZE
|
|
31
|
+
|
|
32
|
+
# @param string [String] the JSON document
|
|
33
|
+
# @param options [Hash] multi_json load options; +symbolize_keys+
|
|
34
|
+
# arrives normalized as +symbolize_names+
|
|
35
|
+
# @return [Object] the parsed value tree
|
|
36
|
+
# @raise [JSON::ParserError] when the document is malformed
|
|
37
|
+
def load(string, options = {})
|
|
38
|
+
::NOSJ.parse(string, options[:symbolize_names] ? SYMBOLIZE : nil)
|
|
39
|
+
rescue RuntimeError => e
|
|
40
|
+
raise ParseError, e.message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param object [Object] the value tree to serialize
|
|
44
|
+
# @param options [Hash] multi_json dump options; +pretty+ selects
|
|
45
|
+
# pretty-printing
|
|
46
|
+
# @return [String] the JSON document
|
|
47
|
+
def dump(object, options = {})
|
|
48
|
+
options[:pretty] ? ::NOSJ.pretty_generate(object) : ::NOSJ.generate(object)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/nosj/native.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Precompiled platform gems ship one extension per Ruby minor under
|
|
4
|
+
# lib/nosj/<major.minor>/; the source gem compiles straight to
|
|
5
|
+
# lib/nosj/nosj.<dlext>.
|
|
6
|
+
begin
|
|
7
|
+
ruby_version = RUBY_VERSION[/\d+\.\d+/]
|
|
8
|
+
require_relative "#{ruby_version}/nosj"
|
|
9
|
+
rescue LoadError
|
|
10
|
+
require_relative "nosj"
|
|
11
|
+
end
|