lz4rip 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f66c12c967a06a207c9e8e742fac306280863bf655bd7b4318a2265d08c1cdf
4
- data.tar.gz: be95ea7eb336509e7077737f0d4703dc500931f1608fdc2f52dc3b170904b16d
3
+ metadata.gz: ca8b2f012551b63cf5efaaee91d49b319486300a9bb1e22f0d79d64d868ba076
4
+ data.tar.gz: ff7f8c704a56fa29e80ade1d046f3e7fb93eac22bca44d8b684a4727db69355d
5
5
  SHA512:
6
- metadata.gz: 2fc02b8f797989ca99f23e97658dab9442c4fbb70ab03ff3436a73b5026b78d20c1bbf240e4febba0baed905863b225655f03c627d6db4ddee128224b419c82c
7
- data.tar.gz: 92b2aa66f6282317c5b805f77cf47715288c8debc013520e5dffc78fb7d9c102f62f330906042b062b93aa2104ca21c5b1b0830edd87852b585f6451f4146efd
6
+ metadata.gz: 689b55762fed92109d07f280cc1d534e3f4e71ba76c63c1a6c149441ded0ae08d21e4d134a7000f1b4fb40941e7768856113e82272ce88e359bfedcc1b97e3ff
7
+ data.tar.gz: 7f0a1a29b634874f60bde320df035aa14e6cd0080758a3dbd7e3147a74e88e3c1327ee77d1cf383448f591c6162d76e6355637286eed3ee74958e0315eb3d3fc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.1] - 2026-10-11
6
+
7
+ - Update the `lz4rip` crate dependency to 0.11.10 and `rb-sys` to 0.9.130.
8
+ - Make `FrameCodec` instances Ractor-shareable as documented, with native
9
+ dictionary copies that leave caller strings mutable.
10
+ - Unlock input strings and free native results when Ruby interrupts a call
11
+ that released the GVL.
12
+ - Protect mutable inputs when mutex contention releases the GVL, including
13
+ calls below the normal size threshold.
14
+ - Catch Rust panics inside native callbacks instead of aborting the process.
15
+ - Preserve Ruby exceptions, `Thread#kill`, and `catch`/`throw` through
16
+ protected native calls.
17
+ - Avoid trainer deadlocks during sample coercion and borrow decompression
18
+ input after size coercion.
19
+ - Update GitHub Actions checkout to v7 and check Rust tests, formatting, and
20
+ Clippy in CI.
21
+
5
22
  ## [0.2.0] - 2026-08-07
6
23
 
7
24
  - Release the Ruby GVL around large frame/block compression and frame
data/Cargo.lock CHANGED
@@ -31,9 +31,9 @@ dependencies = [
31
31
 
32
32
  [[package]]
33
33
  name = "bitflags"
34
- version = "2.13.1"
34
+ version = "2.13.2"
35
35
  source = "registry+https://github.com/rust-lang/crates.io-index"
36
- checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
36
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
37
37
 
38
38
  [[package]]
39
39
  name = "cexpr"
@@ -46,9 +46,9 @@ dependencies = [
46
46
 
47
47
  [[package]]
48
48
  name = "cfg-if"
49
- version = "1.0.4"
49
+ version = "1.0.5"
50
50
  source = "registry+https://github.com/rust-lang/crates.io-index"
51
- checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
51
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
52
52
 
53
53
  [[package]]
54
54
  name = "clang-sys"
@@ -63,9 +63,9 @@ dependencies = [
63
63
 
64
64
  [[package]]
65
65
  name = "either"
66
- version = "1.17.0"
66
+ version = "1.19.0"
67
67
  source = "registry+https://github.com/rust-lang/crates.io-index"
68
- checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
68
+ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
69
69
 
70
70
  [[package]]
71
71
  name = "glob"
@@ -84,15 +84,15 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "lazy_static"
87
- version = "1.5.0"
87
+ version = "1.5.1"
88
88
  source = "registry+https://github.com/rust-lang/crates.io-index"
89
- checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
89
+ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
90
90
 
91
91
  [[package]]
92
92
  name = "libc"
93
- version = "0.2.189"
93
+ version = "0.2.190"
94
94
  source = "registry+https://github.com/rust-lang/crates.io-index"
95
- checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
95
+ checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
96
96
 
97
97
  [[package]]
98
98
  name = "libloading"
@@ -106,17 +106,17 @@ dependencies = [
106
106
 
107
107
  [[package]]
108
108
  name = "lz4rip"
109
- version = "0.1.2"
109
+ version = "0.2.1"
110
110
  dependencies = [
111
- "lz4rip 0.11.3",
111
+ "lz4rip 0.11.10",
112
112
  "rb-sys",
113
113
  ]
114
114
 
115
115
  [[package]]
116
116
  name = "lz4rip"
117
- version = "0.11.3"
117
+ version = "0.11.10"
118
118
  source = "registry+https://github.com/rust-lang/crates.io-index"
119
- checksum = "55bde261679fd1d14bcae126094d7afb98c63c1a5018d0718741b658b686e950"
119
+ checksum = "37d8ecd80125a8e67ebcaacd9fad5757efa2b6f5b875e1b4ce567814d758a419"
120
120
  dependencies = [
121
121
  "lz4rip-core",
122
122
  "lz4rip-decode",
@@ -126,24 +126,24 @@ dependencies = [
126
126
 
127
127
  [[package]]
128
128
  name = "lz4rip-core"
129
- version = "0.5.5"
129
+ version = "0.5.6"
130
130
  source = "registry+https://github.com/rust-lang/crates.io-index"
131
- checksum = "22fc4edb61fcaf1cdad24ac2982eff7af4417ef463c33bcf5c42ba549abd8b96"
131
+ checksum = "edb50206b4d66e3ec849d2e7ac9257d79c19733c435909a0a46cbb34f9ec7107"
132
132
 
133
133
  [[package]]
134
134
  name = "lz4rip-decode"
135
- version = "0.10.3"
135
+ version = "0.10.4"
136
136
  source = "registry+https://github.com/rust-lang/crates.io-index"
137
- checksum = "60d65bfd4c032fefcf8c18fb5f8d9958f4925146845db16dbc9d416f1566b9bf"
137
+ checksum = "d89c54c126f2acb3979c7d8fc9482506d6371438f514f2e83ef96ede9b585594"
138
138
  dependencies = [
139
139
  "lz4rip-core",
140
140
  ]
141
141
 
142
142
  [[package]]
143
143
  name = "lz4rip-encode"
144
- version = "0.10.5"
144
+ version = "0.10.8"
145
145
  source = "registry+https://github.com/rust-lang/crates.io-index"
146
- checksum = "09e92b7345788ec0d32b5e31051dc6a86730e17b01267e502e9ec2744b689d34"
146
+ checksum = "51a3509ddaac04400136c19f6a2d8427c18c46357eb584c9908b3ff476dcd13e"
147
147
  dependencies = [
148
148
  "lz4rip-core",
149
149
  ]
@@ -190,18 +190,18 @@ dependencies = [
190
190
 
191
191
  [[package]]
192
192
  name = "rb-sys"
193
- version = "0.9.128"
193
+ version = "0.9.130"
194
194
  source = "registry+https://github.com/rust-lang/crates.io-index"
195
- checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
195
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
196
196
  dependencies = [
197
197
  "rb-sys-build",
198
198
  ]
199
199
 
200
200
  [[package]]
201
201
  name = "rb-sys-build"
202
- version = "0.9.128"
202
+ version = "0.9.130"
203
203
  source = "registry+https://github.com/rust-lang/crates.io-index"
204
- checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
204
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
205
205
  dependencies = [
206
206
  "bindgen",
207
207
  "lazy_static",
@@ -272,15 +272,15 @@ dependencies = [
272
272
 
273
273
  [[package]]
274
274
  name = "twox-hash"
275
- version = "2.1.3"
275
+ version = "2.1.5"
276
276
  source = "registry+https://github.com/rust-lang/crates.io-index"
277
- checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
277
+ checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
278
278
 
279
279
  [[package]]
280
280
  name = "unicode-ident"
281
- version = "1.0.24"
281
+ version = "1.0.26"
282
282
  source = "registry+https://github.com/rust-lang/crates.io-index"
283
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
283
+ checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
284
284
 
285
285
  [[package]]
286
286
  name = "windows-link"
data/Cargo.toml CHANGED
@@ -6,4 +6,3 @@ resolver = "2"
6
6
  opt-level = 3
7
7
  lto = true
8
8
  codegen-units = 1
9
- panic = "abort"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lz4rip"
3
- version = "0.1.2"
3
+ version = "0.2.1"
4
4
  edition = "2021"
5
5
  build = "build.rs"
6
6
 
@@ -9,8 +9,8 @@ name = "lz4rip"
9
9
  crate-type = ["cdylib", "rlib"]
10
10
 
11
11
  [dependencies]
12
- lz4 = { version = "0.11.3", package = "lz4rip" }
13
- rb-sys = "0.9"
12
+ lz4 = { version = "0.11.10", package = "lz4rip" }
13
+ rb-sys = "0.9.130"
14
14
 
15
15
  [build-dependencies]
16
- rb-sys = "0.9"
16
+ rb-sys = "0.9.130"
@@ -40,27 +40,38 @@ fn should_release_frame_decompress_gvl(input_len: usize) -> bool {
40
40
  input_len >= GVL_FRAME_DECOMPRESS_THRESHOLD
41
41
  }
42
42
 
43
- fn with_mutex<T, R, F>(mutex: &Mutex<T>, release_gvl: bool, name: &str, func: F) -> RbResult<R>
43
+ fn with_mutex<T, R, F>(
44
+ mutex: &Mutex<T>,
45
+ release_gvl: bool,
46
+ name: &str,
47
+ input: &mut rb::InputBytes,
48
+ func: F,
49
+ ) -> RbResult<R>
44
50
  where
45
- F: FnOnce(&mut T) -> RbResult<R>,
51
+ F: FnOnce(&mut T, &[u8]) -> RbResult<R>,
46
52
  {
47
53
  if release_gvl {
54
+ input.lock_for_without_gvl(true)?;
48
55
  return rb::maybe_without_gvl(true, || {
49
56
  let mut guard = mutex
50
57
  .lock()
51
58
  .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
52
- func(&mut guard)
53
- });
59
+ func(&mut guard, input.as_slice())
60
+ })?;
54
61
  }
55
62
 
56
63
  match mutex.try_lock() {
57
- Ok(mut guard) => func(&mut guard),
58
- Err(TryLockError::WouldBlock) => rb::maybe_without_gvl(true, || {
59
- let mut guard = mutex
60
- .lock()
61
- .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
62
- func(&mut guard)
63
- }),
64
+ Ok(mut guard) => func(&mut guard, input.as_slice()),
65
+ Err(TryLockError::WouldBlock) => {
66
+ // Even a small input needs protection when contention releases GVL.
67
+ input.lock_for_without_gvl(true)?;
68
+ rb::maybe_without_gvl(true, || {
69
+ let mut guard = mutex
70
+ .lock()
71
+ .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
72
+ func(&mut guard, input.as_slice())
73
+ })?
74
+ }
64
75
  Err(TryLockError::Poisoned(_)) => Err(RubyErr::runtime(format!("{name} mutex poisoned"))),
65
76
  }
66
77
  }
@@ -122,7 +133,7 @@ fn make_frame_codec_data_type() -> rb_data_type_t {
122
133
  },
123
134
  parent: std::ptr::null(),
124
135
  data: std::ptr::null_mut(),
125
- flags: 1,
136
+ flags: rb::frozen_shareable_flags(),
126
137
  }
127
138
  }
128
139
 
@@ -260,10 +271,14 @@ fn block_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE>
260
271
  input.lock_for_without_gvl(release_gvl)?;
261
272
 
262
273
  let out = match &rb_self.compressor {
263
- None => rb::maybe_without_gvl(release_gvl, || Ok(block::compress(input.as_slice())))?,
264
- Some(comp) => with_mutex(comp, release_gvl, "BlockCodec compressor", |comp| {
265
- Ok(comp.compress(input.as_slice()))
266
- })?,
274
+ None => rb::maybe_without_gvl(release_gvl, || block::compress(input.as_slice()))?,
275
+ Some(comp) => with_mutex(
276
+ comp,
277
+ release_gvl,
278
+ "BlockCodec compressor",
279
+ &mut input,
280
+ |comp, input| Ok(comp.compress(input)),
281
+ )?,
267
282
  };
268
283
 
269
284
  rb::new_binary_string(&out)
@@ -275,14 +290,18 @@ fn block_codec_decompress_impl(
275
290
  decompressed_size: VALUE,
276
291
  ) -> RbResult<VALUE> {
277
292
  let rb_self = unsafe { block_codec_ref(rb_self)? };
278
- let compressed = rb::input_bytes(rb_input)?;
279
293
  let decompressed_size = rb::value_to_usize(decompressed_size)?;
294
+ let mut compressed = rb::input_bytes(rb_input)?;
280
295
 
281
296
  let result = match &rb_self.decompressor {
282
297
  None => block::decompress(compressed.as_slice(), decompressed_size),
283
- Some(decomp) => with_mutex(decomp, false, "BlockCodec decompressor", |decomp| {
284
- Ok(decomp.decompress(compressed.as_slice(), decompressed_size))
285
- })?,
298
+ Some(decomp) => with_mutex(
299
+ decomp,
300
+ false,
301
+ "BlockCodec decompressor",
302
+ &mut compressed,
303
+ |decomp, compressed| Ok(decomp.decompress(compressed, decompressed_size)),
304
+ )?,
286
305
  };
287
306
 
288
307
  match result {
@@ -331,23 +350,16 @@ struct DictBound {
331
350
 
332
351
  fn frame_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE) -> RbResult<VALUE> {
333
352
  let id = rb::value_to_u32(id)?;
334
- let dict = if rb_dict == rb::qnil() {
335
- None
336
- } else {
337
- let rb_dict = rb::string_value(rb_dict)?;
338
- rb::freeze_value(rb_dict)?;
339
- Some(DictBound {
340
- bytes: rb::value_to_bytes(rb_dict)?,
341
- id,
342
- })
343
- };
344
- unsafe {
353
+ let dict = rb::value_to_option_bytes(rb_dict)?.map(|bytes| DictBound { bytes, id });
354
+ let codec = unsafe {
345
355
  rb::wrap_typed_data(
346
356
  class,
347
357
  Box::new(FrameCodec { dict }),
348
358
  frame_codec_data_type(),
349
- )
350
- }
359
+ )?
360
+ };
361
+ rb::make_shareable(codec)?;
362
+ Ok(codec)
351
363
  }
352
364
 
353
365
  fn frame_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE> {
@@ -356,7 +368,7 @@ fn frame_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE>
356
368
  let release_gvl = should_release_compress_gvl(input.len());
357
369
  input.lock_for_without_gvl(release_gvl)?;
358
370
 
359
- let out = rb::maybe_without_gvl(release_gvl, || compress_frame(rb_self, input.as_slice()))
371
+ let out = rb::maybe_without_gvl(release_gvl, || compress_frame(rb_self, input.as_slice()))?
360
372
  .map_err(RubyErr::runtime)?;
361
373
 
362
374
  rb::new_binary_string(&out)
@@ -389,8 +401,8 @@ fn frame_codec_decompress_impl(
389
401
  max_decompressed_size: VALUE,
390
402
  ) -> RbResult<VALUE> {
391
403
  let rb_self = unsafe { frame_codec_ref(rb_self)? };
392
- let mut input = rb::input_bytes(rb_input)?;
393
404
  let max_decompressed_size = rb::value_to_option_usize(max_decompressed_size)?;
405
+ let mut input = rb::input_bytes(rb_input)?;
394
406
  let release_gvl = should_release_frame_decompress_gvl(input.len());
395
407
  input.lock_for_without_gvl(release_gvl)?;
396
408
 
@@ -403,7 +415,7 @@ fn frame_codec_decompress_impl(
403
415
 
404
416
  let out = rb::maybe_without_gvl(release_gvl, || {
405
417
  decompress_frame(rb_self, input.as_slice(), max_decompressed_size)
406
- })
418
+ })?
407
419
  .map_err(|e| RubyErr::new(decompress_error(), e))?;
408
420
 
409
421
  rb::new_binary_string(&out)
@@ -496,6 +508,7 @@ fn dict_trainer_new_impl(class: VALUE, max_dict_size: VALUE) -> RbResult<VALUE>
496
508
  }
497
509
 
498
510
  fn dict_trainer_add_sample_impl(rb_self: VALUE, rb_data: VALUE) -> RbResult<VALUE> {
511
+ let data = rb::input_bytes(rb_data)?;
499
512
  let rb_self = unsafe { dict_trainer_ref(rb_self)? };
500
513
  let mut borrow = rb_self
501
514
  .inner
@@ -504,7 +517,6 @@ fn dict_trainer_add_sample_impl(rb_self: VALUE, rb_data: VALUE) -> RbResult<VALU
504
517
  let trainer = borrow
505
518
  .as_mut()
506
519
  .ok_or_else(|| RubyErr::runtime("DictTrainer already consumed by #train"))?;
507
- let data = rb::input_bytes(rb_data)?;
508
520
  let sample = if data.len() > rb_self.max_dict_size {
509
521
  &data.as_slice()[..rb_self.max_dict_size]
510
522
  } else {
data/ext/lz4rip/src/rb.rs CHANGED
@@ -1,6 +1,4 @@
1
1
  use std::ffi::{c_char, c_long, c_void, CStr, CString};
2
- #[cfg(ruby_engine = "mri")]
3
- use std::mem::MaybeUninit;
4
2
  use std::panic::{catch_unwind, AssertUnwindSafe};
5
3
  use std::ptr;
6
4
 
@@ -32,7 +30,7 @@ unsafe extern "C" {
32
30
 
33
31
  #[derive(Debug)]
34
32
  pub enum RubyErr {
35
- Exception(VALUE),
33
+ Jump(i32),
36
34
  Error { class: VALUE, message: String },
37
35
  }
38
36
 
@@ -55,15 +53,6 @@ impl RubyErr {
55
53
  pub fn type_error(message: impl Into<String>) -> Self {
56
54
  Self::new(unsafe { rb_sys::rb_eTypeError }, message)
57
55
  }
58
-
59
- fn current_exception() -> Self {
60
- let err = unsafe { rb_sys::rb_errinfo() };
61
- if err == qnil() {
62
- Self::runtime("Ruby exception")
63
- } else {
64
- Self::Exception(err)
65
- }
66
- }
67
56
  }
68
57
 
69
58
  pub fn wrap<F>(f: F) -> VALUE
@@ -89,12 +78,15 @@ where
89
78
  }
90
79
 
91
80
  pub fn raise(err: RubyErr) -> ! {
92
- let exc = match err {
93
- RubyErr::Exception(exc) => exc,
94
- RubyErr::Error { class, message } => error_exception(class, message),
95
- };
96
-
97
- unsafe { rb_sys::rb_exc_raise(exc) }
81
+ match err {
82
+ // Preserve Ruby's tag and pending error info for Thread#kill and
83
+ // catch/throw, as well as ordinary exceptions.
84
+ RubyErr::Jump(state) => unsafe { rb_sys::rb_jump_tag(state) },
85
+ RubyErr::Error { class, message } => {
86
+ let exc = error_exception(class, message);
87
+ unsafe { rb_sys::rb_exc_raise(exc) }
88
+ }
89
+ }
98
90
  }
99
91
 
100
92
  fn error_exception(class: VALUE, message: String) -> VALUE {
@@ -145,7 +137,7 @@ where
145
137
  };
146
138
 
147
139
  if state != 0 {
148
- Err(RubyErr::current_exception())
140
+ Err(RubyErr::Jump(state))
149
141
  } else if data.panicked {
150
142
  Err(RubyErr::runtime("native Rust panic"))
151
143
  } else {
@@ -167,7 +159,7 @@ where
167
159
  #[cfg(ruby_engine = "mri")]
168
160
  struct WithoutGvlData<F, R> {
169
161
  func: Option<F>,
170
- output: MaybeUninit<R>,
162
+ output: Option<std::thread::Result<R>>,
171
163
  }
172
164
 
173
165
  #[cfg(ruby_engine = "mri")]
@@ -176,49 +168,55 @@ where
176
168
  F: FnOnce() -> R,
177
169
  {
178
170
  let data = unsafe { &mut *(data.cast::<WithoutGvlData<F, R>>()) };
179
- let func = data.func.take().expect("missing without-GVL function");
180
- data.output.write(func());
171
+ // Rust panics must be caught before returning through the C callback.
172
+ data.output = Some(catch_unwind(AssertUnwindSafe(|| {
173
+ data.func.take().expect("missing without-GVL function")()
174
+ })));
181
175
  ptr::null_mut()
182
176
  }
183
177
 
184
178
  #[cfg(ruby_engine = "mri")]
185
- fn without_gvl<F, R>(func: F) -> R
179
+ fn without_gvl<F, R>(func: F) -> RbResult<R>
186
180
  where
187
181
  F: FnOnce() -> R,
188
182
  {
189
183
  let mut data = WithoutGvlData {
190
184
  func: Some(func),
191
- output: MaybeUninit::uninit(),
185
+ output: None,
192
186
  };
193
- unsafe {
187
+ // Ruby checks interrupts before and after the callback. Protect the call
188
+ // so an exception still drops native output and unlocks borrowed strings.
189
+ protect_unit(|| unsafe {
194
190
  rb_thread_call_without_gvl(
195
191
  Some(without_gvl_trampoline::<F, R>),
196
192
  (&mut data as *mut WithoutGvlData<F, R>).cast::<c_void>(),
197
193
  None,
198
194
  ptr::null_mut(),
199
195
  );
200
- data.output.assume_init()
201
- }
196
+ })?;
197
+ data.output
198
+ .ok_or_else(|| RubyErr::runtime("without-GVL callback did not run"))?
199
+ .map_err(|_| RubyErr::runtime("native Rust panic"))
202
200
  }
203
201
 
204
202
  #[cfg(ruby_engine = "mri")]
205
- pub fn maybe_without_gvl<F, R>(release_gvl: bool, func: F) -> R
203
+ pub fn maybe_without_gvl<F, R>(release_gvl: bool, func: F) -> RbResult<R>
206
204
  where
207
205
  F: FnOnce() -> R,
208
206
  {
209
207
  if release_gvl {
210
208
  without_gvl(func)
211
209
  } else {
212
- func()
210
+ Ok(func())
213
211
  }
214
212
  }
215
213
 
216
214
  #[cfg(not(ruby_engine = "mri"))]
217
- pub fn maybe_without_gvl<F, R>(_release_gvl: bool, func: F) -> R
215
+ pub fn maybe_without_gvl<F, R>(_release_gvl: bool, func: F) -> RbResult<R>
218
216
  where
219
217
  F: FnOnce() -> R,
220
218
  {
221
- func()
219
+ Ok(func())
222
220
  }
223
221
 
224
222
  pub const fn qnil() -> VALUE {
@@ -255,8 +253,23 @@ pub fn string_value(value: VALUE) -> RbResult<VALUE> {
255
253
  protect_value(|| unsafe { rb_sys::rb_str_to_str(value) })
256
254
  }
257
255
 
258
- pub fn freeze_value(value: VALUE) -> RbResult<()> {
259
- protect_unit(|| unsafe { rb_sys::RB_OBJ_FREEZE(value) })
256
+ pub fn frozen_shareable_flags() -> VALUE {
257
+ #[cfg(ruby_engine = "mri")]
258
+ {
259
+ // RUBY_TYPED_FROZEN_SHAREABLE equals RUBY_FL_SHAREABLE in Ruby headers.
260
+ 1 | rb_sys::ruby_fl_type::RUBY_FL_SHAREABLE as VALUE
261
+ }
262
+ #[cfg(not(ruby_engine = "mri"))]
263
+ {
264
+ 1
265
+ }
266
+ }
267
+
268
+ pub fn make_shareable(value: VALUE) -> RbResult<()> {
269
+ protect_unit(|| unsafe { rb_sys::RB_OBJ_FREEZE(value) })?;
270
+ #[cfg(ruby_engine = "mri")]
271
+ protect_value(|| unsafe { rb_sys::rb_ractor_make_shareable(value) })?;
272
+ Ok(())
260
273
  }
261
274
 
262
275
  pub fn value_to_bytes(value: VALUE) -> RbResult<Vec<u8>> {
@@ -626,3 +639,42 @@ unsafe fn transmute_3(
626
639
  fn c_long_len(len: usize) -> RbResult<c_long> {
627
640
  c_long::try_from(len).map_err(|_| RubyErr::arg("length too large"))
628
641
  }
642
+
643
+ #[cfg(all(test, ruby_engine = "mri"))]
644
+ mod tests {
645
+ use super::*;
646
+
647
+ fn run_callback<F, R>(func: F) -> std::thread::Result<R>
648
+ where
649
+ F: FnOnce() -> R,
650
+ {
651
+ let mut data = WithoutGvlData {
652
+ func: Some(func),
653
+ output: None,
654
+ };
655
+ unsafe {
656
+ without_gvl_trampoline::<F, R>(
657
+ (&mut data as *mut WithoutGvlData<F, R>).cast::<c_void>(),
658
+ );
659
+ }
660
+ data.output.unwrap()
661
+ }
662
+
663
+ #[test]
664
+ fn callback_catches_panics_before_returning_to_c() {
665
+ let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
666
+ struct OnDrop(std::sync::Arc<std::sync::atomic::AtomicBool>);
667
+ impl Drop for OnDrop {
668
+ fn drop(&mut self) {
669
+ self.0.store(true, std::sync::atomic::Ordering::SeqCst);
670
+ }
671
+ }
672
+ let captured = OnDrop(dropped.clone());
673
+ let result = run_callback(move || {
674
+ let _captured = captured;
675
+ panic!("callback panic");
676
+ });
677
+ assert!(result.is_err());
678
+ assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
679
+ }
680
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lz4rip
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lz4rip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.9'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.130
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
28
  version: '0.9'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.9.130
26
32
  description: Ruby bindings (via Rust/rb-sys) for lz4rip, a pure-Rust LZ4 implementation.
27
33
  Block-format and frame-format compress/decompress with optional dictionary support
28
34
  and COVER-based dictionary training. Ractor-safe.
@@ -54,7 +60,7 @@ licenses:
54
60
  - MIT
55
61
  metadata:
56
62
  homepage_uri: https://github.com/paddor/lz4rip-rb
57
- source_code_uri: https://github.com/paddor/lz4rip-rb
63
+ source_code_uri: https://github.com/paddor/lz4rip-rb/tree/main
58
64
  documentation_uri: https://rubydoc.info/gems/lz4rip
59
65
  changelog_uri: https://github.com/paddor/lz4rip-rb/blob/main/CHANGELOG.md
60
66
  rubygems_mfa_required: 'true'
@@ -72,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
78
  - !ruby/object:Gem::Version
73
79
  version: '0'
74
80
  requirements: []
75
- rubygems_version: 4.0.16
81
+ rubygems_version: 4.0.20
76
82
  specification_version: 4
77
83
  summary: Ractor-safe LZ4 compression for Ruby
78
84
  test_files: []