use crate::diag::{ConstraintError, CoppError, PathError};
use std::{
any::Any,
cell::RefCell,
ffi::{CStr, c_char},
slice,
};
const LAST_ERROR_MAX_BYTES: usize = 64 * 1024;
const EMPTY_C_MESSAGE: &[u8] = b"\0";
#[derive(Clone, Debug)]
struct LastError {
code: CoppStatus,
message: Vec<u8>,
}
impl LastError {
fn empty() -> Self {
Self {
code: CoppStatus::Ok,
message: EMPTY_C_MESSAGE.to_vec(),
}
}
}
thread_local! {
static LAST_ERROR: RefCell<LastError> = RefCell::new(LastError::empty());
}
fn truncate_utf8_to_cap(message: &mut String) {
if message.len() <= LAST_ERROR_MAX_BYTES {
return;
}
let mut end = LAST_ERROR_MAX_BYTES;
while end > 0 && !message.is_char_boundary(end) {
end -= 1;
}
message.truncate(end);
}
fn normalize_message(bytes: &[u8]) -> Vec<u8> {
let bytes = if bytes.len() > LAST_ERROR_MAX_BYTES {
&bytes[..LAST_ERROR_MAX_BYTES]
} else {
bytes
};
let mut message = String::from_utf8_lossy(bytes).into_owned();
message = message.replace('\0', "\u{FFFD}");
truncate_utf8_to_cap(&mut message);
let mut bytes = message.into_bytes();
bytes.push(0);
bytes
}
pub(crate) fn clear_last_error() {
LAST_ERROR.with(|last_error| {
*last_error.borrow_mut() = LastError::empty();
});
}
pub(crate) fn set_last_error_bytes(status: CoppStatus, message: &[u8]) {
let status = if status == CoppStatus::Ok {
CoppStatus::SolverOther
} else {
status
};
let message = normalize_message(message);
LAST_ERROR.with(|last_error| {
*last_error.borrow_mut() = LastError {
code: status,
message,
};
});
}
pub(crate) fn set_last_error_message(status: CoppStatus, message: impl AsRef<str>) {
set_last_error_bytes(status, message.as_ref().as_bytes());
}
pub(crate) fn panic_to_status(payload: Box<dyn Any + Send>) -> CoppStatus {
match payload.downcast::<CoppStatus>() {
Ok(status) => {
let status = if *status == CoppStatus::Ok {
CoppStatus::Panic
} else {
*status
};
set_last_error_bytes(status, status.message().to_bytes());
status
}
Err(payload) => match payload.downcast::<String>() {
Ok(message) => {
set_last_error_message(CoppStatus::Panic, *message);
CoppStatus::Panic
}
Err(payload) => match payload.downcast::<&'static str>() {
Ok(message) => {
set_last_error_message(CoppStatus::Panic, *message);
CoppStatus::Panic
}
Err(_) => {
set_last_error_bytes(CoppStatus::Panic, CoppStatus::Panic.message().to_bytes());
CoppStatus::Panic
}
},
},
}
}
fn ensure_last_error(status: CoppStatus) {
if status == CoppStatus::Ok {
clear_last_error();
return;
}
LAST_ERROR.with(|last_error| {
let mut last_error = last_error.borrow_mut();
if last_error.code == CoppStatus::Ok {
*last_error = LastError {
code: status,
message: normalize_message(status.message().to_bytes()),
};
}
});
}
pub(crate) fn current_last_error_message_lossy() -> Option<String> {
LAST_ERROR.with(|last_error| {
let last_error = last_error.borrow();
if last_error.code == CoppStatus::Ok {
return None;
}
let bytes = last_error
.message
.strip_suffix(&[0])
.unwrap_or(&last_error.message);
Some(String::from_utf8_lossy(bytes).into_owned())
})
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoppStatus {
Ok = 0,
NullPointer = 1,
InvalidLength = 2,
InvalidShape = 3,
InvalidArgument = 4,
Panic = 5,
AllocationFailed = 6,
IoError = 100,
ConstraintNonIncreasingS = 200,
ConstraintNoMatchDimensions = 201,
ConstraintNoMatchOrder = 202,
ConstraintInvalidSignedBounds = 203,
ConstraintOutOfSBounds = 204,
ConstraintNonPositiveA = 205,
ConstraintNonPositiveLinearizationFloor = 206,
ConstraintNoGivenQInfo = 207,
ConstraintLinearJerkNotAvailable = 208,
ConstraintNoDynamic = 209,
ConstraintInfeasibleReference = 210,
ConstraintEmptyInterval = 211,
PathInvalidDimension = 300,
PathInvalidRange = 301,
PathInvalidOrder = 302,
PathDimensionMismatch = 303,
PathNotEnoughWaypoints = 304,
PathOutOfRangeS = 305,
PathUnsupportedBoundary = 306,
PathSingularSystem = 307,
PathUnsupportedDerivativeOrder = 308,
SolverInfeasible = 400,
SolverUnbounded = 401,
SolverInvalidInput = 402,
SolverInvalidOptions = 403,
ClarabelSolverError = 404,
ClarabelSolverStatus = 405,
SolverOther = 499,
RobotDynamicsError = 500,
}
impl CoppStatus {
#[inline]
pub fn message(self) -> &'static CStr {
match self {
Self::Ok => c"ok",
Self::NullPointer => c"null pointer",
Self::InvalidLength => c"invalid length",
Self::InvalidShape => c"invalid shape",
Self::InvalidArgument => c"invalid argument",
Self::Panic => c"panic across C ABI boundary",
Self::AllocationFailed => c"allocation failed",
Self::IoError => c"I/O error",
Self::ConstraintNonIncreasingS => c"constraint error: non-increasing s",
Self::ConstraintNoMatchDimensions => c"constraint error: dimensions do not match",
Self::ConstraintNoMatchOrder => c"constraint error: order does not match",
Self::ConstraintInvalidSignedBounds => c"constraint error: invalid signed bounds",
Self::ConstraintOutOfSBounds => c"constraint error: station interval out of bounds",
Self::ConstraintNonPositiveA => c"constraint error: non-positive a",
Self::ConstraintNonPositiveLinearizationFloor => {
c"constraint error: non-positive linearization floor"
}
Self::ConstraintNoGivenQInfo => c"constraint error: missing path derivative data",
Self::ConstraintLinearJerkNotAvailable => {
c"constraint error: linearized jerk unavailable"
}
Self::ConstraintNoDynamic => c"constraint error: dynamic model unavailable",
Self::ConstraintInfeasibleReference => c"constraint error: infeasible reference",
Self::ConstraintEmptyInterval => c"constraint error: empty interval",
Self::PathInvalidDimension => c"path error: invalid dimension",
Self::PathInvalidRange => c"path error: invalid range",
Self::PathInvalidOrder => c"path error: invalid spline order",
Self::PathDimensionMismatch => c"path error: dimension mismatch",
Self::PathNotEnoughWaypoints => c"path error: not enough waypoints",
Self::PathOutOfRangeS => c"path error: s out of range",
Self::PathUnsupportedBoundary => c"path error: unsupported boundary",
Self::PathSingularSystem => c"path error: singular system",
Self::PathUnsupportedDerivativeOrder => c"path error: unsupported derivative order",
Self::SolverInfeasible => c"solver error: infeasible",
Self::SolverUnbounded => c"solver error: unbounded",
Self::SolverInvalidInput => c"solver error: invalid input",
Self::SolverInvalidOptions => c"solver error: invalid options",
Self::ClarabelSolverError => c"solver error: Clarabel internal error",
Self::ClarabelSolverStatus => c"solver error: Clarabel status failure",
Self::SolverOther => c"solver error: other",
Self::RobotDynamicsError => c"robot dynamics error",
}
}
#[inline]
pub fn message_ptr(self) -> *const c_char {
self.message().as_ptr()
}
#[inline]
pub(crate) fn into_ffi_status(self) -> Self {
ensure_last_error(self);
self
}
}
impl From<&CoppError> for CoppStatus {
fn from(error: &CoppError) -> Self {
let status = match error {
CoppError::IoError(_) => Self::IoError,
CoppError::ConstraintError(error) => Self::from(error),
CoppError::PathError(error) => Self::from(error),
CoppError::RobotDynamicsError(_) => Self::RobotDynamicsError,
CoppError::Infeasible(_, _) => Self::SolverInfeasible,
CoppError::Unbounded(_, _) => Self::SolverUnbounded,
CoppError::InvalidInput(_, _) => Self::SolverInvalidInput,
CoppError::InvalidOptions(_, _) => Self::SolverInvalidOptions,
CoppError::ClarabelSolverError(_, _) => Self::ClarabelSolverError,
CoppError::ClarabelSolverStatus(_, _) => Self::ClarabelSolverStatus,
CoppError::Other(_, _) => Self::SolverOther,
};
set_last_error_message(status, error.to_string());
status
}
}
impl From<&ConstraintError> for CoppStatus {
fn from(error: &ConstraintError) -> Self {
let status = match error {
ConstraintError::NonIncreasingS { .. } => Self::ConstraintNonIncreasingS,
ConstraintError::NoMatchDimensions => Self::ConstraintNoMatchDimensions,
ConstraintError::NoMatchOrder => Self::ConstraintNoMatchOrder,
ConstraintError::InvalidSignedBounds { .. } => Self::ConstraintInvalidSignedBounds,
ConstraintError::OutOfSBounds { .. } => Self::ConstraintOutOfSBounds,
ConstraintError::NonPositiveA => Self::ConstraintNonPositiveA,
ConstraintError::NonPositiveLinearizationFloor => {
Self::ConstraintNonPositiveLinearizationFloor
}
ConstraintError::NoGivenQInfo => Self::ConstraintNoGivenQInfo,
ConstraintError::LinearJerkNotAvailable { .. } => {
Self::ConstraintLinearJerkNotAvailable
}
ConstraintError::NoDynamic => Self::ConstraintNoDynamic,
ConstraintError::InfeasibleReference => Self::ConstraintInfeasibleReference,
ConstraintError::EmptyInterval { .. } => Self::ConstraintEmptyInterval,
};
set_last_error_message(status, error.to_string());
status
}
}
impl From<&PathError> for CoppStatus {
fn from(error: &PathError) -> Self {
let status = match error {
PathError::InvalidDimension { .. } => Self::PathInvalidDimension,
PathError::InvalidRange { .. } => Self::PathInvalidRange,
PathError::InvalidOrder { .. } => Self::PathInvalidOrder,
PathError::DimensionMismatch => Self::PathDimensionMismatch,
PathError::UnsupportedDerivativeOrder { .. } => Self::PathUnsupportedDerivativeOrder,
PathError::NotEnoughWaypoints { .. } => Self::PathNotEnoughWaypoints,
PathError::OutOfRangeS { .. } => Self::PathOutOfRangeS,
PathError::UnsupportedBoundary { .. } => Self::PathUnsupportedBoundary,
PathError::SingularSystem => Self::PathSingularSystem,
};
set_last_error_message(status, error.to_string());
status
}
}
#[unsafe(no_mangle)]
pub extern "C" fn copp_last_error_code() -> CoppStatus {
LAST_ERROR.with(|last_error| last_error.borrow().code)
}
#[unsafe(no_mangle)]
pub extern "C" fn copp_last_error_message() -> *const c_char {
LAST_ERROR.with(|last_error| last_error.borrow().message.as_ptr().cast())
}
#[unsafe(no_mangle)]
pub extern "C" fn copp_last_error_message_len() -> usize {
LAST_ERROR.with(|last_error| last_error.borrow().message.len().saturating_sub(1))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn copp_last_error_message_copy(
buffer: *mut c_char,
capacity: usize,
out_len: *mut usize,
) -> CoppStatus {
LAST_ERROR.with(|last_error| {
let last_error = last_error.borrow();
let message = last_error
.message
.strip_suffix(&[0])
.unwrap_or(&last_error.message);
if !out_len.is_null() {
unsafe {
out_len.write(message.len());
}
}
if capacity == 0 {
return CoppStatus::Ok;
}
if buffer.is_null() {
return CoppStatus::NullPointer.into_ffi_status();
}
let mut copy_len = message.len().min(capacity - 1);
while copy_len > 0 && std::str::from_utf8(&message[..copy_len]).is_err() {
copy_len -= 1;
}
unsafe {
ptr_copy_nonoverlapping(message.as_ptr(), buffer.cast::<u8>(), copy_len);
buffer.cast::<u8>().add(copy_len).write(0);
}
CoppStatus::Ok
})
}
#[unsafe(no_mangle)]
pub extern "C" fn copp_clear_last_error() {
clear_last_error();
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn copp_set_last_error_message(
status: CoppStatus,
message: *const c_char,
) -> CoppStatus {
if message.is_null() {
return CoppStatus::NullPointer.into_ffi_status();
}
let bytes = unsafe { CStr::from_ptr(message).to_bytes() };
set_last_error_bytes(status, bytes);
CoppStatus::Ok
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn copp_set_last_error_message_n(
status: CoppStatus,
message: *const c_char,
len: usize,
) -> CoppStatus {
if len > 0 && message.is_null() {
return CoppStatus::NullPointer.into_ffi_status();
}
let bytes = if len == 0 {
&[]
} else {
unsafe { slice::from_raw_parts(message.cast::<u8>(), len) }
};
set_last_error_bytes(status, bytes);
CoppStatus::Ok
}
unsafe fn ptr_copy_nonoverlapping(src: *const u8, dst: *mut u8, len: usize) {
unsafe {
std::ptr::copy_nonoverlapping(src, dst, len);
}
}