zrip 0.1.2 → 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.
data/ext/zrip/src/lib.rs CHANGED
@@ -1,44 +1,89 @@
1
- use magnus::{
2
- exception::ExceptionClass, function, method, prelude::*, r_string::RString, value::Opaque,
3
- Error, Ruby,
4
- };
5
- use std::cell::RefCell;
6
- use std::sync::{Mutex, OnceLock};
1
+ mod rb;
7
2
 
3
+ use std::ffi::c_void;
4
+ use std::panic::{catch_unwind, AssertUnwindSafe};
5
+ use std::sync::{Mutex, OnceLock, TryLockError};
6
+
7
+ use rb_sys::{rb_data_type_struct__bindgen_ty_1, rb_data_type_t, size_t, VALUE};
8
8
  use zstd::dict::fastcover::FastCoverParams;
9
9
  use zstd::{CompressContext, DecompressContext, Dictionary};
10
10
 
11
- static DECOMPRESS_ERROR: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
12
- static COMPRESS_ERROR: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
13
- static MISSING_CONTENT_SIZE_ERROR: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
14
- static OUTPUT_SIZE_LIMIT_ERROR: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
11
+ use crate::rb::{RbResult, RubyErr};
12
+
13
+ static DECOMPRESS_ERROR: OnceLock<GlobalValue> = OnceLock::new();
14
+ static COMPRESS_ERROR: OnceLock<GlobalValue> = OnceLock::new();
15
+ static MISSING_CONTENT_SIZE_ERROR: OnceLock<GlobalValue> = OnceLock::new();
16
+ static OUTPUT_SIZE_LIMIT_ERROR: OnceLock<GlobalValue> = OnceLock::new();
17
+
18
+ const GVL_COMPRESS_THRESHOLD: usize = 64 * 1024;
19
+ const GVL_FRAME_DECOMPRESS_THRESHOLD: usize = 64 * 1024;
15
20
 
16
- fn decompress_error(ruby: &Ruby) -> ExceptionClass {
17
- ruby.get_inner(
18
- *DECOMPRESS_ERROR
19
- .get()
20
- .expect("DecompressError not initialized"),
21
- )
21
+ #[derive(Copy, Clone)]
22
+ struct GlobalValue(VALUE);
23
+
24
+ unsafe impl Send for GlobalValue {}
25
+ unsafe impl Sync for GlobalValue {}
26
+
27
+ fn stored_value(lock: &OnceLock<GlobalValue>, name: &str) -> VALUE {
28
+ lock.get()
29
+ .unwrap_or_else(|| panic!("{name} not initialized"))
30
+ .0
22
31
  }
23
32
 
24
- fn compress_error(ruby: &Ruby) -> ExceptionClass {
25
- ruby.get_inner(*COMPRESS_ERROR.get().expect("CompressError not initialized"))
33
+ fn decompress_error() -> VALUE {
34
+ stored_value(&DECOMPRESS_ERROR, "DecompressError")
26
35
  }
27
36
 
28
- fn missing_content_size_error(ruby: &Ruby) -> ExceptionClass {
29
- ruby.get_inner(
30
- *MISSING_CONTENT_SIZE_ERROR
31
- .get()
32
- .expect("MissingContentSizeError not initialized"),
33
- )
37
+ fn compress_error() -> VALUE {
38
+ stored_value(&COMPRESS_ERROR, "CompressError")
34
39
  }
35
40
 
36
- fn output_size_limit_error(ruby: &Ruby) -> ExceptionClass {
37
- ruby.get_inner(
38
- *OUTPUT_SIZE_LIMIT_ERROR
39
- .get()
40
- .expect("OutputSizeLimitError not initialized"),
41
- )
41
+ fn output_size_limit_error() -> VALUE {
42
+ stored_value(&OUTPUT_SIZE_LIMIT_ERROR, "OutputSizeLimitError")
43
+ }
44
+
45
+ fn should_release_compress_gvl(input_len: usize) -> bool {
46
+ input_len >= GVL_COMPRESS_THRESHOLD
47
+ }
48
+
49
+ fn should_release_frame_decompress_gvl(output_len: usize) -> bool {
50
+ output_len >= GVL_FRAME_DECOMPRESS_THRESHOLD
51
+ }
52
+
53
+ fn with_mutex<T, R, F>(
54
+ mutex: &Mutex<T>,
55
+ release_gvl: bool,
56
+ name: &str,
57
+ input: &mut rb::InputBytes,
58
+ func: F,
59
+ ) -> RbResult<R>
60
+ where
61
+ F: FnOnce(&mut T, &[u8]) -> RbResult<R>,
62
+ {
63
+ if release_gvl {
64
+ input.lock_for_without_gvl(true)?;
65
+ return rb::maybe_without_gvl(true, || {
66
+ let mut guard = mutex
67
+ .lock()
68
+ .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
69
+ func(&mut guard, input.as_slice())
70
+ })?;
71
+ }
72
+
73
+ match mutex.try_lock() {
74
+ Ok(mut guard) => func(&mut guard, input.as_slice()),
75
+ Err(TryLockError::WouldBlock) => {
76
+ // Even a small input needs protection when contention releases GVL.
77
+ input.lock_for_without_gvl(true)?;
78
+ rb::maybe_without_gvl(true, || {
79
+ let mut guard = mutex
80
+ .lock()
81
+ .map_err(|_| RubyErr::runtime(format!("{name} mutex poisoned")))?;
82
+ func(&mut guard, input.as_slice())
83
+ })?
84
+ }
85
+ Err(TryLockError::Poisoned(_)) => Err(RubyErr::runtime(format!("{name} mutex poisoned"))),
86
+ }
42
87
  }
43
88
 
44
89
  // ---------- frame header parsing ----------
@@ -48,54 +93,17 @@ const ZSTD_FRAME_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
48
93
  #[derive(Debug)]
49
94
  enum BoundedError {
50
95
  BadMagic,
51
- MissingContentSize,
52
- OutputSizeLimit { declared: u64, limit: u64 },
96
+ OutputSizeLimit { limit: u64 },
53
97
  DecoderFailed(String),
54
98
  }
55
99
 
56
100
  fn parse_frame_content_size(input: &[u8]) -> Result<Option<u64>, BoundedError> {
57
- if input.len() < 5 {
58
- return Err(BoundedError::BadMagic);
59
- }
60
- if input[..4] != ZSTD_FRAME_MAGIC {
61
- return Err(BoundedError::BadMagic);
62
- }
63
- let fhd = input[4];
64
- let fcs_flag = (fhd >> 6) & 3;
65
- let single_segment = (fhd >> 5) & 1;
66
- let dict_id_flag = fhd & 3;
67
-
68
- let window_desc_size = if single_segment == 0 { 1usize } else { 0 };
69
- let dict_id_size = [0usize, 1, 2, 4][dict_id_flag as usize];
70
- let fcs_field_size = match fcs_flag {
71
- 0 => {
72
- if single_segment == 1 {
73
- 1usize
74
- } else {
75
- return Ok(None);
76
- }
77
- }
78
- 1 => 2,
79
- 2 => 4,
80
- 3 => 8,
81
- _ => unreachable!(),
82
- };
83
-
84
- let fcs_offset = 5 + window_desc_size + dict_id_size;
85
- if input.len() < fcs_offset + fcs_field_size {
86
- return Err(BoundedError::BadMagic);
87
- }
88
-
89
- let fcs_bytes = &input[fcs_offset..fcs_offset + fcs_field_size];
90
- let value = match fcs_field_size {
91
- 1 => fcs_bytes[0] as u64,
92
- 2 => u16::from_le_bytes([fcs_bytes[0], fcs_bytes[1]]) as u64 + 256,
93
- 4 => u32::from_le_bytes([fcs_bytes[0], fcs_bytes[1], fcs_bytes[2], fcs_bytes[3]]) as u64,
94
- 8 => u64::from_le_bytes(fcs_bytes.try_into().unwrap()),
95
- _ => unreachable!(),
96
- };
97
-
98
- Ok(Some(value))
101
+ zstd::frame::header::parse_frame_header(input)
102
+ .map(|header| header.frame_content_size)
103
+ .map_err(|err| match err {
104
+ zstd::DecompressError::BadMagic => BoundedError::BadMagic,
105
+ _ => BoundedError::DecoderFailed(err.to_string()),
106
+ })
99
107
  }
100
108
 
101
109
  fn decompress_bounded(
@@ -107,71 +115,186 @@ fn decompress_bounded(
107
115
  return Err(BoundedError::BadMagic);
108
116
  }
109
117
 
110
- let upper = match parse_frame_content_size(compressed)? {
111
- Some(n) => {
112
- if max_output != 0 && n > max_output as u64 {
113
- return Err(BoundedError::OutputSizeLimit {
114
- declared: n,
115
- limit: max_output as u64,
116
- });
117
- }
118
- if n > u64::from(u32::MAX) {
119
- return Err(BoundedError::OutputSizeLimit {
120
- declared: n,
121
- limit: u64::from(u32::MAX),
122
- });
123
- }
124
- n as usize
125
- }
126
- None => {
127
- if max_output != 0 {
128
- return Err(BoundedError::MissingContentSize);
129
- }
130
- 1024 * 1024
131
- }
118
+ let limit = if max_output == 0 {
119
+ usize::MAX
120
+ } else {
121
+ max_output
132
122
  };
133
-
134
- let result = dctx
135
- .decompress_with_limit(compressed, upper)
136
- .map_err(|e| BoundedError::DecoderFailed(format!("{e}")))?;
137
-
138
- Ok(result.into_owned())
123
+ dctx.decompress_with_limit(compressed, limit)
124
+ .map(|out| out.into_owned())
125
+ .map_err(|err| match err {
126
+ zstd::DecompressError::OutputTooSmall => BoundedError::OutputSizeLimit {
127
+ limit: max_output as u64,
128
+ },
129
+ _ => BoundedError::DecoderFailed(err.to_string()),
130
+ })
131
+ }
132
+
133
+ fn decompress_work_size(compressed: &[u8], max_output: usize) -> usize {
134
+ match parse_frame_content_size(compressed) {
135
+ Ok(Some(n)) => usize::try_from(n).unwrap_or(usize::MAX),
136
+ Ok(None) if max_output != 0 => max_output,
137
+ _ => compressed.len(),
138
+ }
139
139
  }
140
140
 
141
- fn raise_bounded(ruby: &Ruby, err: BoundedError, prefix: &str) -> Error {
141
+ fn bounded_err(err: BoundedError, prefix: &str) -> RubyErr {
142
142
  match err {
143
- BoundedError::BadMagic => Error::new(
144
- decompress_error(ruby),
143
+ BoundedError::BadMagic => RubyErr::new(
144
+ decompress_error(),
145
145
  format!("{prefix}: bad magic (input is not a Zstd frame)"),
146
146
  ),
147
- BoundedError::MissingContentSize => Error::new(
148
- missing_content_size_error(ruby),
149
- format!("{prefix}: Frame_Content_Size absent from frame header"),
150
- ),
151
- BoundedError::OutputSizeLimit { declared, limit } => Error::new(
152
- output_size_limit_error(ruby),
153
- format!("{prefix}: declared content size {declared} exceeds limit {limit}"),
147
+ BoundedError::OutputSizeLimit { limit } => RubyErr::new(
148
+ output_size_limit_error(),
149
+ format!("{prefix}: decompressed output exceeds limit {limit}"),
154
150
  ),
155
151
  BoundedError::DecoderFailed(msg) => {
156
- Error::new(decompress_error(ruby), format!("{prefix}: {msg}"))
152
+ RubyErr::new(decompress_error(), format!("{prefix}: {msg}"))
157
153
  }
158
154
  }
159
155
  }
160
156
 
161
157
  // ---------- dict helper ----------
162
158
 
163
- fn load_dict(ruby: &Ruby, bytes: &[u8]) -> Result<Dictionary, Error> {
159
+ fn load_dict(bytes: &[u8]) -> RbResult<Dictionary> {
164
160
  Dictionary::from_bytes(bytes).map_err(|_| {
165
- Error::new(
166
- ruby.exception_runtime_error(),
167
- "dictionary must be in ZDICT format (use DictTrainer to train one)",
168
- )
161
+ RubyErr::runtime("dictionary must be in ZDICT format (use DictTrainer to train one)")
169
162
  })
170
163
  }
171
164
 
165
+ // ---------- typed data ----------
166
+
167
+ struct NativeDataType(rb_data_type_t);
168
+
169
+ unsafe impl Send for NativeDataType {}
170
+ unsafe impl Sync for NativeDataType {}
171
+
172
+ static FRAME_CODEC_DATA_TYPE: OnceLock<NativeDataType> = OnceLock::new();
173
+ static BLOCK_CODEC_DATA_TYPE: OnceLock<NativeDataType> = OnceLock::new();
174
+ static DICT_TRAINER_DATA_TYPE: OnceLock<NativeDataType> = OnceLock::new();
175
+
176
+ fn frame_codec_data_type() -> *const rb_data_type_t {
177
+ &FRAME_CODEC_DATA_TYPE
178
+ .get_or_init(|| NativeDataType(make_frame_codec_data_type()))
179
+ .0
180
+ }
181
+
182
+ fn block_codec_data_type() -> *const rb_data_type_t {
183
+ &BLOCK_CODEC_DATA_TYPE
184
+ .get_or_init(|| NativeDataType(make_block_codec_data_type()))
185
+ .0
186
+ }
187
+
188
+ fn dict_trainer_data_type() -> *const rb_data_type_t {
189
+ &DICT_TRAINER_DATA_TYPE
190
+ .get_or_init(|| NativeDataType(make_dict_trainer_data_type()))
191
+ .0
192
+ }
193
+
194
+ fn make_frame_codec_data_type() -> rb_data_type_t {
195
+ rb_data_type_t {
196
+ wrap_struct_name: c"zrip_frame_codec".as_ptr(),
197
+ function: rb_data_type_struct__bindgen_ty_1 {
198
+ dmark: None,
199
+ dfree: Some(frame_codec_free),
200
+ dsize: Some(frame_codec_native_size),
201
+ dcompact: None,
202
+ reserved: [std::ptr::null_mut(); 1],
203
+ },
204
+ parent: std::ptr::null(),
205
+ data: std::ptr::null_mut(),
206
+ flags: rb::frozen_shareable_flags(),
207
+ }
208
+ }
209
+
210
+ fn make_block_codec_data_type() -> rb_data_type_t {
211
+ rb_data_type_t {
212
+ wrap_struct_name: c"zrip_block_codec".as_ptr(),
213
+ function: rb_data_type_struct__bindgen_ty_1 {
214
+ dmark: None,
215
+ dfree: Some(block_codec_free),
216
+ dsize: Some(block_codec_native_size),
217
+ dcompact: None,
218
+ reserved: [std::ptr::null_mut(); 1],
219
+ },
220
+ parent: std::ptr::null(),
221
+ data: std::ptr::null_mut(),
222
+ flags: 1,
223
+ }
224
+ }
225
+
226
+ fn make_dict_trainer_data_type() -> rb_data_type_t {
227
+ rb_data_type_t {
228
+ wrap_struct_name: c"zrip_dict_trainer".as_ptr(),
229
+ function: rb_data_type_struct__bindgen_ty_1 {
230
+ dmark: None,
231
+ dfree: Some(dict_trainer_free),
232
+ dsize: Some(dict_trainer_native_size),
233
+ dcompact: None,
234
+ reserved: [std::ptr::null_mut(); 1],
235
+ },
236
+ parent: std::ptr::null(),
237
+ data: std::ptr::null_mut(),
238
+ flags: 1,
239
+ }
240
+ }
241
+
242
+ unsafe extern "C" fn frame_codec_free(ptr: *mut c_void) {
243
+ if ptr.is_null() {
244
+ return;
245
+ }
246
+
247
+ let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
248
+ drop(Box::from_raw(ptr as *mut FrameCodec));
249
+ }));
250
+ }
251
+
252
+ unsafe extern "C" fn block_codec_free(ptr: *mut c_void) {
253
+ if ptr.is_null() {
254
+ return;
255
+ }
256
+
257
+ let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
258
+ drop(Box::from_raw(ptr as *mut BlockCodec));
259
+ }));
260
+ }
261
+
262
+ unsafe extern "C" fn dict_trainer_free(ptr: *mut c_void) {
263
+ if ptr.is_null() {
264
+ return;
265
+ }
266
+
267
+ let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
268
+ drop(Box::from_raw(ptr as *mut RbDictTrainer));
269
+ }));
270
+ }
271
+
272
+ unsafe extern "C" fn frame_codec_native_size(_ptr: *const c_void) -> size_t {
273
+ std::mem::size_of::<FrameCodec>() as size_t
274
+ }
275
+
276
+ unsafe extern "C" fn block_codec_native_size(_ptr: *const c_void) -> size_t {
277
+ std::mem::size_of::<BlockCodec>() as size_t
278
+ }
279
+
280
+ unsafe extern "C" fn dict_trainer_native_size(_ptr: *const c_void) -> size_t {
281
+ std::mem::size_of::<RbDictTrainer>() as size_t
282
+ }
283
+
284
+ unsafe fn frame_codec_ref(value: VALUE) -> RbResult<&'static FrameCodec> {
285
+ unsafe { rb::typed_data_ref(value, frame_codec_data_type(), "Zrip::FrameCodec") }
286
+ }
287
+
288
+ unsafe fn block_codec_ref(value: VALUE) -> RbResult<&'static BlockCodec> {
289
+ unsafe { rb::typed_data_ref(value, block_codec_data_type(), "Zrip::BlockCodec") }
290
+ }
291
+
292
+ unsafe fn dict_trainer_ref(value: VALUE) -> RbResult<&'static RbDictTrainer> {
293
+ unsafe { rb::typed_data_ref(value, dict_trainer_data_type(), "Zrip::DictTrainer") }
294
+ }
295
+
172
296
  // ---------- FrameCodec ----------
173
297
 
174
- #[magnus::wrap(class = "Zrip::FrameCodec", free_immediately, size)]
175
298
  struct FrameCodec {
176
299
  dict_len: usize,
177
300
  dict_id: Option<u32>,
@@ -183,198 +306,311 @@ struct FrameCodec {
183
306
  unsafe impl Send for FrameCodec {}
184
307
  unsafe impl Sync for FrameCodec {}
185
308
 
186
- fn frame_codec_new(
187
- ruby: &Ruby,
188
- rb_dict: Option<RString>,
189
- id: u32,
190
- level: i32,
191
- ) -> Result<FrameCodec, Error> {
309
+ fn frame_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -> RbResult<VALUE> {
310
+ let rb_dict = rb::value_to_option_bytes(rb_dict)?;
311
+ let id = rb::value_to_u32(id)?;
312
+ let level = rb::value_to_i32(level)?;
192
313
  let (dict_len, dict_id, cctx, dctx) = match rb_dict {
193
314
  None => {
194
315
  let cctx = CompressContext::new(level).map_err(|e| {
195
- Error::new(
196
- compress_error(ruby),
316
+ RubyErr::new(
317
+ compress_error(),
197
318
  format!("CompressContext::new failed: {e}"),
198
319
  )
199
320
  })?;
200
321
  (0, None, cctx, DecompressContext::new())
201
322
  }
202
- Some(s) => {
203
- let bytes: Vec<u8> = unsafe { s.as_slice().to_vec() };
204
- s.freeze();
205
- let dict = load_dict(ruby, &bytes)?;
323
+ Some(bytes) => {
324
+ let dict = load_dict(&bytes)?;
206
325
  let dl = bytes.len();
207
326
  let cctx = CompressContext::with_dict(level, dict.clone()).map_err(|e| {
208
- Error::new(
209
- compress_error(ruby),
327
+ RubyErr::new(
328
+ compress_error(),
210
329
  format!("CompressContext::with_dict failed: {e}"),
211
330
  )
212
331
  })?;
213
- let dctx = DecompressContext::with_dict(dict);
332
+ let dctx = DecompressContext::with_dict(dict.clone());
214
333
  (dl, Some(id), cctx, dctx)
215
334
  }
216
335
  };
217
336
 
218
- Ok(FrameCodec {
219
- dict_len,
220
- dict_id,
221
- level,
222
- cctx: Mutex::new(cctx),
223
- dctx: Mutex::new(dctx),
224
- })
337
+ let codec = unsafe {
338
+ rb::wrap_typed_data(
339
+ class,
340
+ Box::new(FrameCodec {
341
+ dict_len,
342
+ dict_id,
343
+ level,
344
+ cctx: Mutex::new(cctx),
345
+ dctx: Mutex::new(dctx),
346
+ }),
347
+ frame_codec_data_type(),
348
+ )?
349
+ };
350
+ rb::make_shareable(codec)?;
351
+ Ok(codec)
352
+ }
353
+
354
+ fn frame_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE> {
355
+ let rb_self = unsafe { frame_codec_ref(rb_self)? };
356
+ let mut input = rb::input_bytes(rb_input)?;
357
+ let release_gvl = should_release_compress_gvl(input.len());
358
+ let out = with_mutex(
359
+ &rb_self.cctx,
360
+ release_gvl,
361
+ "FrameCodec CCtx",
362
+ &mut input,
363
+ |cctx, input| {
364
+ cctx.compress(input)
365
+ .map(|out| out.into_owned())
366
+ .map_err(|e| RubyErr::new(compress_error(), format!("zstd compress failed: {e}")))
367
+ },
368
+ )?;
369
+ rb::new_binary_string(&out)
370
+ }
371
+
372
+ fn frame_codec_decompress_impl(
373
+ rb_self: VALUE,
374
+ rb_input: VALUE,
375
+ max_output: VALUE,
376
+ ) -> RbResult<VALUE> {
377
+ let rb_self = unsafe { frame_codec_ref(rb_self)? };
378
+ let max_output = rb::value_to_usize(max_output)?;
379
+ let mut compressed = rb::input_bytes(rb_input)?;
380
+ let release_gvl = should_release_frame_decompress_gvl(decompress_work_size(
381
+ compressed.as_slice(),
382
+ max_output,
383
+ ));
384
+ let out = with_mutex(
385
+ &rb_self.dctx,
386
+ release_gvl,
387
+ "FrameCodec DCtx",
388
+ &mut compressed,
389
+ |dctx, compressed| {
390
+ decompress_bounded(compressed, max_output, dctx)
391
+ .map_err(|e| bounded_err(e, "zstd frame decode failed"))
392
+ },
393
+ )?;
394
+ rb::new_binary_string(&out)
225
395
  }
226
396
 
227
- fn frame_codec_compress(
228
- ruby: &Ruby,
229
- rb_self: &FrameCodec,
230
- rb_input: RString,
231
- ) -> Result<RString, Error> {
232
- let input: &[u8] = unsafe { rb_input.as_slice() };
233
- let mut cctx = rb_self.cctx.lock().expect("FrameCodec CCtx mutex poisoned");
234
- let out = cctx
235
- .compress(input)
236
- .map_err(|e| Error::new(compress_error(ruby), format!("zstd compress failed: {e}")))?;
237
- Ok(ruby.str_from_slice(&out))
397
+ fn frame_codec_size_impl(rb_self: VALUE) -> RbResult<VALUE> {
398
+ let rb_self = unsafe { frame_codec_ref(rb_self)? };
399
+ rb::usize_value(rb_self.dict_len)
238
400
  }
239
401
 
240
- fn frame_codec_decompress(
241
- ruby: &Ruby,
242
- rb_self: &FrameCodec,
243
- rb_input: RString,
244
- max_output: usize,
245
- ) -> Result<RString, Error> {
246
- let compressed: &[u8] = unsafe { rb_input.as_slice() };
247
- let mut dctx = rb_self.dctx.lock().expect("FrameCodec DCtx mutex poisoned");
248
- let out = decompress_bounded(compressed, max_output, &mut dctx)
249
- .map_err(|e| raise_bounded(ruby, e, "zstd frame decode failed"))?;
250
- Ok(ruby.str_from_slice(&out))
402
+ fn frame_codec_has_dict_impl(rb_self: VALUE) -> RbResult<VALUE> {
403
+ let rb_self = unsafe { frame_codec_ref(rb_self)? };
404
+ Ok(rb::bool_value(rb_self.dict_id.is_some()))
251
405
  }
252
406
 
253
- fn frame_codec_size(rb_self: &FrameCodec) -> usize {
254
- rb_self.dict_len
407
+ fn frame_codec_id_impl(rb_self: VALUE) -> RbResult<VALUE> {
408
+ let rb_self = unsafe { frame_codec_ref(rb_self)? };
409
+ rb::u32_option_value(rb_self.dict_id)
255
410
  }
256
411
 
257
- fn frame_codec_has_dict(rb_self: &FrameCodec) -> bool {
258
- rb_self.dict_id.is_some()
412
+ fn frame_codec_level_impl(rb_self: VALUE) -> RbResult<VALUE> {
413
+ let rb_self = unsafe { frame_codec_ref(rb_self)? };
414
+ Ok(rb::i32_value(rb_self.level))
259
415
  }
260
416
 
261
- fn frame_codec_id(rb_self: &FrameCodec) -> Option<u32> {
262
- rb_self.dict_id
417
+ fn frame_codec_get_frame_content_size_impl(rb_input: VALUE) -> RbResult<VALUE> {
418
+ let bytes = rb::input_bytes(rb_input)?;
419
+ match parse_frame_content_size(bytes.as_slice()) {
420
+ Ok(v) => rb::u64_option_value(v),
421
+ Err(BoundedError::BadMagic) => Err(RubyErr::new(
422
+ decompress_error(),
423
+ "zstd frame header parse failed: bad magic (input is not a Zstd frame)",
424
+ )),
425
+ Err(e) => Err(bounded_err(e, "zstd frame header parse failed")),
426
+ }
263
427
  }
264
428
 
265
- fn frame_codec_level(rb_self: &FrameCodec) -> i32 {
266
- rb_self.level
429
+ unsafe extern "C" fn frame_codec_new(
430
+ class: VALUE,
431
+ rb_dict: VALUE,
432
+ id: VALUE,
433
+ level: VALUE,
434
+ ) -> VALUE {
435
+ rb::wrap(|| frame_codec_new_impl(class, rb_dict, id, level))
267
436
  }
268
437
 
269
- fn frame_codec_get_frame_content_size(
270
- ruby: &Ruby,
271
- rb_input: RString,
272
- ) -> Result<Option<u64>, Error> {
273
- let bytes: &[u8] = unsafe { rb_input.as_slice() };
274
- match parse_frame_content_size(bytes) {
275
- Ok(v) => Ok(v),
276
- Err(BoundedError::BadMagic) => Err(Error::new(
277
- decompress_error(ruby),
278
- "zstd frame header parse failed: bad magic (input is not a Zstd frame)",
279
- )),
280
- Err(e) => Err(raise_bounded(ruby, e, "zstd frame header parse failed")),
281
- }
438
+ unsafe extern "C" fn frame_codec_compress(rb_self: VALUE, rb_input: VALUE) -> VALUE {
439
+ rb::wrap(|| frame_codec_compress_impl(rb_self, rb_input))
440
+ }
441
+
442
+ unsafe extern "C" fn frame_codec_decompress(
443
+ rb_self: VALUE,
444
+ rb_input: VALUE,
445
+ max_output: VALUE,
446
+ ) -> VALUE {
447
+ rb::wrap(|| frame_codec_decompress_impl(rb_self, rb_input, max_output))
448
+ }
449
+
450
+ unsafe extern "C" fn frame_codec_size(rb_self: VALUE) -> VALUE {
451
+ rb::wrap(|| frame_codec_size_impl(rb_self))
452
+ }
453
+
454
+ unsafe extern "C" fn frame_codec_has_dict(rb_self: VALUE) -> VALUE {
455
+ rb::wrap(|| frame_codec_has_dict_impl(rb_self))
456
+ }
457
+
458
+ unsafe extern "C" fn frame_codec_id(rb_self: VALUE) -> VALUE {
459
+ rb::wrap(|| frame_codec_id_impl(rb_self))
460
+ }
461
+
462
+ unsafe extern "C" fn frame_codec_level(rb_self: VALUE) -> VALUE {
463
+ rb::wrap(|| frame_codec_level_impl(rb_self))
464
+ }
465
+
466
+ unsafe extern "C" fn frame_codec_get_frame_content_size(_class: VALUE, rb_input: VALUE) -> VALUE {
467
+ rb::wrap(|| frame_codec_get_frame_content_size_impl(rb_input))
282
468
  }
283
469
 
284
470
  // ---------- BlockCodec ----------
285
471
 
286
- #[magnus::wrap(class = "Zrip::BlockCodec", free_immediately, size)]
287
472
  struct BlockCodec {
288
473
  dict_len: usize,
289
474
  dict_id: Option<u32>,
290
475
  level: i32,
291
- cctx: RefCell<CompressContext>,
292
- dctx: RefCell<DecompressContext>,
476
+ cctx: Mutex<CompressContext>,
477
+ dctx: Mutex<DecompressContext>,
293
478
  }
294
479
 
295
- fn block_codec_new(
296
- ruby: &Ruby,
297
- rb_dict: Option<RString>,
298
- id: u32,
299
- level: i32,
300
- ) -> Result<BlockCodec, Error> {
480
+ fn block_codec_new_impl(class: VALUE, rb_dict: VALUE, id: VALUE, level: VALUE) -> RbResult<VALUE> {
481
+ let rb_dict = rb::value_to_option_bytes(rb_dict)?;
482
+ let id = rb::value_to_u32(id)?;
483
+ let level = rb::value_to_i32(level)?;
301
484
  let (dict_len, dict_id, cctx, dctx) = match rb_dict {
302
485
  None => {
303
486
  let cctx = CompressContext::new(level).map_err(|e| {
304
- Error::new(
305
- compress_error(ruby),
487
+ RubyErr::new(
488
+ compress_error(),
306
489
  format!("CompressContext::new failed: {e}"),
307
490
  )
308
491
  })?;
309
492
  (0, None, cctx, DecompressContext::new())
310
493
  }
311
- Some(s) => {
312
- let bytes: Vec<u8> = unsafe { s.as_slice().to_vec() };
313
- let dict = load_dict(ruby, &bytes)?;
494
+ Some(bytes) => {
495
+ let dict = load_dict(&bytes)?;
314
496
  let dl = bytes.len();
315
497
  let cctx = CompressContext::with_dict(level, dict.clone()).map_err(|e| {
316
- Error::new(
317
- compress_error(ruby),
498
+ RubyErr::new(
499
+ compress_error(),
318
500
  format!("CompressContext::with_dict failed: {e}"),
319
501
  )
320
502
  })?;
321
- let dctx = DecompressContext::with_dict(dict);
503
+ let dctx = DecompressContext::with_dict(dict.clone());
322
504
  (dl, Some(id), cctx, dctx)
323
505
  }
324
506
  };
325
507
 
326
- Ok(BlockCodec {
327
- dict_len,
328
- dict_id,
329
- level,
330
- cctx: RefCell::new(cctx),
331
- dctx: RefCell::new(dctx),
332
- })
508
+ unsafe {
509
+ rb::wrap_typed_data(
510
+ class,
511
+ Box::new(BlockCodec {
512
+ dict_len,
513
+ dict_id,
514
+ level,
515
+ cctx: Mutex::new(cctx),
516
+ dctx: Mutex::new(dctx),
517
+ }),
518
+ block_codec_data_type(),
519
+ )
520
+ }
333
521
  }
334
522
 
335
- fn block_codec_compress(
336
- ruby: &Ruby,
337
- rb_self: &BlockCodec,
338
- rb_input: RString,
339
- ) -> Result<RString, Error> {
340
- let input: &[u8] = unsafe { rb_input.as_slice() };
341
- let mut cctx = rb_self.cctx.borrow_mut();
342
- let out = cctx
343
- .compress(input)
344
- .map_err(|e| Error::new(compress_error(ruby), format!("zstd compress failed: {e}")))?;
345
- Ok(ruby.str_from_slice(&out))
523
+ fn block_codec_compress_impl(rb_self: VALUE, rb_input: VALUE) -> RbResult<VALUE> {
524
+ let rb_self = unsafe { block_codec_ref(rb_self)? };
525
+ let mut input = rb::input_bytes(rb_input)?;
526
+ let release_gvl = should_release_compress_gvl(input.len());
527
+ let out = with_mutex(
528
+ &rb_self.cctx,
529
+ release_gvl,
530
+ "BlockCodec CCtx",
531
+ &mut input,
532
+ |cctx, input| {
533
+ cctx.compress(input)
534
+ .map(|out| out.into_owned())
535
+ .map_err(|e| RubyErr::new(compress_error(), format!("zstd compress failed: {e}")))
536
+ },
537
+ )?;
538
+ rb::new_binary_string(&out)
539
+ }
540
+
541
+ fn block_codec_decompress_impl(
542
+ rb_self: VALUE,
543
+ rb_input: VALUE,
544
+ max_output: VALUE,
545
+ ) -> RbResult<VALUE> {
546
+ let rb_self = unsafe { block_codec_ref(rb_self)? };
547
+ let max_output = rb::value_to_usize(max_output)?;
548
+ let mut compressed = rb::input_bytes(rb_input)?;
549
+ let out = with_mutex(
550
+ &rb_self.dctx,
551
+ false,
552
+ "BlockCodec DCtx",
553
+ &mut compressed,
554
+ |dctx, compressed| {
555
+ decompress_bounded(compressed, max_output, dctx)
556
+ .map_err(|e| bounded_err(e, "zstd block decode failed"))
557
+ },
558
+ )?;
559
+ rb::new_binary_string(&out)
346
560
  }
347
561
 
348
- fn block_codec_decompress(
349
- ruby: &Ruby,
350
- rb_self: &BlockCodec,
351
- rb_input: RString,
352
- max_output: usize,
353
- ) -> Result<RString, Error> {
354
- let compressed: &[u8] = unsafe { rb_input.as_slice() };
355
- let mut dctx = rb_self.dctx.borrow_mut();
356
- let out = decompress_bounded(compressed, max_output, &mut dctx)
357
- .map_err(|e| raise_bounded(ruby, e, "zstd block decode failed"))?;
358
- Ok(ruby.str_from_slice(&out))
562
+ fn block_codec_size_impl(rb_self: VALUE) -> RbResult<VALUE> {
563
+ let rb_self = unsafe { block_codec_ref(rb_self)? };
564
+ rb::usize_value(rb_self.dict_len)
565
+ }
566
+
567
+ fn block_codec_has_dict_impl(rb_self: VALUE) -> RbResult<VALUE> {
568
+ let rb_self = unsafe { block_codec_ref(rb_self)? };
569
+ Ok(rb::bool_value(rb_self.dict_id.is_some()))
359
570
  }
360
571
 
361
- fn block_codec_size(rb_self: &BlockCodec) -> usize {
362
- rb_self.dict_len
572
+ fn block_codec_level_impl(rb_self: VALUE) -> RbResult<VALUE> {
573
+ let rb_self = unsafe { block_codec_ref(rb_self)? };
574
+ Ok(rb::i32_value(rb_self.level))
363
575
  }
364
576
 
365
- fn block_codec_has_dict(rb_self: &BlockCodec) -> bool {
366
- rb_self.dict_id.is_some()
577
+ unsafe extern "C" fn block_codec_new(
578
+ class: VALUE,
579
+ rb_dict: VALUE,
580
+ id: VALUE,
581
+ level: VALUE,
582
+ ) -> VALUE {
583
+ rb::wrap(|| block_codec_new_impl(class, rb_dict, id, level))
367
584
  }
368
585
 
369
- fn block_codec_level(rb_self: &BlockCodec) -> i32 {
370
- rb_self.level
586
+ unsafe extern "C" fn block_codec_compress(rb_self: VALUE, rb_input: VALUE) -> VALUE {
587
+ rb::wrap(|| block_codec_compress_impl(rb_self, rb_input))
588
+ }
589
+
590
+ unsafe extern "C" fn block_codec_decompress(
591
+ rb_self: VALUE,
592
+ rb_input: VALUE,
593
+ max_output: VALUE,
594
+ ) -> VALUE {
595
+ rb::wrap(|| block_codec_decompress_impl(rb_self, rb_input, max_output))
596
+ }
597
+
598
+ unsafe extern "C" fn block_codec_size(rb_self: VALUE) -> VALUE {
599
+ rb::wrap(|| block_codec_size_impl(rb_self))
600
+ }
601
+
602
+ unsafe extern "C" fn block_codec_has_dict(rb_self: VALUE) -> VALUE {
603
+ rb::wrap(|| block_codec_has_dict_impl(rb_self))
604
+ }
605
+
606
+ unsafe extern "C" fn block_codec_level(rb_self: VALUE) -> VALUE {
607
+ rb::wrap(|| block_codec_level_impl(rb_self))
371
608
  }
372
609
 
373
610
  // ---------- DictTrainer ----------
374
611
 
375
- #[magnus::wrap(class = "Zrip::DictTrainer", free_immediately, size)]
376
612
  struct RbDictTrainer {
377
- inner: RefCell<Option<TrainerState>>,
613
+ inner: Mutex<Option<TrainerState>>,
378
614
  max_dict_size: usize,
379
615
  }
380
616
 
@@ -383,67 +619,78 @@ struct TrainerState {
383
619
  total_bytes: usize,
384
620
  }
385
621
 
386
- fn dict_trainer_new(_ruby: &Ruby, max_dict_size: usize) -> RbDictTrainer {
387
- RbDictTrainer {
388
- max_dict_size,
389
- inner: RefCell::new(Some(TrainerState {
390
- samples: Vec::new(),
391
- total_bytes: 0,
392
- })),
622
+ fn dict_trainer_new_impl(class: VALUE, max_dict_size: VALUE) -> RbResult<VALUE> {
623
+ let max_dict_size = rb::value_to_usize(max_dict_size)?;
624
+ unsafe {
625
+ rb::wrap_typed_data(
626
+ class,
627
+ Box::new(RbDictTrainer {
628
+ max_dict_size,
629
+ inner: Mutex::new(Some(TrainerState {
630
+ samples: Vec::new(),
631
+ total_bytes: 0,
632
+ })),
633
+ }),
634
+ dict_trainer_data_type(),
635
+ )
393
636
  }
394
637
  }
395
638
 
396
- fn dict_trainer_add_sample(
397
- ruby: &Ruby,
398
- rb_self: &RbDictTrainer,
399
- rb_data: RString,
400
- ) -> Result<(), Error> {
401
- let mut borrow = rb_self.inner.borrow_mut();
402
- let state = borrow.as_mut().ok_or_else(|| {
403
- Error::new(
404
- ruby.exception_runtime_error(),
405
- "DictTrainer already consumed by #train",
406
- )
407
- })?;
408
- let data: Vec<u8> = unsafe { rb_data.as_slice().to_vec() };
639
+ fn dict_trainer_add_sample_impl(rb_self: VALUE, rb_data: VALUE) -> RbResult<VALUE> {
640
+ let data = rb::value_to_bytes(rb_data)?;
641
+ let rb_self = unsafe { dict_trainer_ref(rb_self)? };
642
+ let mut borrow = rb_self
643
+ .inner
644
+ .lock()
645
+ .map_err(|_| RubyErr::runtime("DictTrainer mutex poisoned"))?;
646
+ let state = borrow
647
+ .as_mut()
648
+ .ok_or_else(|| RubyErr::runtime("DictTrainer already consumed by #train"))?;
409
649
  if data.len() < 4 {
410
- return Ok(());
650
+ return Ok(rb::qnil());
411
651
  }
412
652
  state.total_bytes += data.len();
413
653
  state.samples.push(data);
414
- Ok(())
415
- }
416
-
417
- fn dict_trainer_sample_count(ruby: &Ruby, rb_self: &RbDictTrainer) -> Result<usize, Error> {
418
- let borrow = rb_self.inner.borrow();
419
- borrow.as_ref().map(|s| s.samples.len()).ok_or_else(|| {
420
- Error::new(
421
- ruby.exception_runtime_error(),
422
- "DictTrainer already consumed by #train",
423
- )
424
- })
425
- }
426
-
427
- fn dict_trainer_total_bytes(ruby: &Ruby, rb_self: &RbDictTrainer) -> Result<usize, Error> {
428
- let borrow = rb_self.inner.borrow();
429
- borrow.as_ref().map(|s| s.total_bytes).ok_or_else(|| {
430
- Error::new(
431
- ruby.exception_runtime_error(),
432
- "DictTrainer already consumed by #train",
433
- )
434
- })
435
- }
436
-
437
- fn dict_trainer_train(ruby: &Ruby, rb_self: &RbDictTrainer) -> Result<RString, Error> {
438
- let state = rb_self.inner.borrow_mut().take().ok_or_else(|| {
439
- Error::new(
440
- ruby.exception_runtime_error(),
441
- "DictTrainer already consumed by #train",
442
- )
443
- })?;
654
+ Ok(rb::qnil())
655
+ }
656
+
657
+ fn dict_trainer_sample_count_impl(rb_self: VALUE) -> RbResult<VALUE> {
658
+ let rb_self = unsafe { dict_trainer_ref(rb_self)? };
659
+ let borrow = rb_self
660
+ .inner
661
+ .lock()
662
+ .map_err(|_| RubyErr::runtime("DictTrainer mutex poisoned"))?;
663
+ let value = borrow
664
+ .as_ref()
665
+ .map(|s| s.samples.len())
666
+ .ok_or_else(|| RubyErr::runtime("DictTrainer already consumed by #train"))?;
667
+ rb::usize_value(value)
668
+ }
669
+
670
+ fn dict_trainer_total_bytes_impl(rb_self: VALUE) -> RbResult<VALUE> {
671
+ let rb_self = unsafe { dict_trainer_ref(rb_self)? };
672
+ let borrow = rb_self
673
+ .inner
674
+ .lock()
675
+ .map_err(|_| RubyErr::runtime("DictTrainer mutex poisoned"))?;
676
+ let value = borrow
677
+ .as_ref()
678
+ .map(|s| s.total_bytes)
679
+ .ok_or_else(|| RubyErr::runtime("DictTrainer already consumed by #train"))?;
680
+ rb::usize_value(value)
681
+ }
682
+
683
+ fn dict_trainer_train_impl(rb_self: VALUE) -> RbResult<VALUE> {
684
+ let rb_self = unsafe { dict_trainer_ref(rb_self)? };
685
+ let state = rb_self
686
+ .inner
687
+ .lock()
688
+ .map_err(|_| RubyErr::runtime("DictTrainer mutex poisoned"))?
689
+ .take()
690
+ .ok_or_else(|| RubyErr::runtime("DictTrainer already consumed by #train"))?;
444
691
 
445
692
  if state.samples.len() < 2 {
446
- return Ok(ruby.str_from_slice(b""));
693
+ return rb::new_binary_string(b"");
447
694
  }
448
695
 
449
696
  let refs: Vec<&[u8]> = state.samples.iter().map(|s| s.as_slice()).collect();
@@ -456,81 +703,146 @@ fn dict_trainer_train(ruby: &Ruby, rb_self: &RbDictTrainer) -> Result<RString, E
456
703
  let dict_bytes =
457
704
  zstd::dict::finalize::finalize_dictionary(&content, &refs, rb_self.max_dict_size);
458
705
 
459
- Ok(ruby.str_from_slice(&dict_bytes))
706
+ rb::new_binary_string(&dict_bytes)
707
+ }
708
+
709
+ fn dict_trainer_max_dict_size_impl(rb_self: VALUE) -> RbResult<VALUE> {
710
+ let rb_self = unsafe { dict_trainer_ref(rb_self)? };
711
+ rb::usize_value(rb_self.max_dict_size)
712
+ }
713
+
714
+ fn dict_trainer_trained_impl(rb_self: VALUE) -> RbResult<VALUE> {
715
+ let rb_self = unsafe { dict_trainer_ref(rb_self)? };
716
+ let borrow = rb_self
717
+ .inner
718
+ .lock()
719
+ .map_err(|_| RubyErr::runtime("DictTrainer mutex poisoned"))?;
720
+ Ok(rb::bool_value(borrow.is_none()))
721
+ }
722
+
723
+ unsafe extern "C" fn dict_trainer_new(class: VALUE, max_dict_size: VALUE) -> VALUE {
724
+ rb::wrap(|| dict_trainer_new_impl(class, max_dict_size))
725
+ }
726
+
727
+ unsafe extern "C" fn dict_trainer_add_sample(rb_self: VALUE, rb_data: VALUE) -> VALUE {
728
+ rb::wrap(|| dict_trainer_add_sample_impl(rb_self, rb_data))
460
729
  }
461
730
 
462
- fn dict_trainer_max_dict_size(rb_self: &RbDictTrainer) -> usize {
463
- rb_self.max_dict_size
731
+ unsafe extern "C" fn dict_trainer_sample_count(rb_self: VALUE) -> VALUE {
732
+ rb::wrap(|| dict_trainer_sample_count_impl(rb_self))
464
733
  }
465
734
 
466
- fn dict_trainer_trained(rb_self: &RbDictTrainer) -> bool {
467
- rb_self.inner.borrow().is_none()
735
+ unsafe extern "C" fn dict_trainer_total_bytes(rb_self: VALUE) -> VALUE {
736
+ rb::wrap(|| dict_trainer_total_bytes_impl(rb_self))
737
+ }
738
+
739
+ unsafe extern "C" fn dict_trainer_train(rb_self: VALUE) -> VALUE {
740
+ rb::wrap(|| dict_trainer_train_impl(rb_self))
741
+ }
742
+
743
+ unsafe extern "C" fn dict_trainer_max_dict_size(rb_self: VALUE) -> VALUE {
744
+ rb::wrap(|| dict_trainer_max_dict_size_impl(rb_self))
745
+ }
746
+
747
+ unsafe extern "C" fn dict_trainer_trained(rb_self: VALUE) -> VALUE {
748
+ rb::wrap(|| dict_trainer_trained_impl(rb_self))
468
749
  }
469
750
 
470
751
  // ---------- module init ----------
471
752
 
472
- #[magnus::init]
473
- fn init(ruby: &Ruby) -> Result<(), Error> {
474
- unsafe { rb_sys::rb_ext_ractor_safe(true) };
753
+ /// # Safety
754
+ ///
755
+ /// Ruby calls this function while loading the native extension. The Ruby VM
756
+ /// must be initialized, and the symbol must only be entered by Ruby's extension
757
+ /// loader.
758
+ #[no_mangle]
759
+ pub unsafe extern "C" fn Init_zrip() {
760
+ rb::wrap_init(init);
761
+ }
475
762
 
476
- let module = ruby.define_module("Zrip")?;
763
+ fn init() -> RbResult<()> {
764
+ #[cfg(ruby_engine = "mri")]
765
+ unsafe {
766
+ rb_sys::rb_ext_ractor_safe(true);
767
+ }
768
+
769
+ let module = unsafe { rb::define_module(c"Zrip")? };
477
770
 
478
771
  let decompress_error_class =
479
- module.define_error("DecompressError", ruby.exception_standard_error())?;
772
+ unsafe { rb::define_error_under(module, c"DecompressError", rb_sys::rb_eStandardError)? };
480
773
  DECOMPRESS_ERROR
481
- .set(Opaque::from(decompress_error_class))
774
+ .set(GlobalValue(decompress_error_class))
482
775
  .unwrap_or_else(|_| panic!("init called more than once"));
483
776
 
484
777
  let compress_error_class =
485
- module.define_error("CompressError", ruby.exception_standard_error())?;
778
+ unsafe { rb::define_error_under(module, c"CompressError", rb_sys::rb_eStandardError)? };
486
779
  COMPRESS_ERROR
487
- .set(Opaque::from(compress_error_class))
780
+ .set(GlobalValue(compress_error_class))
488
781
  .unwrap_or_else(|_| panic!("init called more than once"));
489
782
 
490
- let missing_content_size_error_class =
491
- module.define_error("MissingContentSizeError", decompress_error_class)?;
783
+ let missing_content_size_error_class = unsafe {
784
+ rb::define_error_under(module, c"MissingContentSizeError", decompress_error_class)?
785
+ };
492
786
  MISSING_CONTENT_SIZE_ERROR
493
- .set(Opaque::from(missing_content_size_error_class))
787
+ .set(GlobalValue(missing_content_size_error_class))
494
788
  .unwrap_or_else(|_| panic!("init called more than once"));
495
789
 
496
790
  let output_size_limit_error_class =
497
- module.define_error("OutputSizeLimitError", decompress_error_class)?;
791
+ unsafe { rb::define_error_under(module, c"OutputSizeLimitError", decompress_error_class)? };
498
792
  OUTPUT_SIZE_LIMIT_ERROR
499
- .set(Opaque::from(output_size_limit_error_class))
793
+ .set(GlobalValue(output_size_limit_error_class))
500
794
  .unwrap_or_else(|_| panic!("init called more than once"));
501
795
 
502
- // FrameCodec
503
- let frame_codec_class = module.define_class("FrameCodec", ruby.class_object())?;
504
- frame_codec_class.define_singleton_method("_native_new", function!(frame_codec_new, 3))?;
505
- frame_codec_class.define_singleton_method(
506
- "get_frame_content_size",
507
- function!(frame_codec_get_frame_content_size, 1),
508
- )?;
509
- frame_codec_class.define_method("compress", method!(frame_codec_compress, 1))?;
510
- frame_codec_class.define_method("_native_decompress", method!(frame_codec_decompress, 2))?;
511
- frame_codec_class.define_method("size", method!(frame_codec_size, 0))?;
512
- frame_codec_class.define_method("has_dict?", method!(frame_codec_has_dict, 0))?;
513
- frame_codec_class.define_method("id", method!(frame_codec_id, 0))?;
514
- frame_codec_class.define_method("level", method!(frame_codec_level, 0))?;
515
-
516
- // BlockCodec
517
- let block_codec_class = module.define_class("BlockCodec", ruby.class_object())?;
518
- block_codec_class.define_singleton_method("_native_new", function!(block_codec_new, 3))?;
519
- block_codec_class.define_method("compress", method!(block_codec_compress, 1))?;
520
- block_codec_class.define_method("_native_decompress", method!(block_codec_decompress, 2))?;
521
- block_codec_class.define_method("size", method!(block_codec_size, 0))?;
522
- block_codec_class.define_method("has_dict?", method!(block_codec_has_dict, 0))?;
523
- block_codec_class.define_method("level", method!(block_codec_level, 0))?;
524
-
525
- // DictTrainer
526
- let trainer_class = module.define_class("DictTrainer", ruby.class_object())?;
527
- trainer_class.define_singleton_method("_native_new", function!(dict_trainer_new, 1))?;
528
- trainer_class.define_method("add_sample", method!(dict_trainer_add_sample, 1))?;
529
- trainer_class.define_method("sample_count", method!(dict_trainer_sample_count, 0))?;
530
- trainer_class.define_method("total_bytes", method!(dict_trainer_total_bytes, 0))?;
531
- trainer_class.define_method("train", method!(dict_trainer_train, 0))?;
532
- trainer_class.define_method("max_dict_size", method!(dict_trainer_max_dict_size, 0))?;
533
- trainer_class.define_method("trained?", method!(dict_trainer_trained, 0))?;
796
+ let frame_codec_class =
797
+ unsafe { rb::define_class_under(module, c"FrameCodec", rb_sys::rb_cObject)? };
798
+ unsafe {
799
+ rb::undef_alloc_func(frame_codec_class)?;
800
+ rb::define_singleton_method_3(frame_codec_class, c"_native_new", frame_codec_new)?;
801
+ rb::define_singleton_method_1(
802
+ frame_codec_class,
803
+ c"get_frame_content_size",
804
+ frame_codec_get_frame_content_size,
805
+ )?;
806
+ rb::define_method_1(frame_codec_class, c"compress", frame_codec_compress)?;
807
+ rb::define_method_2(
808
+ frame_codec_class,
809
+ c"_native_decompress",
810
+ frame_codec_decompress,
811
+ )?;
812
+ rb::define_method_0(frame_codec_class, c"size", frame_codec_size)?;
813
+ rb::define_method_0(frame_codec_class, c"has_dict?", frame_codec_has_dict)?;
814
+ rb::define_method_0(frame_codec_class, c"id", frame_codec_id)?;
815
+ rb::define_method_0(frame_codec_class, c"level", frame_codec_level)?;
816
+ }
817
+
818
+ let block_codec_class =
819
+ unsafe { rb::define_class_under(module, c"BlockCodec", rb_sys::rb_cObject)? };
820
+ unsafe {
821
+ rb::undef_alloc_func(block_codec_class)?;
822
+ rb::define_singleton_method_3(block_codec_class, c"_native_new", block_codec_new)?;
823
+ rb::define_method_1(block_codec_class, c"compress", block_codec_compress)?;
824
+ rb::define_method_2(
825
+ block_codec_class,
826
+ c"_native_decompress",
827
+ block_codec_decompress,
828
+ )?;
829
+ rb::define_method_0(block_codec_class, c"size", block_codec_size)?;
830
+ rb::define_method_0(block_codec_class, c"has_dict?", block_codec_has_dict)?;
831
+ rb::define_method_0(block_codec_class, c"level", block_codec_level)?;
832
+ }
833
+
834
+ let trainer_class =
835
+ unsafe { rb::define_class_under(module, c"DictTrainer", rb_sys::rb_cObject)? };
836
+ unsafe {
837
+ rb::undef_alloc_func(trainer_class)?;
838
+ rb::define_singleton_method_1(trainer_class, c"_native_new", dict_trainer_new)?;
839
+ rb::define_method_1(trainer_class, c"add_sample", dict_trainer_add_sample)?;
840
+ rb::define_method_0(trainer_class, c"sample_count", dict_trainer_sample_count)?;
841
+ rb::define_method_0(trainer_class, c"total_bytes", dict_trainer_total_bytes)?;
842
+ rb::define_method_0(trainer_class, c"train", dict_trainer_train)?;
843
+ rb::define_method_0(trainer_class, c"max_dict_size", dict_trainer_max_dict_size)?;
844
+ rb::define_method_0(trainer_class, c"trained?", dict_trainer_trained)?;
845
+ }
534
846
 
535
847
  Ok(())
536
848
  }
@@ -573,4 +885,22 @@ mod tests {
573
885
  let fcs = parse_frame_content_size(&compressed).unwrap();
574
886
  assert_eq!(fcs, Some(data.len() as u64));
575
887
  }
888
+
889
+ #[test]
890
+ fn bounded_decompress_uses_total_concatenated_limit() {
891
+ let mut dctx = DecompressContext::new();
892
+ let mut compressed = zstd::compress(&[b'a'; 100], 1).unwrap();
893
+ compressed.extend_from_slice(&zstd::compress(&[b'b'; 100], 1).unwrap());
894
+
895
+ assert!(matches!(
896
+ decompress_bounded(&compressed, 199, &mut dctx),
897
+ Err(BoundedError::OutputSizeLimit { .. })
898
+ ));
899
+ assert_eq!(
900
+ decompress_bounded(&compressed, 200, &mut dctx)
901
+ .unwrap()
902
+ .len(),
903
+ 200
904
+ );
905
+ }
576
906
  }