Skip to content

Commit 3cbfbc6

Browse files
feat: add panic guards for FFI entry points (#27)
- Wrap every exported FFI function in catch_unwind so a panic in the Rust engine turns into an error response instead of tearing down the JVM. - Add a Panic response code and propagate friendly messaging to help SDKs surface the failure.
1 parent 1a72890 commit 3cbfbc6

File tree

1 file changed

+34
-18
lines changed

1 file changed

+34
-18
lines changed

yggdrasilffi/src/lib.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{
33
ffi::{c_char, c_void, CStr, CString},
44
fmt::{self, Display, Formatter},
55
mem::forget,
6+
panic::{self, AssertUnwindSafe},
67
str::Utf8Error,
78
sync::{Arc, Mutex, MutexGuard},
89
};
@@ -64,6 +65,7 @@ enum FFIError {
6465
Utf8Error,
6566
NullError,
6667
InvalidJson(String),
68+
Panic,
6769
PartialUpdate(Vec<EvalWarning>),
6870
}
6971

@@ -78,6 +80,10 @@ impl Display for FFIError {
7880
"Engine state was updated but warnings were reported, this may result in some flags evaluating in unexpected ways, please report this: {:?}",
7981
messages
8082
),
83+
FFIError::Panic => write!(
84+
f,
85+
"Engine panicked while processing the request. Please report this as a bug with the accompanying stack trace if available."
86+
),
8187
}
8288
}
8389
}
@@ -113,6 +119,16 @@ fn result_to_json_ptr<T: Serialize>(result: Result<Option<T>, FFIError>) -> *mut
113119
CString::new(json_string).unwrap().into_raw()
114120
}
115121

122+
fn guard_result<T, F>(action: F) -> Result<Option<T>, FFIError>
123+
where
124+
F: FnOnce() -> Result<Option<T>, FFIError>,
125+
{
126+
match panic::catch_unwind(AssertUnwindSafe(action)) {
127+
Ok(result) => result,
128+
Err(_) => Err(FFIError::Panic),
129+
}
130+
}
131+
116132
unsafe fn get_engine(engine_ptr: *mut c_void) -> Result<ManagedEngine, FFIError> {
117133
if engine_ptr.is_null() {
118134
return Err(FFIError::NullError);
@@ -178,7 +194,7 @@ pub unsafe extern "C" fn take_state(
178194
engine_ptr: *mut c_void,
179195
json_ptr: *const c_char,
180196
) -> *const c_char {
181-
let result: Result<Option<()>, FFIError> = (|| {
197+
let result = guard_result::<(), _>(|| {
182198
let guard = get_engine(engine_ptr)?;
183199
let mut engine = recover_lock(&guard);
184200

@@ -189,7 +205,7 @@ pub unsafe extern "C" fn take_state(
189205
} else {
190206
Ok(Some(()))
191207
}
192-
})();
208+
});
193209

194210
result_to_json_ptr(result)
195211
}
@@ -202,11 +218,11 @@ pub unsafe extern "C" fn take_state(
202218
/// be called with a null pointer.
203219
#[no_mangle]
204220
pub unsafe extern "C" fn get_state(engine_ptr: *mut c_void) -> *const c_char {
205-
let result: Result<Option<ClientFeatures>, FFIError> = (|| {
221+
let result = guard_result::<ClientFeatures, _>(|| {
206222
let guard = get_engine(engine_ptr)?;
207223
let engine = recover_lock(&guard);
208224
Ok(Some(engine.get_state()))
209-
})();
225+
});
210226

211227
result_to_json_ptr(result)
212228
}
@@ -229,7 +245,7 @@ pub unsafe extern "C" fn check_enabled(
229245
context_ptr: *const c_char,
230246
custom_strategy_results_ptr: *const c_char,
231247
) -> *const c_char {
232-
let result: Result<Option<bool>, FFIError> = (|| {
248+
let result = guard_result::<bool, _>(|| {
233249
let guard = get_engine(engine_ptr)?;
234250
let engine = recover_lock(&guard);
235251

@@ -241,7 +257,7 @@ pub unsafe extern "C" fn check_enabled(
241257
EnrichedContext::from(context, toggle_name.into(), Some(custom_strategy_results));
242258

243259
Ok(engine.check_enabled(&enriched_context))
244-
})();
260+
});
245261

246262
result_to_json_ptr(result)
247263
}
@@ -264,7 +280,7 @@ pub unsafe extern "C" fn check_variant(
264280
context_ptr: *const c_char,
265281
custom_strategy_results_ptr: *const c_char,
266282
) -> *const c_char {
267-
let result: Result<Option<ExtendedVariantDef>, FFIError> = (|| {
283+
let result = guard_result::<ExtendedVariantDef, _>(|| {
268284
let guard = get_engine(engine_ptr)?;
269285
let engine = recover_lock(&guard);
270286

@@ -278,7 +294,7 @@ pub unsafe extern "C" fn check_variant(
278294
let base_variant = engine.check_variant(&enriched_context);
279295
let toggle_enabled = engine.check_enabled(&enriched_context).unwrap_or_default();
280296
Ok(base_variant.map(|variant| variant.to_enriched_response(toggle_enabled)))
281-
})();
297+
});
282298

283299
result_to_json_ptr(result)
284300
}
@@ -347,15 +363,15 @@ pub unsafe extern "C" fn count_toggle(
347363

348364
let enabled = enabled & 1 == 1;
349365

350-
let result: Result<Option<()>, FFIError> = (|| {
366+
let result = guard_result::<(), _>(|| {
351367
let guard = get_engine(engine_ptr)?;
352368
let engine = recover_lock(&guard);
353369

354370
let toggle_name = get_str(toggle_name_ptr)?;
355371

356372
engine.count_toggle(toggle_name, enabled);
357373
Ok(Some(()))
358-
})();
374+
});
359375

360376
result_to_json_ptr(result)
361377
}
@@ -378,7 +394,7 @@ pub unsafe extern "C" fn count_variant(
378394
toggle_name_ptr: *const c_char,
379395
variant_name_ptr: *const c_char,
380396
) -> *const c_char {
381-
let result: Result<Option<()>, FFIError> = (|| {
397+
let result = guard_result::<(), _>(|| {
382398
let guard = get_engine(engine_ptr)?;
383399
let engine = recover_lock(&guard);
384400

@@ -387,7 +403,7 @@ pub unsafe extern "C" fn count_variant(
387403

388404
engine.count_variant(toggle_name, variant_name);
389405
Ok(Some(()))
390-
})();
406+
});
391407

392408
result_to_json_ptr(result)
393409
}
@@ -406,12 +422,12 @@ pub unsafe extern "C" fn count_variant(
406422
/// `free_response` and passing in the pointer returned by this method. Failure to do so will result in a leak.
407423
#[no_mangle]
408424
pub unsafe extern "C" fn get_metrics(engine_ptr: *mut c_void) -> *mut c_char {
409-
let result: Result<Option<MetricBucket>, FFIError> = (|| {
425+
let result = guard_result::<MetricBucket, _>(|| {
410426
let guard = get_engine(engine_ptr)?;
411427
let mut engine = recover_lock(&guard);
412428

413429
Ok(engine.get_metrics(Utc::now()))
414-
})();
430+
});
415431

416432
result_to_json_ptr(result)
417433
}
@@ -428,14 +444,14 @@ pub unsafe extern "C" fn should_emit_impression_event(
428444
engine_ptr: *mut c_void,
429445
toggle_name_ptr: *const c_char,
430446
) -> *mut c_char {
431-
let result: Result<Option<bool>, FFIError> = (|| {
447+
let result = guard_result::<bool, _>(|| {
432448
let guard = get_engine(engine_ptr)?;
433449
let engine = recover_lock(&guard);
434450

435451
let toggle_name = get_str(toggle_name_ptr)?;
436452

437453
Ok(Some(engine.should_emit_impression_event(toggle_name)))
438-
})();
454+
});
439455

440456
result_to_json_ptr(result)
441457
}
@@ -453,12 +469,12 @@ pub unsafe extern "C" fn should_emit_impression_event(
453469
/// `free_response` and passing in the pointer returned by this method. Failure to do so will result in a leak.
454470
#[no_mangle]
455471
pub unsafe extern "C" fn list_known_toggles(engine_ptr: *mut c_void) -> *mut c_char {
456-
let result: Result<Option<Vec<ToggleDefinition>>, FFIError> = (|| {
472+
let result = guard_result::<Vec<ToggleDefinition>, _>(|| {
457473
let guard = get_engine(engine_ptr)?;
458474
let engine = recover_lock(&guard);
459475

460476
Ok(Some(engine.list_known_toggles()))
461-
})();
477+
});
462478

463479
result_to_json_ptr(result)
464480
}

0 commit comments

Comments
 (0)