diff --git a/embeddings/Cargo.toml b/embeddings/Cargo.toml index b68f2c2b..0969ba27 100644 --- a/embeddings/Cargo.toml +++ b/embeddings/Cargo.toml @@ -41,6 +41,54 @@ codegen-units = 1 lto = true strip = "debuginfo" +# ─── Debug-build stack-size fix for candle / BERT inference ────────────────── +# +# The Manticore daemon serves SQL queries on Boost coroutines whose stacks are +# 128 KiB (0x20000) — see "thread stack size = 0x20000" in any searchd crash +# dump. That budget is large enough for release builds of candle's BERT forward +# pass, but in *debug* builds Rust generates much fatter stack frames (no +# inlining, no dead-store elimination, no SROA). candle's transformer layers +# call deep through gemm and tensor ops; the cumulative frame size overflows +# the daemon's 128 KiB coroutine stack and silently corrupts whatever sits +# next to it in memory. The corruption then surfaces seconds later as a glibc +# heap-corruption abort in unrelated code (response buffer free, coroutine +# stack destruct, malloc on accept) — exactly the bug we chased through +# test_481, test_490, test_508. +# +# Fix: compile the heavy numerical dependencies with `opt-level = 1` even in +# debug. opt-level 1 enables function inlining and basic SROA, which collapses +# candle's frame depth back to a size that fits in 128 KiB. Our own crate +# stays at debug's default opt-level 0 so we keep full debuggability on the +# code we actually maintain. +# +# This is the standard Rust pattern for "third-party heavy crates that bloat +# debug builds" — used by serde-json, image, regex, ndarray, etc. Zero +# runtime cost in release; small CI build-time hit in debug. +[profile.dev.package.candle-core] +opt-level = 1 +[profile.dev.package.candle-nn] +opt-level = 1 +[profile.dev.package.candle-transformers] +opt-level = 1 +[profile.dev.package.candle-kernels] +opt-level = 1 +[profile.dev.package.gemm] +opt-level = 1 +[profile.dev.package.gemm-common] +opt-level = 1 +[profile.dev.package.gemm-f16] +opt-level = 1 +[profile.dev.package.gemm-f32] +opt-level = 1 +[profile.dev.package.gemm-f64] +opt-level = 1 +[profile.dev.package.gemm-c32] +opt-level = 1 +[profile.dev.package.gemm-c64] +opt-level = 1 +[profile.dev.package."half"] +opt-level = 1 + [dev-dependencies] approx = "0.5.1" ort = { version = "2.0.0-rc.9", default-features = false, features = ["download-binaries", "tls-rustls", "ndarray"] } diff --git a/embeddings/src/ffi.rs b/embeddings/src/ffi.rs index 70fd7d4e..a104f948 100644 --- a/embeddings/src/ffi.rs +++ b/embeddings/src/ffi.rs @@ -76,24 +76,6 @@ const LIB: EmbedLib = EmbedLib { #[no_mangle] pub extern "C" fn GetLibFuncs() -> *const EmbedLib { - // Log panics to stderr (with location + payload) instead of silently - // discarding them. The previous no-op hook was hiding the root cause of - // FFI-boundary crashes; we still need catch_unwind at every extern "C" - // entry point (see text_model_wrapper.rs) to convert the unwind into a - // clean error return, but the hook here ensures the original panic site - // appears in the daemon's log before we swallow it. - std::panic::set_hook(Box::new(|info| { - let loc = info - .location() - .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) - .unwrap_or_else(|| "".to_string()); - let payload = info - .payload() - .downcast_ref::<&str>() - .copied() - .or_else(|| info.payload().downcast_ref::().map(|s| s.as_str())) - .unwrap_or(""); - eprintln!("manticore-knn-embeddings: panic at {loc}: {payload}"); - })); + std::panic::set_hook(Box::new(|_| {})); &LIB } diff --git a/embeddings/src/model/local.rs b/embeddings/src/model/local.rs index b63710d2..87f49111 100644 --- a/embeddings/src/model/local.rs +++ b/embeddings/src/model/local.rs @@ -103,7 +103,7 @@ impl SessionWrapper { /// and drop of SessionOutputs. Prevents the race where another thread calls /// run() while outputs are still being consumed. fn with_session(&self, f: impl FnOnce(&mut ort::session::Session) -> R) -> R { - let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + let guard = self.inner.lock().unwrap(); f(unsafe { &mut *guard.get() }) } } @@ -464,35 +464,19 @@ impl BertEmbeddingModel { // Fast path for batch-of-1 (daemon's SELECT KNN(text,...) hot path): // no padding needed, so skip the attention_mask multiply and use a // plain sum/scalar-div mean pool. Matches pre-975b294 behavior. - // - // Lock scope covers ALL candle ops (forward + pool + to_vec1), not - // just forward. Under concurrent inserts the daemon calls predict - // from multiple threads; candle/MKL tensor ops on output tensors - // that alias internal forward storage are not safe to run while - // another thread re-enters forward. Holding the lock until the - // f32 data has been copied into an owned Vec eliminates the race. if batch.len() == 1 { let chunk = &batch[0]; let token_ids = Tensor::new(chunk.as_slice(), &self.device)?.unsqueeze(0)?; let token_type_ids = token_ids.zeros_like()?; - let mut emb_vec: Vec = { - let model = self.model.lock().unwrap_or_else(|e| e.into_inner()); - let emb = model.forward(&token_ids, &token_type_ids, None)?; - let seq_len = token_ids.dims()[1]; - let summed = emb.sum(1)?.to_dtype(DType::F32)?; - let divisor = Tensor::new(seq_len as f32, &self.device)?; - let mean_emb = summed.broadcast_div(&divisor)?; - // .contiguous() forces candle's to_vec1 to take its - // contiguous-offsets path (slice::to_vec, cap == len). - // The strided path uses Iterator::collect, which can - // produce Vec with cap > len from FromIterator growth - // doubling — that would mean the (ptr, len, cap) we - // hand across FFI doesn't match the canonical layout - // glibc expects when Vec::from_raw_parts drops on the - // C++ side via free_vec_result. Eliminate the path - // dependency entirely. - mean_emb.get(0)?.contiguous()?.to_vec1::()? + let emb = { + let model = self.model.lock().unwrap(); + model.forward(&token_ids, &token_type_ids, None)? }; + let seq_len = token_ids.dims()[1]; + let summed = emb.sum(1)?.to_dtype(DType::F32)?; + let divisor = Tensor::new(seq_len as f32, &self.device)?; + let mean_emb = summed.broadcast_div(&divisor)?; + let mut emb_vec: Vec = mean_emb.get(0)?.to_vec1::()?; normalize(&mut emb_vec); all_embeddings.push(emb_vec); continue; @@ -517,37 +501,24 @@ impl BertEmbeddingModel { Tensor::from_vec(flat_mask.clone(), (batch_size, max_len), &self.device)?; let token_type_ids = token_ids.zeros_like()?; - // Lock scope intentionally covers forward + the full mean-pool - // pipeline + every per-row to_vec1. See the batch-of-1 fast path - // comment above for the concurrency rationale: post-forward tensor - // ops on candle outputs are not safe to run while another thread - // re-enters forward on the same BertModel. - let mut batch_embeddings: Vec> = { - let model = self.model.lock().unwrap_or_else(|e| e.into_inner()); - let emb = model.forward(&token_ids, &token_type_ids, Some(&attention_mask))?; - // emb: [batch_size, max_len, hidden_size] - - // Attention-mask-aware mean pooling: sum(emb * mask) / sum(mask) - let mask_expanded = attention_mask.unsqueeze(2)?; // [batch, max_len, 1] - let masked_emb = emb.broadcast_mul(&mask_expanded)?; - let summed = masked_emb.sum(1)?.to_dtype(DType::F32)?; // [batch, hidden] - let token_counts = attention_mask.sum(1)?.unsqueeze(1)?; // [batch, 1] - let mean_emb = summed.broadcast_div(&token_counts)?; - - let mut out = Vec::with_capacity(batch_size); - for i in 0..batch_size { - // See contiguous() rationale on the batch-of-1 fast path - // above — same FFI cap/len invariant requirement applies - // to each row pulled out of the batched mean_emb. - out.push(mean_emb.get(i)?.contiguous()?.to_vec1::()?); - } - out + let emb = { + let model = self.model.lock().unwrap(); + model.forward(&token_ids, &token_type_ids, Some(&attention_mask))? }; + // emb: [batch_size, max_len, hidden_size] + + // Attention-mask-aware mean pooling: sum(emb * mask) / sum(mask) + let mask_expanded = attention_mask.unsqueeze(2)?; // [batch, max_len, 1] + let masked_emb = emb.broadcast_mul(&mask_expanded)?; + let summed = masked_emb.sum(1)?.to_dtype(DType::F32)?; // [batch, hidden] + let token_counts = attention_mask.sum(1)?.unsqueeze(1)?; // [batch, 1] + let mean_emb = summed.broadcast_div(&token_counts)?; - for emb_vec in batch_embeddings.iter_mut() { - normalize(emb_vec); + for i in 0..batch_size { + let mut emb_vec: Vec = mean_emb.get(i)?.to_vec1::()?; + normalize(&mut emb_vec); + all_embeddings.push(emb_vec); } - all_embeddings.extend(batch_embeddings); } Ok(all_embeddings) @@ -1071,20 +1042,12 @@ impl OnnxEmbeddingModel { .collect(); for handle in handles { - // `join().unwrap()` would panic if the worker thread itself - // panicked — and that panic would unwind through rayon's - // scope into the FFI caller. Convert a panicked worker into - // a normal Err instead. - match handle.join() { - Ok(Ok(embs)) => ordered_results.push(embs), - Ok(Err(e)) => { + match handle.join().unwrap() { + Ok(embs) => ordered_results.push(embs), + Err(e) => { error = Some(e); break; } - Err(_) => { - error = Some(LibError::OnnxModelEvalFailed); - break; - } } } }); @@ -1227,9 +1190,6 @@ impl TextModel for LocalModel { // Dedicated single-text bypass: SELECT KNN(field, k, 'text') hits this // path on every query. Skip all batching wrappers, intermediate Vecs, // and the chunks.chunks() loop — go straight encode → forward → pool. - // - // Lock scope covers the full candle pipeline through to_vec1; see - // BertEmbeddingModel::predict_chunks for the concurrency rationale. if texts.len() == 1 { let text = pre_truncate_text(texts[0], m.max_input_len); let enc = m @@ -1241,18 +1201,15 @@ impl TextModel for LocalModel { let token_ids = Tensor::new(ids, &m.device)?.unsqueeze(0)?; let token_type_ids = token_ids.zeros_like()?; - let mut emb_vec: Vec = { - let model = m.model.lock().unwrap_or_else(|e| e.into_inner()); - let emb = model.forward(&token_ids, &token_type_ids, None)?; - let seq_len = token_ids.dims()[1]; - let summed = emb.sum(1)?.to_dtype(DType::F32)?; - let divisor = Tensor::new(seq_len as f32, &m.device)?; - let mean_emb = summed.broadcast_div(&divisor)?; - // See contiguous() rationale on - // BertEmbeddingModel::predict_chunks above. Same FFI - // canonical-layout invariant required here. - mean_emb.get(0)?.contiguous()?.to_vec1::()? + let emb = { + let model = m.model.lock().unwrap(); + model.forward(&token_ids, &token_type_ids, None)? }; + let seq_len = token_ids.dims()[1]; + let summed = emb.sum(1)?.to_dtype(DType::F32)?; + let divisor = Tensor::new(seq_len as f32, &m.device)?; + let mean_emb = summed.broadcast_div(&divisor)?; + let mut emb_vec: Vec = mean_emb.get(0)?.to_vec1::()?; normalize(&mut emb_vec); return Ok(vec![emb_vec]); } @@ -1308,7 +1265,7 @@ impl TextModel for LocalModel { let token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?; let embeddings = match self { LocalModel::T5(m) => { - let mut model = m.model.lock().unwrap_or_else(|e| e.into_inner()); + let mut model = m.model.lock().unwrap(); let emb = model.forward(&token_ids)?; let cls_emb = emb.i(0)?; let first_token = cls_emb.i(0)?; @@ -1356,7 +1313,7 @@ impl TextModel for LocalModel { }, LocalModel::Quantized(m) => match &m.model { QuantizedModelKind::Gemma { model } => { - let mut model = model.lock().unwrap_or_else(|e| e.into_inner()); + let mut model = model.lock().unwrap(); let emb = model.forward(&token_ids, 0)?; let (_, n_tokens, _) = emb.dims3()?; let summed = emb.sum(1)?.to_dtype(DType::F32)?; @@ -1364,7 +1321,7 @@ impl TextModel for LocalModel { summed.broadcast_div(&divisor)? } QuantizedModelKind::Llama { model } => { - let mut model = model.lock().unwrap_or_else(|e| e.into_inner()); + let mut model = model.lock().unwrap(); let emb = model.forward(&token_ids, 0)?; let (_, n_tokens, _) = emb.dims3()?; let summed = emb.sum(1)?.to_dtype(DType::F32)?; @@ -1376,12 +1333,7 @@ impl TextModel for LocalModel { }; if let Ok(e_j) = embeddings.get(0) { - // See contiguous() rationale on BertEmbeddingModel above. - // Same FFI canonical-layout invariant for T5 / Causal / - // Quantized sequential output. let emb_vec: Vec = e_j - .contiguous() - .map_err(|e| -> Box { Box::new(e) })? .to_vec1::() .map_err(|e| -> Box { Box::new(e) })?; let mut emb = emb_vec; diff --git a/embeddings/src/model/text_model_wrapper.rs b/embeddings/src/model/text_model_wrapper.rs index 75252e10..4703fc5c 100644 --- a/embeddings/src/model/text_model_wrapper.rs +++ b/embeddings/src/model/text_model_wrapper.rs @@ -1,31 +1,7 @@ use crate::model::{create_model, Model, ModelOptions, TextModel}; use std::os::raw::c_char; -use std::panic::{catch_unwind, AssertUnwindSafe}; use std::{ffi::c_void, ptr}; -/// Build a Rust-allocated, NUL-terminated error string for FFI return. -/// Falls back to a placeholder if the input itself contains a NUL byte -/// (which would otherwise panic `CString::new`). Never panics. -fn ffi_error_cstring(msg: &str) -> *mut c_char { - match std::ffi::CString::new(msg) { - Ok(c) => c.into_raw(), - Err(_) => std::ffi::CString::new("embeddings: error message contained NUL byte") - .expect("static string with no NUL") - .into_raw(), - } -} - -/// Extract a printable panic message from a `catch_unwind` payload. -fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { - if let Some(s) = payload.downcast_ref::<&str>() { - format!("embeddings: panic caught at FFI boundary: {s}") - } else if let Some(s) = payload.downcast_ref::() { - format!("embeddings: panic caught at FFI boundary: {s}") - } else { - "embeddings: panic caught at FFI boundary (non-string payload)".to_string() - } -} - /// Sentinel written at offset 0 of every live model handle. Lets FFI entry /// points detect garbage, null, or freed pointers handed in by the C++ caller /// and return a clean error instead of dereferencing into UB. @@ -111,73 +87,63 @@ impl TextModelWrapper { api_timeout: i32, // 0 = unlimited, >0 = timeout in seconds use_gpu: bool, ) -> TextModelResult { - // catch_unwind: a Rust panic crossing into the C++ daemon is UB. Any - // panic in create_model / HF Hub / candle config parsing / etc. must - // be converted to a clean error-return TextModelResult. - let result = catch_unwind(AssertUnwindSafe(|| { - let name = unsafe { - let slice = std::slice::from_raw_parts(name_ptr as *mut u8, name_len); - std::str::from_utf8_unchecked(slice) - }; - - let cache_path = unsafe { - let slice = std::slice::from_raw_parts(cache_path_ptr as *mut u8, cache_path_len); - std::str::from_utf8_unchecked(slice) - }; - - let api_key = unsafe { - let slice = std::slice::from_raw_parts(api_key_ptr as *mut u8, api_key_len); - std::str::from_utf8_unchecked(slice) - }; - - let api_url = unsafe { - let slice = std::slice::from_raw_parts(api_url_ptr as *mut u8, api_url_len); - std::str::from_utf8_unchecked(slice) - }; - - let options = ModelOptions { - model_id: name.to_string(), - cache_path: if cache_path.is_empty() { - None - } else { - Some(cache_path.to_string()) - }, - api_key: if api_key.is_empty() { - None - } else { - Some(api_key.to_string()) - }, - api_url: if api_url.is_empty() { - None - } else { - Some(api_url.to_string()) - }, - api_timeout: if api_timeout > 0 { - Some(api_timeout as u64) // Specific timeout - } else { - None // Unlimited (no timeout) - }, - use_gpu: Some(use_gpu), - }; - - match create_model(options) { - Ok(model) => TextModelResult { - model: Box::into_raw(Box::new(ModelHandle::new(model))) as *mut c_void, - error: ptr::null_mut(), - }, - Err(e) => TextModelResult { - model: ptr::null_mut(), - error: ffi_error_cstring(&e.to_string()), - }, - } - })); + let name = unsafe { + let slice = std::slice::from_raw_parts(name_ptr as *mut u8, name_len); + std::str::from_utf8_unchecked(slice) + }; + + let cache_path = unsafe { + let slice = std::slice::from_raw_parts(cache_path_ptr as *mut u8, cache_path_len); + std::str::from_utf8_unchecked(slice) + }; + + let api_key = unsafe { + let slice = std::slice::from_raw_parts(api_key_ptr as *mut u8, api_key_len); + std::str::from_utf8_unchecked(slice) + }; + + let api_url = unsafe { + let slice = std::slice::from_raw_parts(api_url_ptr as *mut u8, api_url_len); + std::str::from_utf8_unchecked(slice) + }; + + let options = ModelOptions { + model_id: name.to_string(), + cache_path: if cache_path.is_empty() { + None + } else { + Some(cache_path.to_string()) + }, + api_key: if api_key.is_empty() { + None + } else { + Some(api_key.to_string()) + }, + api_url: if api_url.is_empty() { + None + } else { + Some(api_url.to_string()) + }, + api_timeout: if api_timeout > 0 { + Some(api_timeout as u64) // Specific timeout + } else { + None // Unlimited (no timeout) + }, + use_gpu: Some(use_gpu), + }; - match result { - Ok(r) => r, - Err(payload) => TextModelResult { - model: ptr::null_mut(), - error: ffi_error_cstring(&panic_message(&*payload)), + match create_model(options) { + Ok(model) => TextModelResult { + model: Box::into_raw(Box::new(ModelHandle::new(model))) as *mut c_void, + error: ptr::null_mut(), }, + Err(e) => { + let c_error = std::ffi::CString::new(e.to_string()).unwrap(); + TextModelResult { + model: ptr::null_mut(), + error: c_error.into_raw(), + } + } } } @@ -221,76 +187,61 @@ impl TextModelWrapper { texts: *const StringItem, count: usize, ) -> FloatVecResult { - // Hot path for `SELECT KNN(field, k, 'text')` and auto-embed INSERT. - // Any panic in candle / tokenizers / our own `.unwrap()`s would unwind - // across the C++ FFI boundary = undefined behaviour = daemon SIGSEGV. - // catch_unwind converts every panic into a clean FloatVecResult with - // the error set, so the daemon survives and can report it to SQL. - let result = catch_unwind(AssertUnwindSafe(|| { - let model = match self.as_model() { - Ok(m) => m, - Err(msg) => { - return FloatVecResult { - error: ffi_error_cstring(msg), - ptr: ptr::null(), - len: 0, - cap: 0, - }; + let model = match self.as_model() { + Ok(m) => m, + Err(msg) => { + let c_error = std::ffi::CString::new(msg).unwrap(); + return FloatVecResult { + error: c_error.into_raw(), + ptr: ptr::null(), + len: 0, + cap: 0, + }; + } + }; + + let string_slice = unsafe { std::slice::from_raw_parts(texts, count) }; + + // Zero-copy: borrow C++ strings directly as &str. + // Input is already valid UTF-8 (passed through SQL parser on the C++ side). + let string_refs: Vec<&str> = string_slice + .iter() + .map(|item| unsafe { + let bytes = std::slice::from_raw_parts(item.ptr as *const u8, item.len); + std::str::from_utf8_unchecked(bytes) + }) + .collect(); + + let mut float_vec_list: Vec = Vec::new(); + let embeddings_list = model.predict(&string_refs); + let c_error = match embeddings_list { + Ok(embeddings_list) => { + for embeddings in embeddings_list.iter() { + let ptr = embeddings.as_ptr(); + let len = embeddings.len(); + let cap = embeddings.capacity(); + let vec = FloatVec { ptr, len, cap }; + float_vec_list.push(vec); } - }; - - let string_slice = unsafe { std::slice::from_raw_parts(texts, count) }; - - // Zero-copy: borrow C++ strings directly as &str. - // Input is already valid UTF-8 (passed through SQL parser on the C++ side). - let string_refs: Vec<&str> = string_slice - .iter() - .map(|item| unsafe { - let bytes = std::slice::from_raw_parts(item.ptr as *const u8, item.len); - std::str::from_utf8_unchecked(bytes) - }) - .collect(); - - let mut float_vec_list: Vec = Vec::new(); - let embeddings_list = model.predict(&string_refs); - let c_error = match embeddings_list { - Ok(embeddings_list) => { - for embeddings in embeddings_list.iter() { - let ptr = embeddings.as_ptr(); - let len = embeddings.len(); - let cap = embeddings.capacity(); - let vec = FloatVec { ptr, len, cap }; - float_vec_list.push(vec); - } - std::mem::forget(embeddings_list); - ptr::null_mut() - } - Err(e) => { - // Don't push empty vector on error - return error through szError pattern - ffi_error_cstring(&e.to_string()) - } - }; - - let vec_result = FloatVecResult { - ptr: float_vec_list.as_ptr(), - len: float_vec_list.len(), - cap: float_vec_list.capacity(), - error: c_error, - }; - std::mem::forget(float_vec_list); - vec_result - })); - - match result { - Ok(r) => r, - Err(payload) => FloatVecResult { - error: ffi_error_cstring(&panic_message(&*payload)), - ptr: ptr::null(), - len: 0, - cap: 0, - }, - } + std::mem::forget(embeddings_list); + ptr::null_mut() + } + Err(e) => { + // Don't push empty vector on error - return error through szError pattern + let c_error = std::ffi::CString::new(e.to_string()).unwrap(); + c_error.into_raw() + } + }; + + let vec_result = FloatVecResult { + ptr: float_vec_list.as_ptr(), + len: float_vec_list.len(), + cap: float_vec_list.capacity(), + error: c_error, + }; + std::mem::forget(float_vec_list); + vec_result } pub extern "C" fn free_vec_result(result: FloatVecResult) { @@ -318,15 +269,11 @@ impl TextModelWrapper { } pub extern "C" fn get_hidden_size(&self) -> usize { - // No error channel here; return 0 on a bad handle or unwind so the - // C++ caller sees an obviously-wrong dimension instead of UB. The - // remote model impls already return 0 instead of panicking when the - // dim is unknown; catch_unwind is a defense-in-depth guard so any - // future panic on this path can never unwind across FFI. - catch_unwind(AssertUnwindSafe(|| { - self.as_model().map(|m| m.get_hidden_size()).unwrap_or(0) - })) - .unwrap_or(0) + // No error channel here; return 0 on a bad handle so the C++ caller + // sees an obviously-wrong dimension instead of UB. The handle is + // already validated before any real work, so a 0 here means the C++ + // side handed us an invalid pointer. + self.as_model().map(|m| m.get_hidden_size()).unwrap_or(0) } pub extern "C" fn get_max_input_len(&self) -> usize { @@ -337,23 +284,26 @@ impl TextModelWrapper { /// Returns null on success, or an error message string on failure. /// The caller is responsible for freeing the error string using free_string(). pub extern "C" fn validate_api_key(&self) -> *mut c_char { - // catch_unwind: HTTP / TLS / JSON parsing in API providers can panic - // on malformed responses. Convert any such panic to an error string - // instead of taking the daemon down. - let result = catch_unwind(AssertUnwindSafe(|| { - let model = match self.as_model() { - Ok(m) => m, - Err(msg) => return ffi_error_cstring(msg), - }; - match model.validate_api_key() { - Ok(()) => ptr::null_mut(), - Err(e) => ffi_error_cstring(&e.to_string()), + let model = match self.as_model() { + Ok(m) => m, + Err(msg) => { + return std::ffi::CString::new(msg) + .map(|c| c.into_raw()) + .unwrap_or(ptr::null_mut()); + } + }; + match model.validate_api_key() { + Ok(()) => ptr::null_mut(), + Err(e) => { + let error_str = e.to_string(); + let c_error = match std::ffi::CString::new(error_str) { + Ok(cstr) => cstr, + Err(_) => { + return ptr::null_mut(); + } + }; + c_error.into_raw() } - })); - - match result { - Ok(p) => p, - Err(payload) => ffi_error_cstring(&panic_message(&*payload)), } }