nosj 0.3.2 → 0.4.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.
data/ext/nosj/src/sink.rs CHANGED
@@ -35,24 +35,21 @@ pub(crate) enum SinkAbort {
35
35
  NonFiniteFloat(&'static str),
36
36
  }
37
37
 
38
- /// `RB_INT2FIX` ported from Ruby's public inline headers: fixnums are
39
- /// `(i << 1) + 1` for `|i| <= LONG_MAX / 2`, and the range is defined by
40
- /// the C `long`, which is 32-bit on Windows (LLP64): fixnums there hold
41
- /// only 31 bits, and tagging anything wider crashes Ruby with
42
- /// "Unnormalized Fixnum value". C extensions get the check inlined by the
43
- /// header; going through the extern `rb_ll2inum` costs an FFI call per
44
- /// integer. Non-fixable values still take the call.
45
- // The widening is an identity on LP64 hosts (clippy flags it there) but
46
- // required on Windows, where c_long is 32-bit.
47
- #[allow(clippy::unnecessary_cast)]
38
+ /// Integer VALUE for `i` via rb-sys's inline `LONG2NUM` (the header
39
+ /// macro: a Fixnum tagged inline when it fits, else a Bignum), which
40
+ /// saves the FFI call per integer that `rb_ll2inum` costs. The fixable
41
+ /// range is defined by the C `long`, which is 32-bit on Windows (LLP64):
42
+ /// fixnums there hold only 31 bits, and tagging anything wider crashes
43
+ /// Ruby with "Unnormalized Fixnum value"; values beyond `long` take
44
+ /// `rb_ll2inum`.
45
+ // The conversion is an identity on LP64 hosts (clippy flags it there)
46
+ // but narrows on Windows, where c_long is 32-bit.
47
+ #[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
48
48
  #[inline(always)]
49
49
  fn int_to_raw(i: i64) -> rb_sys::VALUE {
50
- const FIXABLE_MIN: i64 = (std::os::raw::c_long::MIN / 2) as i64;
51
- const FIXABLE_MAX: i64 = (std::os::raw::c_long::MAX / 2) as i64;
52
- if (FIXABLE_MIN..=FIXABLE_MAX).contains(&i) {
53
- ((i as u64) << 1).wrapping_add(1) as rb_sys::VALUE
54
- } else {
55
- unsafe { rb_sys::rb_ll2inum(i) }
50
+ match std::os::raw::c_long::try_from(i) {
51
+ Ok(l) => rb_sys::macros::LONG2NUM(l),
52
+ Err(_) => unsafe { rb_sys::rb_ll2inum(i) },
56
53
  }
57
54
  }
58
55
 
@@ -10,18 +10,36 @@ use ahash::AHashMap;
10
10
  use magnus::typed_data::Obj;
11
11
  use magnus::{DataTypeFunctions, TypedData};
12
12
  use nosj::Buffers;
13
- use std::cell::RefCell;
13
+ use std::cell::Cell;
14
+ use std::thread::LocalKey;
14
15
 
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.
16
+ /// Run `f` on a pooled thread-local value, taken OUT of its cell for the
17
+ /// call and stored back afterwards. Pooled state is never borrowed
18
+ /// across a call that can reach Ruby: any allocation can raise
19
+ /// NoMemoryError, whose longjmp skips these frames, and a RefCell borrow
20
+ /// held across it would stay borrowed for good (under panic=abort, the
21
+ /// next borrow kills the process). A value lost that way is only leaked;
22
+ /// the next call, like a nested one finding the cell empty, starts from
23
+ /// `T::default()`. The outermost call's value is the one stored back.
24
+ pub(crate) fn with_taken<T: Default, R>(
25
+ key: &'static LocalKey<Cell<T>>,
26
+ f: impl FnOnce(&mut T) -> R,
27
+ ) -> R {
28
+ let mut value = key.take();
29
+ let result = f(&mut value);
30
+ key.set(value);
31
+ result
32
+ }
33
+
34
+ /// Everything a parse touches, reused across calls: nosj's scratch
35
+ /// buffers, the interned-key caches, and the GC-marked stacks.
18
36
  pub(crate) struct PullState {
19
37
  pub(crate) bufs: Buffers,
20
38
  pub(crate) keys: AHashMap<Box<str>, rb_sys::VALUE>,
21
39
  /// Separate cache for symbolize_names mode: symbol and string VALUEs
22
40
  /// must never share a map.
23
41
  pub(crate) sym_keys: AHashMap<Box<str>, rb_sys::VALUE>,
24
- /// Leaked once per thread; kept alive + GC-marked via the wrapped
42
+ /// Leaked once per state; kept alive + GC-marked via the wrapped
25
43
  /// handle.
26
44
  pub(crate) vstack: Option<&'static mut VStackShadow>,
27
45
  /// Marked shadow holding the cached key VALUEs; keys are kept alive by
@@ -29,14 +47,36 @@ pub(crate) struct PullState {
29
47
  pub(crate) key_shadow: Option<&'static mut VStackShadow>,
30
48
  }
31
49
 
50
+ impl PullState {
51
+ #[cold]
52
+ fn fresh() -> Box<Self> {
53
+ Box::new(PullState {
54
+ bufs: Buffers::new(),
55
+ keys: AHashMap::with_capacity(256),
56
+ sym_keys: AHashMap::new(),
57
+ vstack: None,
58
+ key_shadow: None,
59
+ })
60
+ }
61
+ }
62
+
32
63
  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
- });
64
+ static PULL_STATE: Cell<Option<Box<PullState>>> = const { Cell::new(None) };
65
+ }
66
+
67
+ /// Run `f` on this thread's parse state, taken out like [`with_taken`]
68
+ /// (a state lost to a longjmp leaks its shadows' last VALUEs; a nested
69
+ /// call would start a fresh one). Unlike the generate scratch, parse
70
+ /// bodies never run Ruby code, so the thread cannot hop native threads
71
+ /// mid-call and one thread-local access serves both the take and the
72
+ /// put-back: measured ~5ns per call on tiny documents against two.
73
+ pub(crate) fn with_pull_state<R>(f: impl FnOnce(&mut PullState) -> R) -> R {
74
+ PULL_STATE.with(|cell| {
75
+ let mut state = cell.take().unwrap_or_else(PullState::fresh);
76
+ let result = f(&mut state);
77
+ cell.set(Some(state));
78
+ result
79
+ })
40
80
  }
41
81
 
42
82
  /// GC-marked holder for pending VALUEs.
@@ -48,6 +88,12 @@ pub(crate) struct VStackShadow {
48
88
  /// through [`DataTypeFunctions::mark`] (its trampoline, not ours),
49
89
  /// pinning every pending VALUE with `rb_gc_mark` semantics. The class
50
90
  /// is defined (and made a private constant) at init.
91
+ ///
92
+ /// Deliberately NOT `wb_protected`: parses push VALUEs into the shadow
93
+ /// with plain stores, no write barriers, so an old protected handle
94
+ /// would let the GC miss young values it holds (a use-after-free).
95
+ /// Staying write-barrier-unprotected makes every GC rescan the handle,
96
+ /// which costs nothing measurable: there are one to three per thread.
51
97
  #[derive(TypedData)]
52
98
  #[magnus(class = "NOSJ::ValueStackShadow", mark)]
53
99
  pub(crate) struct ShadowHandle(*const VStackShadow);
@@ -74,7 +120,7 @@ impl DataTypeFunctions for ShadowHandle {
74
120
  }
75
121
  }
76
122
 
77
- /// Create (once per thread) a leaked, GC-marked VStackShadow.
123
+ /// Create (once per owning state) a leaked, GC-marked VStackShadow.
78
124
  pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>) {
79
125
  if slot.is_none() {
80
126
  let ruby = magnus::Ruby::get().expect("called on a Ruby thread");
@@ -83,7 +129,7 @@ pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>)
83
129
  }));
84
130
  let ptr = std::ptr::from_mut::<VStackShadow>(shadow).cast_const();
85
131
  let handle: Obj<ShadowHandle> = ruby.obj_wrap(ShadowHandle(ptr));
86
- magnus::gc::register_mark_object(handle);
132
+ ruby.gc_register_mark_object(handle);
87
133
  *slot = Some(shadow);
88
134
  }
89
135
  }
@@ -12,7 +12,7 @@ use crate::errors::{nesting_error, parser_error, parser_error_at};
12
12
  use crate::files::with_mapped_file;
13
13
  use crate::parse::{parse_native_opts, utf8_input};
14
14
  use crate::sink::SinkAbort;
15
- use crate::state::PULL_STATE;
15
+ use crate::state::with_pull_state;
16
16
 
17
17
  /// What the document's root value was; reported as a Symbol. The
18
18
  /// Default is never observable (a successful pass always saw a root
@@ -256,8 +256,7 @@ fn stats_over(ruby: &Ruby, input: &[u8], opts: Value) -> Result<Value, Error> {
256
256
  },
257
257
  ..StatsSink::default()
258
258
  };
259
- let result = PULL_STATE.with(|cell| {
260
- let mut state = cell.borrow_mut();
259
+ let result = with_pull_state(|state| {
261
260
  // Safety: callers verified UTF-8 (coderange or a full scan).
262
261
  unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
263
262
  });
data/lib/nosj/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NOSJ
4
4
  # The gem version.
5
- VERSION = "0.3.2"
5
+ VERSION = "0.4.1"
6
6
  end
data/lib/nosj.rb CHANGED
@@ -205,7 +205,8 @@ module NOSJ
205
205
 
206
206
  # Partial parsing by JSON Pointer (with the standard +~0+/+~1+
207
207
  # escapes). The matched subtree materializes under the same options
208
- # as {.parse}.
208
+ # as {.parse}; +allow_nan+ and +allow_trailing_comma+ also govern the
209
+ # walk to it.
209
210
  #
210
211
  # @example
211
212
  # NOSJ.at_pointer(json, "/users/3/name") #=> "grace" or nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nosj
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -104,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
104
  - !ruby/object:Gem::Version
105
105
  version: 3.3.11
106
106
  requirements: []
107
- rubygems_version: 4.0.16
107
+ rubygems_version: 4.0.20
108
108
  specification_version: 4
109
109
  summary: An extremely fast JSON parser and generator for Ruby, written in Rust and
110
110
  SIMD-accelerated on every platform.