Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 119 additions & 38 deletions source/ports/rs_port/src/types/metacall_exception.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
use super::{MetaCallStringConversionError, MetaCallValue};
use crate::{
bindings::{
metacall_exception_type, metacall_throwable_value, metacall_value_create_exception,
metacall_value_destroy, metacall_value_to_exception, metacall_value_to_throwable,
metacall_error_from_value, metacall_error_throw, metacall_exception_type,
metacall_throwable_value, metacall_value_copy, metacall_value_destroy,
metacall_value_to_throwable,
},
cast, cstring,
};
use std::{
ffi::{c_char, c_void, CStr},
ffi::{c_void, CStr, CString},
fmt::{self, Debug, Formatter},
sync::Arc,
};

unsafe impl Send for metacall_exception_type {}
unsafe impl Sync for metacall_exception_type {}

/// Represents MetaCall exception. You can create an exception with [new](#method.new).
pub struct MetaCallException {
exception_struct: Arc<metacall_exception_type>,
message: String,
label: String,
stacktrace: String,
code: i64,
leak: bool,
value: *mut c_void,
}
Expand All @@ -27,10 +30,24 @@ unsafe impl Sync for MetaCallException {}

impl Clone for MetaCallException {
fn clone(&self) -> Self {
let value = if self.value.is_null() {
std::ptr::null_mut()
} else {
let copied = unsafe { metacall_value_copy(self.value) };
assert!(
!copied.is_null(),
"metacall_value_copy failed to clone MetaCallException"
);
copied
};

Self {
exception_struct: self.exception_struct.clone(),
leak: true,
value: self.value,
message: self.message.clone(),
label: self.label.clone(),
stacktrace: self.stacktrace.clone(),
code: self.code,
leak: false,
value,
}
}
}
Expand All @@ -48,65 +65,129 @@ impl MetaCallException {
stacktrace: impl ToString,
code: i64,
) -> Result<Self, MetaCallStringConversionError> {
let message = cstring!(message)?.into_raw();
let label = cstring!(label)?.into_raw();
let stacktrace = cstring!(stacktrace)?.into_raw();
let mut exception_struct = metacall_exception_type {
message: message.cast(),
label: label.cast(),
stacktrace: stacktrace.cast(),
code,
};
let exception_ptr = unsafe {
metacall_value_create_exception(&mut exception_struct as *mut _ as *mut c_void)
let message_str = message.to_string();
let label_str = label.to_string();
let stacktrace_str = stacktrace.to_string();

let c_message = cstring!(&message_str)?;
let c_label = cstring!(&label_str)?;
let c_stacktrace = cstring!(&stacktrace_str)?;
let format_str = CString::new("%s").unwrap();

let th = unsafe {
metacall_error_throw(
c_label.as_ptr(),
code,
c_stacktrace.as_ptr(),
format_str.as_ptr(),
c_message.as_ptr(),
)
};

assert!(
!th.is_null(),
"metacall_error_throw failed to allocate exception throwable"
);

let th_obj = unsafe { metacall_value_to_throwable(th) };
if th_obj.is_null() {
unsafe { metacall_value_destroy(th) };
panic!("metacall_value_to_throwable returned null");
}

let ex_val = unsafe { metacall_throwable_value(th_obj) };
if ex_val.is_null() {
unsafe { metacall_value_destroy(th) };
panic!("metacall_throwable_value returned null");
}

let copied = unsafe { metacall_value_copy(ex_val) };
unsafe { metacall_value_destroy(th) };

assert!(
!copied.is_null(),
"metacall_value_copy failed to copy exception value"
);

Ok(Self {
exception_struct: Arc::new(exception_struct),
value: exception_ptr,
message: message_str,
label: label_str,
stacktrace: stacktrace_str,
code,
value: copied,
leak: false,
})
}

#[doc(hidden)]
pub fn new_raw(value: *mut c_void) -> Self {
let exception = unsafe { metacall_value_to_exception(value) };
let mut ex_struct = metacall_exception_type {
message: std::ptr::null(),
label: std::ptr::null(),
stacktrace: std::ptr::null(),
code: 0,
};

if !value.is_null() {
// metacall_error_from_value returns 0 on success, or non-zero if value is not an
// exception or throwable. If it fails, ex_struct remains null/zero-initialized.
let _ = unsafe { metacall_error_from_value(value, &mut ex_struct) };
}

let message = if ex_struct.message.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ex_struct.message) }
.to_string_lossy()
.into_owned()
};
let label = if ex_struct.label.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ex_struct.label) }
.to_string_lossy()
.into_owned()
};
let stacktrace = if ex_struct.stacktrace.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ex_struct.stacktrace) }
.to_string_lossy()
.into_owned()
};

Self {
exception_struct: Arc::new(unsafe { *(exception as *mut metacall_exception_type) }),
message,
label,
stacktrace,
code: ex_struct.code,
leak: false,
value,
}
}

#[doc(hidden)]
pub fn new_raw_leak(value: *mut c_void) -> Self {
let exception = unsafe { metacall_value_to_exception(value) };
Self {
exception_struct: Arc::new(unsafe { *(exception as *mut metacall_exception_type) }),
leak: true,
value,
}
}

fn string_convertor(string: *const c_char) -> String {
String::from(unsafe { CStr::from_ptr(string) }.to_str().unwrap())
let mut s = Self::new_raw(value);
s.leak = true;
s
}

/// Gets the exception message.
pub fn get_message(&self) -> String {
Self::string_convertor(self.exception_struct.message)
self.message.clone()
}
/// Gets the exception label.
pub fn get_label(&self) -> String {
Self::string_convertor(self.exception_struct.label)
self.label.clone()
}
/// Gets the exception stacktrace.
pub fn get_stacktrace(&self) -> String {
Self::string_convertor(self.exception_struct.stacktrace)
self.stacktrace.clone()
}
/// Gets the exception code.
pub fn get_code(&self) -> i64 {
self.exception_struct.code
self.code
}

#[doc(hidden)]
Expand Down Expand Up @@ -222,7 +303,7 @@ impl fmt::Display for MetaCallThrowable {

impl Drop for MetaCallException {
fn drop(&mut self) {
if !self.leak {
if !self.leak && !self.value.is_null() {
unsafe { metacall_value_destroy(self.value) }
}
}
Expand Down
100 changes: 96 additions & 4 deletions source/ports/rs_port/tests/metacall_exception_test.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,94 @@
use metacall::{
initialize, is_initialized,
load::{self, Tag},
MetaCallException,
};
use std::env;

#[test]
fn metacall_exception() {
let _d = initialize().unwrap();
fn test_exception_new_and_drop() {
let ex = MetaCallException::new(
"Invalid state encountered",
"RuntimeError",
"at module.rs:42",
404,
)
.expect("MetaCallException::new should succeed");

assert!(is_initialized());
assert_eq!(ex.get_message(), "Invalid state encountered");
assert_eq!(ex.get_label(), "RuntimeError");
assert_eq!(ex.get_stacktrace(), "at module.rs:42");
assert_eq!(ex.get_code(), 404);

drop(ex);
}

fn test_exception_clone_outlives_original() {
let clone = {
let original =
MetaCallException::new("Original error", "CustomException", "at handler.rs:12", 500)
.expect("Failed to create original exception");

assert_eq!(original.get_message(), "Original error");
original.clone()
}; // original is dropped here

assert_eq!(clone.get_message(), "Original error");
assert_eq!(clone.get_label(), "CustomException");
assert_eq!(clone.get_stacktrace(), "at handler.rs:12");
assert_eq!(clone.get_code(), 500);

drop(clone);
}

fn test_exception_original_outlives_clone() {
let original = MetaCallException::new(
"Persistent error",
"PersistentLabel",
"at service.rs:88",
503,
)
.expect("Failed to create original exception");

{
let clone = original.clone();
assert_eq!(clone.get_message(), "Persistent error");
assert_eq!(clone.get_label(), "PersistentLabel");
assert_eq!(clone.get_stacktrace(), "at service.rs:88");
assert_eq!(clone.get_code(), 503);
} // clone is dropped here

assert_eq!(original.get_message(), "Persistent error");
assert_eq!(original.get_label(), "PersistentLabel");
assert_eq!(original.get_stacktrace(), "at service.rs:88");
assert_eq!(original.get_code(), 503);

drop(original);
}

fn test_exception_repeated_cloning() {
let original =
MetaCallException::new("Repeated clone message", "CloneLabel", "at clone.rs:1", 200)
.expect("Failed to create original exception");

let mut clones = Vec::new();
for _ in 0..20 {
clones.push(original.clone());
}

// Drop original while clones are alive
drop(original);

for clone in &clones {
assert_eq!(clone.get_message(), "Repeated clone message");
assert_eq!(clone.get_label(), "CloneLabel");
assert_eq!(clone.get_stacktrace(), "at clone.rs:1");
assert_eq!(clone.get_code(), 200);
}

drop(clones);
}

fn test_exception_nodejs_load() {
let tests_dir = env::current_dir().unwrap().join("tests/scripts");
let js_test_file = tests_dir.join("script.js");

Expand Down Expand Up @@ -43,3 +122,16 @@ fn metacall_exception() {
drop(cloned_val_3);
}
}

#[test]
fn metacall_exception() {
let _d = initialize().unwrap();

assert!(is_initialized());

test_exception_new_and_drop();
test_exception_clone_outlives_original();
test_exception_original_outlives_clone();
test_exception_repeated_cloning();
test_exception_nodejs_load();
}