prosody 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.cargo/config.toml +2 -0
  3. data/.release-please-manifest.json +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.standard.yml +9 -0
  7. data/.taplo.toml +6 -0
  8. data/ARCHITECTURE.md +591 -0
  9. data/CHANGELOG.md +92 -0
  10. data/Cargo.lock +3513 -0
  11. data/Cargo.toml +77 -0
  12. data/LICENSE +21 -0
  13. data/Makefile +36 -0
  14. data/README.md +946 -0
  15. data/Rakefile +26 -0
  16. data/ext/prosody/Cargo.toml +38 -0
  17. data/ext/prosody/extconf.rb +6 -0
  18. data/ext/prosody/src/admin.rs +171 -0
  19. data/ext/prosody/src/bridge/callback.rs +60 -0
  20. data/ext/prosody/src/bridge/mod.rs +332 -0
  21. data/ext/prosody/src/client/config.rs +819 -0
  22. data/ext/prosody/src/client/mod.rs +379 -0
  23. data/ext/prosody/src/gvl.rs +149 -0
  24. data/ext/prosody/src/handler/context.rs +436 -0
  25. data/ext/prosody/src/handler/message.rs +144 -0
  26. data/ext/prosody/src/handler/mod.rs +338 -0
  27. data/ext/prosody/src/handler/trigger.rs +93 -0
  28. data/ext/prosody/src/lib.rs +82 -0
  29. data/ext/prosody/src/logging.rs +353 -0
  30. data/ext/prosody/src/scheduler/cancellation.rs +67 -0
  31. data/ext/prosody/src/scheduler/handle.rs +50 -0
  32. data/ext/prosody/src/scheduler/mod.rs +169 -0
  33. data/ext/prosody/src/scheduler/processor.rs +166 -0
  34. data/ext/prosody/src/scheduler/result.rs +197 -0
  35. data/ext/prosody/src/tracing_util.rs +56 -0
  36. data/ext/prosody/src/util.rs +219 -0
  37. data/lib/prosody/configuration.rb +333 -0
  38. data/lib/prosody/handler.rb +177 -0
  39. data/lib/prosody/native_stubs.rb +417 -0
  40. data/lib/prosody/processor.rb +321 -0
  41. data/lib/prosody/sentry.rb +36 -0
  42. data/lib/prosody/version.rb +10 -0
  43. data/lib/prosody.rb +42 -0
  44. data/release-please-config.json +10 -0
  45. data/sig/configuration.rbs +252 -0
  46. data/sig/handler.rbs +79 -0
  47. data/sig/processor.rbs +100 -0
  48. data/sig/prosody.rbs +171 -0
  49. data/sig/version.rbs +9 -0
  50. metadata +193 -0
@@ -0,0 +1,379 @@
1
+ //! # Client Module
2
+ //!
3
+ //! Provides the Ruby interface to the Prosody messaging system. This module
4
+ //! defines the `Client` class that allows Ruby applications to send messages
5
+ //! and process events from Kafka topics using the Prosody library.
6
+ //!
7
+ //! The client supports:
8
+ //! - Sending messages to Kafka topics
9
+ //! - Subscribing to topics with Ruby handler objects
10
+ //! - OpenTelemetry context propagation for distributed tracing
11
+ //! - Different operation modes (`Pipeline`, `LowLatency`, `BestEffort`)
12
+
13
+ use crate::bridge::Bridge;
14
+ use crate::client::config::NativeConfiguration;
15
+ use crate::handler::RubyHandler;
16
+ use crate::tracing_util::extract_opentelemetry_context;
17
+ use crate::util::ensure_runtime_context;
18
+ use crate::{BRIDGE, ROOT_MOD, id};
19
+ use magnus::value::ReprValue;
20
+ use magnus::{Error, Module, Object, RClass, Ruby, StaticSymbol, Value, function, method};
21
+ use opentelemetry::propagation::TextMapCompositePropagator;
22
+ use prosody::high_level::ConsumerBuilders;
23
+ use prosody::high_level::HighLevelClient;
24
+ use prosody::high_level::mode::Mode;
25
+ use prosody::high_level::state::ConsumerState;
26
+ use prosody::propagator::new_propagator;
27
+ use serde_magnus::deserialize;
28
+ use std::sync::Arc;
29
+ use tracing::{Span, debug, info_span};
30
+ use tracing_opentelemetry::OpenTelemetrySpanExt;
31
+
32
+ /// Configuration types and conversion between Ruby and Rust representations
33
+ mod config;
34
+
35
+ /// A Ruby-compatible wrapper around the Prosody high-level client.
36
+ ///
37
+ /// This struct bridges Ruby applications with the Prosody messaging system,
38
+ /// providing methods for sending messages to Kafka topics and subscribing to
39
+ /// events with Ruby handlers.
40
+ #[derive(Debug)]
41
+ #[magnus::wrap(class = "Prosody::Client")]
42
+ pub struct Client {
43
+ /// The underlying Prosody client
44
+ inner: Arc<HighLevelClient<RubyHandler>>,
45
+ /// Bridge for communicating between Rust and Ruby
46
+ bridge: Bridge,
47
+ /// OpenTelemetry propagator for distributed tracing
48
+ propagator: TextMapCompositePropagator,
49
+ /// PID at construction time, used to detect post-fork usage
50
+ pid: u32,
51
+ }
52
+
53
+ impl Client {
54
+ /// Creates a new Prosody client with the given configuration.
55
+ ///
56
+ /// # Arguments
57
+ ///
58
+ /// * `ruby` - The Ruby VM context
59
+ /// * `config` - A Ruby Configuration object or hash containing client
60
+ /// configuration options
61
+ ///
62
+ /// # Errors
63
+ ///
64
+ /// Returns an error if:
65
+ /// - The OpenTelemetry API gem cannot be loaded
66
+ /// - The configuration mode is invalid
67
+ /// - The client cannot be initialized with the given configuration
68
+ /// - The bridge is not initialized
69
+ fn new(ruby: &Ruby, config: Value) -> Result<Self, Error> {
70
+ ruby.require("opentelemetry-api")?;
71
+
72
+ let _guard = ensure_runtime_context(ruby);
73
+
74
+ // Check if config is already a Configuration object, if not create one
75
+ let config_class: RClass = ruby
76
+ .get_inner(&ROOT_MOD)
77
+ .const_get(id!(ruby, "Configuration"))?;
78
+ let config_obj = if config.is_kind_of(config_class) {
79
+ config
80
+ } else {
81
+ config_class.funcall(id!(ruby, "new"), (config,))?
82
+ };
83
+
84
+ let config_hash: Value = config_obj.funcall(id!(ruby, "to_hash"), ())?;
85
+ let native_config = NativeConfiguration::from_value(ruby, config_hash)?;
86
+ let config_ref = &native_config;
87
+
88
+ let mode: Mode = config_ref
89
+ .try_into()
90
+ .map_err(|error: String| Error::new(ruby.exception_arg_error(), error))?;
91
+
92
+ let consumer_builders: ConsumerBuilders = config_ref
93
+ .try_into()
94
+ .map_err(|error: String| Error::new(ruby.exception_arg_error(), error))?;
95
+
96
+ let client = HighLevelClient::new(
97
+ mode,
98
+ &mut config_ref.into(),
99
+ &consumer_builders,
100
+ &config_ref.into(),
101
+ )
102
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
103
+
104
+ let bridge = BRIDGE
105
+ .get()
106
+ .ok_or(Error::new(
107
+ ruby.exception_runtime_error(),
108
+ "Bridge not initialized",
109
+ ))?
110
+ .clone();
111
+
112
+ Ok(Self {
113
+ inner: Arc::new(client),
114
+ bridge: bridge.clone(),
115
+ propagator: new_propagator(),
116
+ pid: std::process::id(),
117
+ })
118
+ }
119
+
120
+ fn check_fork(ruby: &Ruby, this: &Self) -> Result<(), Error> {
121
+ if std::process::id() != this.pid {
122
+ return Err(Error::new(
123
+ ruby.exception_runtime_error(),
124
+ "Prosody::Client cannot be used after fork. Create a new client in the child \
125
+ process.",
126
+ ));
127
+ }
128
+ Ok(())
129
+ }
130
+
131
+ /// Returns the current state of the consumer.
132
+ ///
133
+ /// The consumer can be in one of three states:
134
+ /// - `:unconfigured` - The consumer has not been configured yet
135
+ /// - `:configured` - The consumer is configured but not running
136
+ /// - `:running` - The consumer is actively consuming messages
137
+ ///
138
+ /// # Arguments
139
+ ///
140
+ /// * `ruby` - The Ruby VM context
141
+ /// * `this` - The client instance
142
+ ///
143
+ /// # Returns
144
+ ///
145
+ /// A Ruby symbol representing the current consumer state.
146
+ ///
147
+ /// # Errors
148
+ ///
149
+ /// Raises `RuntimeError` if the consumer configuration failed during
150
+ /// build, with the full error message from the underlying
151
+ /// `ModeConfigurationError`.
152
+ pub fn consumer_state(ruby: &Ruby, this: &Self) -> Result<StaticSymbol, Error> {
153
+ Self::check_fork(ruby, this)?;
154
+ let inner = this.inner.clone();
155
+ let state: Result<&'static str, String> = this.bridge.wait_for(
156
+ ruby,
157
+ async move {
158
+ let view = inner.consumer_state().await;
159
+ match &*view {
160
+ ConsumerState::Unconfigured => Ok("unconfigured"),
161
+ ConsumerState::ConfigurationFailed(err) => {
162
+ Err(format!("consumer configuration failed: {err:#}"))
163
+ }
164
+ ConsumerState::Configured(_) => Ok("configured"),
165
+ ConsumerState::Running { .. } => Ok("running"),
166
+ }
167
+ },
168
+ Span::current(),
169
+ )?;
170
+
171
+ let state = state.map_err(|msg| Error::new(ruby.exception_runtime_error(), msg))?;
172
+
173
+ Ok(ruby.sym_new(state))
174
+ }
175
+
176
+ /// Sends a message to the specified Kafka topic.
177
+ ///
178
+ /// # Arguments
179
+ ///
180
+ /// * `ruby` - The Ruby VM context
181
+ /// * `this` - The client instance
182
+ /// * `topic` - The destination topic name
183
+ /// * `key` - The message key for partitioning
184
+ /// * `payload` - The message payload (will be serialized)
185
+ ///
186
+ /// # Errors
187
+ ///
188
+ /// Returns an error if:
189
+ /// - The payload cannot be deserialized
190
+ /// - OpenTelemetry context extraction fails
191
+ /// - The message cannot be sent to Kafka
192
+ fn send(
193
+ ruby: &Ruby,
194
+ this: &Self,
195
+ topic: String,
196
+ key: String,
197
+ payload: Value,
198
+ ) -> Result<(), Error> {
199
+ Self::check_fork(ruby, this)?;
200
+ let _guard = ensure_runtime_context(ruby);
201
+ let client = this.inner.clone();
202
+ let value = deserialize(ruby, payload)?;
203
+ let context = extract_opentelemetry_context(ruby, &this.propagator)?;
204
+
205
+ // Create span for tracing and set parent context
206
+ let span = info_span!("ruby-send", %topic, %key);
207
+ if let Err(err) = span.set_parent(context) {
208
+ debug!("failed to set parent span: {err:#}");
209
+ }
210
+
211
+ // Wait for the async send operation to complete
212
+ this.bridge
213
+ .wait_for(
214
+ ruby,
215
+ async move { client.send(topic.as_str().into(), &key, &value).await },
216
+ span,
217
+ )?
218
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
219
+ }
220
+
221
+ /// Subscribes to events using the provided Ruby handler.
222
+ ///
223
+ /// The handler must implement an `on_message(context, message)` method
224
+ /// that will be called for each received message.
225
+ ///
226
+ /// # Arguments
227
+ ///
228
+ /// * `ruby` - The Ruby VM context
229
+ /// * `this` - The client instance
230
+ /// * `handler` - A Ruby object that will handle incoming messages
231
+ ///
232
+ /// # Errors
233
+ ///
234
+ /// Returns an error if:
235
+ /// - The handler cannot be wrapped
236
+ /// - The client cannot subscribe with the handler
237
+ fn subscribe(ruby: &Ruby, this: &Self, handler: Value) -> Result<(), Error> {
238
+ Self::check_fork(ruby, this)?;
239
+ let _guard = ensure_runtime_context(ruby);
240
+ let wrapper = RubyHandler::new(this.bridge.clone(), ruby, handler)?;
241
+ let inner = this.inner.clone();
242
+
243
+ this.bridge
244
+ .wait_for(
245
+ ruby,
246
+ async move { inner.subscribe(wrapper).await },
247
+ Span::current(),
248
+ )?
249
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
250
+
251
+ Ok(())
252
+ }
253
+
254
+ /// Returns the number of Kafka partitions currently assigned to this
255
+ /// consumer.
256
+ ///
257
+ /// This method can be used to monitor the consumer's workload and ensure
258
+ /// proper load balancing across multiple consumer instances.
259
+ ///
260
+ /// # Arguments
261
+ ///
262
+ /// * `self` - The client instance
263
+ ///
264
+ /// # Returns
265
+ ///
266
+ /// The number of assigned partitions as a u32.
267
+ pub fn assigned_partitions(ruby: &Ruby, this: &Self) -> Result<u32, Error> {
268
+ Self::check_fork(ruby, this)?;
269
+ let inner = this.inner.clone();
270
+ this.bridge.wait_for(
271
+ ruby,
272
+ async move { inner.assigned_partition_count().await },
273
+ Span::current(),
274
+ )
275
+ }
276
+
277
+ /// Checks if the consumer is stalled.
278
+ ///
279
+ /// A stalled consumer is one that has stopped processing messages due to
280
+ /// errors or reaching processing limits. This can be used to detect
281
+ /// unhealthy consumers that need attention.
282
+ ///
283
+ /// # Arguments
284
+ ///
285
+ /// * `self` - The client instance
286
+ ///
287
+ /// # Returns
288
+ ///
289
+ /// `true` if the consumer is stalled, `false` otherwise.
290
+ pub fn is_stalled(ruby: &Ruby, this: &Self) -> Result<bool, Error> {
291
+ Self::check_fork(ruby, this)?;
292
+ let inner = this.inner.clone();
293
+ this.bridge.wait_for(
294
+ ruby,
295
+ async move { inner.is_stalled().await },
296
+ Span::current(),
297
+ )
298
+ }
299
+
300
+ /// Unsubscribes from all topics, stopping message processing.
301
+ ///
302
+ /// This method gracefully shuts down the consumer, completing any in-flight
303
+ /// messages before stopping.
304
+ ///
305
+ /// # Arguments
306
+ ///
307
+ /// * `ruby` - The Ruby VM context
308
+ /// * `this` - The client instance
309
+ ///
310
+ /// # Errors
311
+ ///
312
+ /// Returns an error if the unsubscribe operation fails.
313
+ fn unsubscribe(ruby: &Ruby, this: &Self) -> Result<(), Error> {
314
+ Self::check_fork(ruby, this)?;
315
+ let _guard = ensure_runtime_context(ruby);
316
+ let client = this.inner.clone();
317
+
318
+ this.bridge
319
+ .wait_for(
320
+ ruby,
321
+ async move { client.unsubscribe().await },
322
+ Span::current(),
323
+ )?
324
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
325
+ }
326
+
327
+ /// Returns the configured source system identifier.
328
+ ///
329
+ /// The source system is used to identify the originating service or
330
+ /// component in produced messages, enabling loop detection.
331
+ ///
332
+ /// # Arguments
333
+ ///
334
+ /// * `this` - The client instance
335
+ ///
336
+ /// # Returns
337
+ ///
338
+ /// The source system identifier.
339
+ fn source_system(this: &Self) -> &str {
340
+ this.inner.source_system()
341
+ }
342
+ }
343
+
344
+ /// Initializes the client module in Ruby.
345
+ ///
346
+ /// Defines the `Prosody::Client` class and its methods, making the client
347
+ /// functionality available to Ruby code.
348
+ ///
349
+ /// # Arguments
350
+ ///
351
+ /// * `ruby` - The Ruby VM context
352
+ ///
353
+ /// # Errors
354
+ ///
355
+ /// Returns an error if Ruby class or method definition fails.
356
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
357
+ let module = ruby.get_inner(&ROOT_MOD);
358
+ let class = module.define_class(id!(ruby, "Client"), ruby.class_object())?;
359
+
360
+ class.define_singleton_method("new", function!(Client::new, 1))?;
361
+ class.define_method(
362
+ id!(ruby, "consumer_state"),
363
+ method!(Client::consumer_state, 0),
364
+ )?;
365
+ class.define_method(id!(ruby, "send_message"), method!(Client::send, 3))?;
366
+ class.define_method(id!(ruby, "subscribe"), method!(Client::subscribe, 1))?;
367
+ class.define_method(
368
+ id!(ruby, "assigned_partitions"),
369
+ method!(Client::assigned_partitions, 0),
370
+ )?;
371
+ class.define_method(id!(ruby, "is_stalled?"), method!(Client::is_stalled, 0))?;
372
+ class.define_method(id!(ruby, "unsubscribe"), method!(Client::unsubscribe, 0))?;
373
+ class.define_method(
374
+ id!(ruby, "source_system"),
375
+ method!(Client::source_system, 0),
376
+ )?;
377
+
378
+ Ok(())
379
+ }
@@ -0,0 +1,149 @@
1
+ use rb_sys::rb_thread_call_without_gvl;
2
+ use std::ffi::c_void;
3
+ use std::panic::{AssertUnwindSafe, catch_unwind};
4
+ use thiserror::Error;
5
+ use tracing::error;
6
+
7
+ /// Executes the given closure `func` without holding Ruby’s Global VM Lock
8
+ /// (GVL), allowing long-running or blocking Rust code to run without blocking
9
+ /// Ruby’s threads.
10
+ ///
11
+ /// # Arguments
12
+ ///
13
+ /// - `func`: A closure that performs the work without the GVL and returns a
14
+ /// value of type `R`. Called **exactly once** by Ruby.
15
+ /// - `unblock`: A closure that Ruby will call to "unblock" the thread if needed
16
+ /// while `func` is running. Ruby **may** call this multiple times, depending
17
+ /// on signals, etc.
18
+ ///
19
+ /// # Returns
20
+ ///
21
+ /// The result of `func`.
22
+ ///
23
+ /// # Panics
24
+ ///
25
+ /// - If either `func` or `unblock` panics, that panic is caught and the process
26
+ /// is aborted. This is necessary to prevent unwinding across the C FFI
27
+ /// boundary, which is undefined behavior.
28
+ ///
29
+ /// # Overview
30
+ ///
31
+ /// 1. We box both closures (`func` and `unblock`) and convert them into raw
32
+ /// pointers via `Box::into_raw`.
33
+ /// 2. We define two `unsafe extern "C"` callbacks (`anon_func` and
34
+ /// `anon_unblock`), which:
35
+ /// - `anon_func` (for `func`) *consumes* its Box exactly once via
36
+ /// `Box::from_raw`, then calls it inside `catch_unwind`.
37
+ /// - `anon_unblock` (for `unblock`) does **not** consume its Box. Instead,
38
+ /// it just uses a pointer to call `unblock` each time Ruby needs it.
39
+ /// Because Ruby may call it multiple times, we only do `Box::from_raw` on
40
+ /// this pointer **after** `rb_thread_call_without_gvl` returns.
41
+ /// 3. We wrap calls in `catch_unwind` + `abort()` so that no panic can cross
42
+ /// the C boundary.
43
+ /// 4. Once `rb_thread_call_without_gvl` returns, we do a single
44
+ /// `Box::from_raw(unblock_ptr)`, ensuring no memory leaks. We also retrieve
45
+ /// the result from `anon_func` via `Box::from_raw` exactly once.
46
+ ///
47
+ /// # Safety
48
+ ///
49
+ /// - We must ensure each pointer from `Box::into_raw` is only reconstructed
50
+ /// exactly once via `Box::from_raw`.
51
+ /// - `anon_unblock` does not free the pointer on each call, thus avoiding
52
+ /// double-free if Ruby calls it multiple times.
53
+ /// - `catch_unwind` and `abort()` ensure that we never unwind through C code.
54
+ ///
55
+ /// Adapted from: <https://github.com/temporalio/sdk-ruby/blob/main/temporalio/ext/src/util.rs>,
56
+ /// plus various examples in the Rust/Ruby ecosystem.
57
+ #[allow(unsafe_code)]
58
+ pub(crate) fn without_gvl<F, R, U>(func: F, unblock: U) -> Result<R, GvlError>
59
+ where
60
+ F: FnOnce() -> R,
61
+ U: FnMut() + Send,
62
+ {
63
+ // SAFETY: This is an FFI callback, called exactly once by Ruby. We:
64
+ // - Reconstruct the Box<F> from `data`.
65
+ // - Catch any panic to avoid unwinding across the C boundary.
66
+ // - Return a pointer to a boxed result or abort on panic.
67
+ unsafe extern "C" fn anon_func<F, R>(data: *mut c_void) -> *mut c_void
68
+ where
69
+ F: FnOnce() -> R,
70
+ {
71
+ // SAFETY: `data` is guaranteed to come from `Box::into_raw(Box<F>)`.
72
+ // We reconstruct exactly once, transferring ownership back into Rust.
73
+ let func: Box<F> = unsafe { Box::from_raw(data.cast()) };
74
+
75
+ // Catch unwind so we don't panic across FFI.
76
+ let result = catch_unwind(AssertUnwindSafe(|| (*func)())).map_err(|_| GvlError::Panicked);
77
+
78
+ // SAFETY: We box the result, returning a raw pointer. The caller
79
+ // will Box::from_raw it exactly once after `rb_thread_call_without_gvl`.
80
+ Box::into_raw(Box::new(result)).cast::<c_void>()
81
+ }
82
+
83
+ // SAFETY: Another FFI callback. Ruby may call this multiple times, so we do
84
+ // NOT call `Box::from_raw` here. Instead, we just dereference the pointer
85
+ // each time. We also catch any panic and abort to avoid unwinding across FFI.
86
+ unsafe extern "C" fn anon_unblock<U>(data: *mut c_void)
87
+ where
88
+ U: FnMut() + Send,
89
+ {
90
+ // We take a pointer to `U`; we do NOT consume the Box here.
91
+ // Note that `&mut *ptr` is not automatically UnwindSafe, so we wrap the call
92
+ // in `AssertUnwindSafe`.
93
+ let closure_ptr = data.cast::<U>();
94
+
95
+ if catch_unwind(AssertUnwindSafe(|| {
96
+ // SAFETY: closure_ptr was allocated via `Box::into_raw`. We only
97
+ // borrow it here, not freeing it. That’s safe for multiple calls.
98
+ unsafe { (*closure_ptr)() };
99
+ }))
100
+ .is_err()
101
+ {
102
+ error!("panicked while attempting to unblock a function running outside of the GVL");
103
+ }
104
+ }
105
+
106
+ // Box up both closures. We'll consume `func` exactly once in `anon_func`,
107
+ // and we'll only free `unblock` after `rb_thread_call_without_gvl` returns.
108
+ let boxed_func = Box::new(func);
109
+ let boxed_unblock = Box::new(unblock);
110
+
111
+ // Convert to raw pointers for FFI.
112
+ let func_ptr = Box::into_raw(boxed_func);
113
+ let unblock_ptr = Box::into_raw(boxed_unblock);
114
+
115
+ // SAFETY: Passing valid function pointers and data pointers to
116
+ // `rb_thread_call_without_gvl`. Ruby will:
117
+ // - Call `anon_func` once, passing `func_ptr`.
118
+ // - Potentially call `anon_unblock` multiple times with `unblock_ptr`.
119
+ // After `rb_thread_call_without_gvl` returns, we are guaranteed that Ruby
120
+ // won't call `anon_unblock` again, so we can safely free the unblock closure.
121
+ let raw_result = unsafe {
122
+ rb_thread_call_without_gvl(
123
+ Some(anon_func::<F, R>),
124
+ func_ptr.cast(),
125
+ Some(anon_unblock::<U>),
126
+ unblock_ptr.cast(),
127
+ )
128
+ };
129
+
130
+ // SAFETY: This pointer came from `anon_func`, which did
131
+ // `Box::into_raw(Box::new(val))`. We reconstruct it here exactly once to
132
+ // get the result.
133
+ let result = unsafe { *Box::from_raw(raw_result.cast()) };
134
+
135
+ // SAFETY: Now that `rb_thread_call_without_gvl` has returned, Ruby will no
136
+ // longer call our `anon_unblock` callback. Thus, it's safe to free the
137
+ // `unblock` closure exactly once here.
138
+ unsafe {
139
+ let _ = Box::from_raw(unblock_ptr);
140
+ }
141
+
142
+ result
143
+ }
144
+
145
+ #[derive(Debug, Error)]
146
+ pub enum GvlError {
147
+ #[error("panicked while attempting to call function running outside of the GVL")]
148
+ Panicked,
149
+ }