use log::{debug, trace};
use crate::{
emulation::{
engine::{
cctors::CctorTracker, context::EmulationContext, error::synthetic_exception,
interpreter::Interpreter, EmulationError,
},
exception::{
ExceptionClause, ExceptionInfo, HandlerMatch, InstructionLocation, ThreadExceptionState,
},
memory::AddressSpace,
thread::EmulationThread,
tokens, EmValue,
},
metadata::token::Token,
Result,
};
pub fn create_clr_exception(
address_space: &AddressSpace,
error: &EmulationError,
) -> Result<EmValue> {
let type_token = error.to_exception_token();
let heap_ref = address_space.alloc_object(type_token)?;
Ok(EmValue::ObjectRef(heap_ref))
}
pub fn create_exception_from_type(
address_space: &AddressSpace,
exception_type: Token,
) -> Result<EmValue> {
let heap_ref = address_space.alloc_object(exception_type)?;
Ok(EmValue::ObjectRef(heap_ref))
}
pub fn wrap_in_target_invocation_exception(
address_space: &AddressSpace,
inner_exception: &EmValue,
) -> Result<(EmValue, Token)> {
let tie_type = synthetic_exception::TARGET_INVOCATION;
let tie_ref = address_space.alloc_object(tie_type)?;
address_space.set_field(
tie_ref,
tokens::exception_fields::INNER_EXCEPTION,
inner_exception.clone(),
)?;
Ok((EmValue::ObjectRef(tie_ref), tie_type))
}
pub fn route_clr_exception(
address_space: &AddressSpace,
context: &EmulationContext,
interpreter: &mut Interpreter,
thread: &mut EmulationThread,
current_method: Token,
exception_type: Token,
exception: EmValue,
) -> Result<bool> {
let current_offset = interpreter.ip().offset();
let throw_location = InstructionLocation::new(current_method, current_offset);
debug!(
"route_clr_exception: method=0x{:08X} offset=0x{:04X} exc_type=0x{:08X} depth={}",
current_method.value(),
current_offset,
exception_type.value(),
thread.call_depth()
);
if let Some(heap_ref) = exception.as_object_ref() {
let exception_info = ExceptionInfo::new(heap_ref, exception_type, throw_location);
thread.exception_state_mut().set_exception(exception_info);
}
if let Some(handler_match) = find_exception_handler(
context,
current_method,
current_offset,
Some(exception_type),
thread.exception_state_mut(),
None,
)? {
debug!(
" Found handler in current method 0x{:08X}",
current_method.value()
);
thread.stack_mut().clear();
let target_offset = apply_handler_match(
&handler_match,
exception,
current_offset,
current_method,
thread,
)?;
interpreter.set_offset(target_offset);
return Ok(true);
}
debug!(
" No handler in 0x{:08X}, unwinding...",
current_method.value()
);
let mut effective_exception_type = exception_type;
loop {
if let Some(pending) = thread.exception_state_mut().pop_finally() {
interpreter.set_method(pending.method);
interpreter.set_offset(pending.handler_offset);
thread
.exception_state_mut()
.set_leave_target(pending.leave_target);
return Ok(true);
}
let (frame_return_offset, frame_is_reflection, frame_method) = thread
.current_frame()
.map_or((0, false, Token::new(0)), |f| {
(f.return_offset(), f.is_reflection_invoke(), f.method())
});
if let Some(exc_info) = thread.exception_state_mut().exception_mut() {
exc_info.push_stack_frame(InstructionLocation::new(frame_method, frame_return_offset));
}
if frame_is_reflection {
let inner = thread
.exception_state_mut()
.take_exception_as_value()
.unwrap_or_else(|| {
trace!("No pending exception for extraction in reflection frame");
exception.clone()
});
if let Ok((tie_val, tie_type)) =
wrap_in_target_invocation_exception(address_space, &inner)
{
effective_exception_type = tie_type;
if let Some(tie_ref) = tie_val.as_object_ref() {
let throw_loc = InstructionLocation::new(frame_method, frame_return_offset);
thread
.exception_state_mut()
.set_exception(ExceptionInfo::new(tie_ref, tie_type, throw_loc));
}
}
debug!(
" Wrapping exception in TargetInvocationException (reflection invoke boundary)"
);
}
thread.pop_frame();
if thread.call_depth() == 0 {
return Ok(false);
}
let caller_frame = thread
.current_frame()
.ok_or(EmulationError::InternalError {
description: "call stack empty during exception unwinding".to_string(),
})?;
let caller_method = caller_frame.method();
if let Some(handler_match) = find_exception_handler(
context,
caller_method,
frame_return_offset,
Some(effective_exception_type),
thread.exception_state_mut(),
None,
)? {
let exc = thread
.exception_state_mut()
.take_exception_as_value()
.unwrap_or_else(|| {
trace!("No pending exception for extraction");
EmValue::Null
});
thread.stack_mut().clear();
let target_offset = apply_handler_match(
&handler_match,
exc,
frame_return_offset,
caller_method,
thread,
)?;
interpreter.set_method(caller_method);
interpreter.set_offset(target_offset);
return Ok(true);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn find_exception_handler(
context: &EmulationContext,
method_token: Token,
current_offset: u32,
exception_type: Option<Token>,
exception_state: &mut ThreadExceptionState,
skip_handler_offset: Option<u32>,
) -> Result<Option<HandlerMatch>> {
let clauses = if let Some(handlers) = context.get_synthetic_exception_handlers(method_token) {
ExceptionClause::from_metadata_handlers(&handlers)
} else {
let method = context.get_method(method_token)?;
let Some(body) = method.body.get() else {
return Ok(None);
};
ExceptionClause::from_metadata_handlers(&body.exception_handlers)
};
let mut cleanup_candidates: Vec<&ExceptionClause> = Vec::new();
for clause in &clauses {
if !clause.is_in_try(current_offset) {
continue;
}
if let Some(skip_offset) = skip_handler_offset {
if clause.handler_offset() <= skip_offset {
continue;
}
}
match clause {
ExceptionClause::Catch { catch_type, .. } => {
let is_compatible = match exception_type {
Some(exc_token) => context.is_type_compatible(exc_token, *catch_type),
None => true,
};
if is_compatible {
commit_nested_cleanups(
exception_state,
method_token,
clause,
&cleanup_candidates,
);
return Ok(Some(HandlerMatch::Catch {
method: method_token,
handler_offset: clause.handler_offset(),
}));
}
}
ExceptionClause::Filter { filter_offset, .. } => {
let handler_offset = clause.handler_offset();
commit_nested_cleanups(exception_state, method_token, clause, &cleanup_candidates);
exception_state.enter_filter(handler_offset);
return Ok(Some(HandlerMatch::Filter {
method: method_token,
filter_offset: *filter_offset,
handler_offset,
}));
}
ExceptionClause::Finally { .. } | ExceptionClause::Fault { .. } => {
cleanup_candidates.push(clause);
}
}
}
for clause in cleanup_candidates {
exception_state.push_finally(method_token, clause.handler_offset(), None);
}
Ok(None)
}
fn commit_nested_cleanups(
exception_state: &mut ThreadExceptionState,
method_token: Token,
matched: &ExceptionClause,
candidates: &[&ExceptionClause],
) {
let (outer_start, outer_end) = (matched.try_offset(), matched.try_end());
for clause in candidates {
let nested = clause.try_offset() >= outer_start && clause.try_end() <= outer_end
&& (clause.try_offset() != outer_start || clause.try_end() != outer_end);
if nested {
exception_state.push_finally(method_token, clause.handler_offset(), None);
}
}
}
pub fn schedule_finally_blocks(
context: &EmulationContext,
method_token: Token,
current_offset: u32,
leave_target: u32,
exception_state: &mut ThreadExceptionState,
) -> Result<()> {
let method = context.get_method(method_token)?;
let Some(body) = method.body.get() else {
return Ok(());
};
let clauses = ExceptionClause::from_metadata_handlers(&body.exception_handlers);
let mut finally_blocks: Vec<(u32, u32)> = Vec::new();
for clause in &clauses {
if !clause.is_finally() {
continue;
}
let inside_now = clause.is_in_try(current_offset);
let inside_target = clause.is_in_try(leave_target);
if inside_now && !inside_target {
finally_blocks.push((clause.handler_offset(), clause.try_offset()));
}
}
finally_blocks.sort_by_key(|f| f.1);
for (i, (handler_offset, _)) in finally_blocks.iter().enumerate() {
let target = if i == 0 { Some(leave_target) } else { None };
exception_state.push_finally(method_token, *handler_offset, target);
}
Ok(())
}
pub fn resolve_exception_type(exception: &EmValue, thread: &EmulationThread) -> Option<Token> {
match exception {
EmValue::ObjectRef(href) => thread.heap().get_type_token(*href).ok(),
EmValue::ValueType { type_token, .. } | EmValue::TypedRef { type_token, .. } => {
Some(*type_token)
}
_ => None,
}
}
pub fn apply_handler_match(
handler_match: &HandlerMatch,
exception: EmValue,
origin_offset: u32,
method: Token,
thread: &mut EmulationThread,
) -> Result<u32> {
match handler_match {
HandlerMatch::Catch { handler_offset, .. } => {
thread.push(exception)?;
thread.exception_state_mut().enter_catch_handler(
method,
origin_offset,
*handler_offset,
);
Ok(*handler_offset)
}
HandlerMatch::Filter { filter_offset, .. } => {
thread.push(exception)?;
Ok(*filter_offset)
}
HandlerMatch::Finally { handler_offset, .. }
| HandlerMatch::Fault { handler_offset, .. } => Ok(*handler_offset),
}
}
pub fn wrap_in_type_initialization_exception(
address_space: &AddressSpace,
inner_exception: &EmValue,
type_name: &str,
) -> Result<(EmValue, Token)> {
let tie_type = synthetic_exception::TYPE_INITIALIZATION;
let tie_ref = address_space.alloc_object(tie_type)?;
address_space.set_field(
tie_ref,
tokens::exception_fields::MESSAGE,
EmValue::ObjectRef(address_space.heap().alloc_string(type_name)?),
)?;
address_space.set_field(
tie_ref,
tokens::exception_fields::INNER_EXCEPTION,
inner_exception.clone(),
)?;
Ok((EmValue::ObjectRef(tie_ref), tie_type))
}
pub fn track_cctor_failure_if_needed(
cctor_tracker: &CctorTracker,
thread: &EmulationThread,
exception: &EmValue,
context: &EmulationContext,
) -> Result<()> {
let Some(frame) = thread.current_frame() else {
return Ok(());
};
if !frame.is_cctor() {
return Ok(());
}
let cctor_token = frame.method();
let Some(type_token) = context
.assembly()
.resolver()
.declaring_type(cctor_token)
.map(|t| t.token)
else {
return Ok(());
};
let exception_ref = match exception {
EmValue::ObjectRef(href) => *href,
other => {
let desc = format!("TypeInitializationException: {other}");
thread.heap().alloc_string(&desc)?
}
};
cctor_tracker.mark_type_failed(type_token, exception_ref)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn finally_clause(try_offset: u32, try_length: u32, handler_offset: u32) -> ExceptionClause {
ExceptionClause::Finally {
try_offset,
try_length,
handler_offset,
handler_length: 4,
}
}
fn catch_clause(try_offset: u32, try_length: u32, handler_offset: u32) -> ExceptionClause {
ExceptionClause::Catch {
try_offset,
try_length,
handler_offset,
handler_length: 4,
catch_type: Token::new(0x0100_0001),
}
}
#[test]
fn a_finally_nested_inside_the_matched_try_is_queued() {
let mut state = ThreadExceptionState::new();
let inner_finally = finally_clause(10, 10, 30);
let outer_catch = catch_clause(0, 40, 50);
commit_nested_cleanups(
&mut state,
Token::new(0x0600_0001),
&outer_catch,
&[&inner_finally],
);
assert!(state.has_pending_finally(), "the inner finally must run");
assert_eq!(state.pop_finally().map(|p| p.handler_offset), Some(30));
}
#[test]
fn a_finally_enclosing_the_matched_try_is_not_queued() {
let mut state = ThreadExceptionState::new();
let outer_finally = finally_clause(0, 100, 80);
let inner_catch = catch_clause(10, 10, 30);
commit_nested_cleanups(
&mut state,
Token::new(0x0600_0001),
&inner_catch,
&[&outer_finally],
);
assert!(!state.has_pending_finally());
}
#[test]
fn a_finally_over_the_same_try_region_is_not_queued() {
let mut state = ThreadExceptionState::new();
let finally = finally_clause(0, 20, 40);
let catch = catch_clause(0, 20, 20);
commit_nested_cleanups(&mut state, Token::new(0x0600_0001), &catch, &[&finally]);
assert!(!state.has_pending_finally());
}
}