Make SEH exceptions use a rust_panic type instead of unsigned __int64*
This commit is contained in:
parent
83d6bf4929
commit
ad61c88e72
5 changed files with 62 additions and 154 deletions
|
@ -61,7 +61,6 @@ cfg_if::cfg_if! {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod dwarf;
|
mod dwarf;
|
||||||
mod windows;
|
|
||||||
|
|
||||||
// Entry point for catching an exception, implemented using the `try` intrinsic
|
// Entry point for catching an exception, implemented using the `try` intrinsic
|
||||||
// in the compiler.
|
// in the compiler.
|
||||||
|
|
|
@ -51,9 +51,7 @@ use alloc::boxed::Box;
|
||||||
use core::any::Any;
|
use core::any::Any;
|
||||||
use core::mem;
|
use core::mem;
|
||||||
use core::raw;
|
use core::raw;
|
||||||
|
use libc::{c_int, c_uint, c_void};
|
||||||
use crate::windows as c;
|
|
||||||
use libc::{c_int, c_uint};
|
|
||||||
|
|
||||||
// First up, a whole bunch of type definitions. There's a few platform-specific
|
// First up, a whole bunch of type definitions. There's a few platform-specific
|
||||||
// oddities here, and a lot that's just blatantly copied from LLVM. The purpose
|
// oddities here, and a lot that's just blatantly copied from LLVM. The purpose
|
||||||
|
@ -76,18 +74,19 @@ use libc::{c_int, c_uint};
|
||||||
// sort of operation. For example, if you compile this C++ code on MSVC and emit
|
// sort of operation. For example, if you compile this C++ code on MSVC and emit
|
||||||
// the LLVM IR:
|
// the LLVM IR:
|
||||||
//
|
//
|
||||||
// #include <stdin.h>
|
// #include <stdint.h>
|
||||||
|
//
|
||||||
|
// struct rust_panic {
|
||||||
|
// uint64_t x[2];
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// void foo() {
|
// void foo() {
|
||||||
// uint64_t a[2] = {0, 1};
|
// rust_panic a = {0, 1};
|
||||||
// throw a;
|
// throw a;
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// That's essentially what we're trying to emulate. Most of the constant values
|
// That's essentially what we're trying to emulate. Most of the constant values
|
||||||
// below were just copied from LLVM, I'm at least not 100% sure what's going on
|
// below were just copied from LLVM,
|
||||||
// everywhere. For example the `.PA_K\0` and `.PEA_K\0` strings below (stuck in
|
|
||||||
// the names of a few of these) I'm not actually sure what they do, but it seems
|
|
||||||
// to mirror what LLVM does!
|
|
||||||
//
|
//
|
||||||
// In any case, these structures are all constructed in a similar manner, and
|
// In any case, these structures are all constructed in a similar manner, and
|
||||||
// it's just somewhat verbose for us.
|
// it's just somewhat verbose for us.
|
||||||
|
@ -98,10 +97,9 @@ use libc::{c_int, c_uint};
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod imp {
|
mod imp {
|
||||||
pub type ptr_t = *mut u8;
|
pub type ptr_t = *mut u8;
|
||||||
pub const OFFSET: i32 = 4;
|
|
||||||
|
|
||||||
|
#[cfg(bootstrap)]
|
||||||
pub const NAME1: [u8; 7] = [b'.', b'P', b'A', b'_', b'K', 0, 0];
|
pub const NAME1: [u8; 7] = [b'.', b'P', b'A', b'_', b'K', 0, 0];
|
||||||
pub const NAME2: [u8; 7] = [b'.', b'P', b'A', b'X', 0, 0, 0];
|
|
||||||
|
|
||||||
macro_rules! ptr {
|
macro_rules! ptr {
|
||||||
(0) => (core::ptr::null_mut());
|
(0) => (core::ptr::null_mut());
|
||||||
|
@ -113,10 +111,9 @@ mod imp {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod imp {
|
mod imp {
|
||||||
pub type ptr_t = u32;
|
pub type ptr_t = u32;
|
||||||
pub const OFFSET: i32 = 8;
|
|
||||||
|
|
||||||
|
#[cfg(bootstrap)]
|
||||||
pub const NAME1: [u8; 7] = [b'.', b'P', b'E', b'A', b'_', b'K', 0];
|
pub const NAME1: [u8; 7] = [b'.', b'P', b'E', b'A', b'_', b'K', 0];
|
||||||
pub const NAME2: [u8; 7] = [b'.', b'P', b'E', b'A', b'X', 0, 0];
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
pub static __ImageBase: u8;
|
pub static __ImageBase: u8;
|
||||||
|
@ -141,7 +138,7 @@ pub struct _ThrowInfo {
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct _CatchableTypeArray {
|
pub struct _CatchableTypeArray {
|
||||||
pub nCatchableTypes: c_int,
|
pub nCatchableTypes: c_int,
|
||||||
pub arrayOfCatchableTypes: [imp::ptr_t; 2],
|
pub arrayOfCatchableTypes: [imp::ptr_t; 1],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
@ -164,9 +161,19 @@ pub struct _PMD {
|
||||||
pub struct _TypeDescriptor {
|
pub struct _TypeDescriptor {
|
||||||
pub pVFTable: *const u8,
|
pub pVFTable: *const u8,
|
||||||
pub spare: *mut u8,
|
pub spare: *mut u8,
|
||||||
|
#[cfg(bootstrap)]
|
||||||
pub name: [u8; 7],
|
pub name: [u8; 7],
|
||||||
|
#[cfg(not(bootstrap))]
|
||||||
|
pub name: [u8; 11],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that we intentionally ignore name mangling rules here: we don't want C++
|
||||||
|
// to be able to catch Rust panics by simply declaring a `struct rust_panic`.
|
||||||
|
#[cfg(bootstrap)]
|
||||||
|
use imp::NAME1 as TYPE_NAME;
|
||||||
|
#[cfg(not(bootstrap))]
|
||||||
|
const TYPE_NAME: [u8; 11] = *b"rust_panic\0";
|
||||||
|
|
||||||
static mut THROW_INFO: _ThrowInfo = _ThrowInfo {
|
static mut THROW_INFO: _ThrowInfo = _ThrowInfo {
|
||||||
attributes: 0,
|
attributes: 0,
|
||||||
pnfnUnwind: ptr!(0),
|
pnfnUnwind: ptr!(0),
|
||||||
|
@ -175,31 +182,22 @@ static mut THROW_INFO: _ThrowInfo = _ThrowInfo {
|
||||||
};
|
};
|
||||||
|
|
||||||
static mut CATCHABLE_TYPE_ARRAY: _CatchableTypeArray = _CatchableTypeArray {
|
static mut CATCHABLE_TYPE_ARRAY: _CatchableTypeArray = _CatchableTypeArray {
|
||||||
nCatchableTypes: 2,
|
nCatchableTypes: 1,
|
||||||
arrayOfCatchableTypes: [ptr!(0), ptr!(0)],
|
arrayOfCatchableTypes: [ptr!(0)],
|
||||||
};
|
};
|
||||||
|
|
||||||
static mut CATCHABLE_TYPE1: _CatchableType = _CatchableType {
|
static mut CATCHABLE_TYPE: _CatchableType = _CatchableType {
|
||||||
properties: 1,
|
properties: 0,
|
||||||
pType: ptr!(0),
|
pType: ptr!(0),
|
||||||
thisDisplacement: _PMD {
|
thisDisplacement: _PMD {
|
||||||
mdisp: 0,
|
mdisp: 0,
|
||||||
pdisp: -1,
|
pdisp: -1,
|
||||||
vdisp: 0,
|
vdisp: 0,
|
||||||
},
|
},
|
||||||
sizeOrOffset: imp::OFFSET,
|
#[cfg(bootstrap)]
|
||||||
copy_function: ptr!(0),
|
sizeOrOffset: mem::size_of::<*mut u64>() as c_int,
|
||||||
};
|
#[cfg(not(bootstrap))]
|
||||||
|
sizeOrOffset: mem::size_of::<[u64; 2]>() as c_int,
|
||||||
static mut CATCHABLE_TYPE2: _CatchableType = _CatchableType {
|
|
||||||
properties: 1,
|
|
||||||
pType: ptr!(0),
|
|
||||||
thisDisplacement: _PMD {
|
|
||||||
mdisp: 0,
|
|
||||||
pdisp: -1,
|
|
||||||
vdisp: 0,
|
|
||||||
},
|
|
||||||
sizeOrOffset: imp::OFFSET,
|
|
||||||
copy_function: ptr!(0),
|
copy_function: ptr!(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -221,16 +219,10 @@ extern "C" {
|
||||||
//
|
//
|
||||||
// Again, I'm not entirely sure what this is describing, it just seems to work.
|
// Again, I'm not entirely sure what this is describing, it just seems to work.
|
||||||
#[cfg_attr(not(test), lang = "msvc_try_filter")]
|
#[cfg_attr(not(test), lang = "msvc_try_filter")]
|
||||||
static mut TYPE_DESCRIPTOR1: _TypeDescriptor = _TypeDescriptor {
|
static mut TYPE_DESCRIPTOR: _TypeDescriptor = _TypeDescriptor {
|
||||||
pVFTable: unsafe { &TYPE_INFO_VTABLE } as *const _ as *const _,
|
pVFTable: unsafe { &TYPE_INFO_VTABLE } as *const _ as *const _,
|
||||||
spare: core::ptr::null_mut(),
|
spare: core::ptr::null_mut(),
|
||||||
name: imp::NAME1,
|
name: TYPE_NAME,
|
||||||
};
|
|
||||||
|
|
||||||
static mut TYPE_DESCRIPTOR2: _TypeDescriptor = _TypeDescriptor {
|
|
||||||
pVFTable: unsafe { &TYPE_INFO_VTABLE } as *const _ as *const _,
|
|
||||||
spare: core::ptr::null_mut(),
|
|
||||||
name: imp::NAME2,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
|
pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
|
||||||
|
@ -246,6 +238,11 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
|
||||||
let ptrs = mem::transmute::<_, raw::TraitObject>(data);
|
let ptrs = mem::transmute::<_, raw::TraitObject>(data);
|
||||||
let mut ptrs = [ptrs.data as u64, ptrs.vtable as u64];
|
let mut ptrs = [ptrs.data as u64, ptrs.vtable as u64];
|
||||||
let mut ptrs_ptr = ptrs.as_mut_ptr();
|
let mut ptrs_ptr = ptrs.as_mut_ptr();
|
||||||
|
let throw_ptr = if cfg!(bootstrap) {
|
||||||
|
&mut ptrs_ptr as *mut _ as *mut _
|
||||||
|
} else {
|
||||||
|
ptrs_ptr as *mut _
|
||||||
|
};
|
||||||
|
|
||||||
// This... may seems surprising, and justifiably so. On 32-bit MSVC the
|
// This... may seems surprising, and justifiably so. On 32-bit MSVC the
|
||||||
// pointers between these structure are just that, pointers. On 64-bit MSVC,
|
// pointers between these structure are just that, pointers. On 64-bit MSVC,
|
||||||
|
@ -270,17 +267,17 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
|
||||||
atomic_store(&mut THROW_INFO.pCatchableTypeArray as *mut _ as *mut u32,
|
atomic_store(&mut THROW_INFO.pCatchableTypeArray as *mut _ as *mut u32,
|
||||||
ptr!(&CATCHABLE_TYPE_ARRAY as *const _) as u32);
|
ptr!(&CATCHABLE_TYPE_ARRAY as *const _) as u32);
|
||||||
atomic_store(&mut CATCHABLE_TYPE_ARRAY.arrayOfCatchableTypes[0] as *mut _ as *mut u32,
|
atomic_store(&mut CATCHABLE_TYPE_ARRAY.arrayOfCatchableTypes[0] as *mut _ as *mut u32,
|
||||||
ptr!(&CATCHABLE_TYPE1 as *const _) as u32);
|
ptr!(&CATCHABLE_TYPE as *const _) as u32);
|
||||||
atomic_store(&mut CATCHABLE_TYPE_ARRAY.arrayOfCatchableTypes[1] as *mut _ as *mut u32,
|
atomic_store(&mut CATCHABLE_TYPE.pType as *mut _ as *mut u32,
|
||||||
ptr!(&CATCHABLE_TYPE2 as *const _) as u32);
|
ptr!(&TYPE_DESCRIPTOR as *const _) as u32);
|
||||||
atomic_store(&mut CATCHABLE_TYPE1.pType as *mut _ as *mut u32,
|
|
||||||
ptr!(&TYPE_DESCRIPTOR1 as *const _) as u32);
|
|
||||||
atomic_store(&mut CATCHABLE_TYPE2.pType as *mut _ as *mut u32,
|
|
||||||
ptr!(&TYPE_DESCRIPTOR2 as *const _) as u32);
|
|
||||||
|
|
||||||
c::_CxxThrowException(&mut ptrs_ptr as *mut _ as *mut _,
|
extern "system" {
|
||||||
&mut THROW_INFO as *mut _ as *mut _);
|
#[unwind(allowed)]
|
||||||
u32::max_value()
|
pub fn _CxxThrowException(pExceptionObject: *mut c_void, pThrowInfo: *mut u8) -> !;
|
||||||
|
}
|
||||||
|
|
||||||
|
_CxxThrowException(throw_ptr,
|
||||||
|
&mut THROW_INFO as *mut _ as *mut _);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn payload() -> [u64; 2] {
|
pub fn payload() -> [u64; 2] {
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
#![allow(nonstandard_style)]
|
|
||||||
#![allow(dead_code)]
|
|
||||||
#![cfg(windows)]
|
|
||||||
|
|
||||||
use libc::{c_long, c_ulong, c_void};
|
|
||||||
|
|
||||||
pub type DWORD = c_ulong;
|
|
||||||
pub type LONG = c_long;
|
|
||||||
pub type ULONG_PTR = usize;
|
|
||||||
pub type LPVOID = *mut c_void;
|
|
||||||
|
|
||||||
pub const EXCEPTION_MAXIMUM_PARAMETERS: usize = 15;
|
|
||||||
pub const EXCEPTION_NONCONTINUABLE: DWORD = 0x1; // Noncontinuable exception
|
|
||||||
pub const EXCEPTION_UNWINDING: DWORD = 0x2; // Unwind is in progress
|
|
||||||
pub const EXCEPTION_EXIT_UNWIND: DWORD = 0x4; // Exit unwind is in progress
|
|
||||||
pub const EXCEPTION_TARGET_UNWIND: DWORD = 0x20; // Target unwind in progress
|
|
||||||
pub const EXCEPTION_COLLIDED_UNWIND: DWORD = 0x40; // Collided exception handler call
|
|
||||||
pub const EXCEPTION_UNWIND: DWORD = EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND |
|
|
||||||
EXCEPTION_TARGET_UNWIND |
|
|
||||||
EXCEPTION_COLLIDED_UNWIND;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct EXCEPTION_RECORD {
|
|
||||||
pub ExceptionCode: DWORD,
|
|
||||||
pub ExceptionFlags: DWORD,
|
|
||||||
pub ExceptionRecord: *mut EXCEPTION_RECORD,
|
|
||||||
pub ExceptionAddress: LPVOID,
|
|
||||||
pub NumberParameters: DWORD,
|
|
||||||
pub ExceptionInformation: [LPVOID; EXCEPTION_MAXIMUM_PARAMETERS],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct EXCEPTION_POINTERS {
|
|
||||||
pub ExceptionRecord: *mut EXCEPTION_RECORD,
|
|
||||||
pub ContextRecord: *mut CONTEXT,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum UNWIND_HISTORY_TABLE {}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct RUNTIME_FUNCTION {
|
|
||||||
pub BeginAddress: DWORD,
|
|
||||||
pub EndAddress: DWORD,
|
|
||||||
pub UnwindData: DWORD,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CONTEXT {}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct DISPATCHER_CONTEXT {
|
|
||||||
pub ControlPc: LPVOID,
|
|
||||||
pub ImageBase: LPVOID,
|
|
||||||
pub FunctionEntry: *const RUNTIME_FUNCTION,
|
|
||||||
pub EstablisherFrame: LPVOID,
|
|
||||||
pub TargetIp: LPVOID,
|
|
||||||
pub ContextRecord: *const CONTEXT,
|
|
||||||
pub LanguageHandler: LPVOID,
|
|
||||||
pub HandlerData: *const u8,
|
|
||||||
pub HistoryTable: *const UNWIND_HISTORY_TABLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub enum EXCEPTION_DISPOSITION {
|
|
||||||
ExceptionContinueExecution,
|
|
||||||
ExceptionContinueSearch,
|
|
||||||
ExceptionNestedException,
|
|
||||||
ExceptionCollidedUnwind,
|
|
||||||
}
|
|
||||||
pub use self::EXCEPTION_DISPOSITION::*;
|
|
||||||
|
|
||||||
extern "system" {
|
|
||||||
#[unwind(allowed)]
|
|
||||||
pub fn RaiseException(dwExceptionCode: DWORD,
|
|
||||||
dwExceptionFlags: DWORD,
|
|
||||||
nNumberOfArguments: DWORD,
|
|
||||||
lpArguments: *const ULONG_PTR);
|
|
||||||
#[unwind(allowed)]
|
|
||||||
pub fn RtlUnwindEx(TargetFrame: LPVOID,
|
|
||||||
TargetIp: LPVOID,
|
|
||||||
ExceptionRecord: *const EXCEPTION_RECORD,
|
|
||||||
ReturnValue: LPVOID,
|
|
||||||
OriginalContext: *const CONTEXT,
|
|
||||||
HistoryTable: *const UNWIND_HISTORY_TABLE);
|
|
||||||
#[unwind(allowed)]
|
|
||||||
pub fn _CxxThrowException(pExceptionObject: *mut c_void, pThrowInfo: *mut u8);
|
|
||||||
}
|
|
|
@ -849,7 +849,7 @@ fn codegen_msvc_try(
|
||||||
// We're generating an IR snippet that looks like:
|
// We're generating an IR snippet that looks like:
|
||||||
//
|
//
|
||||||
// declare i32 @rust_try(%func, %data, %ptr) {
|
// declare i32 @rust_try(%func, %data, %ptr) {
|
||||||
// %slot = alloca i64*
|
// %slot = alloca [2 x i64]
|
||||||
// invoke %func(%data) to label %normal unwind label %catchswitch
|
// invoke %func(%data) to label %normal unwind label %catchswitch
|
||||||
//
|
//
|
||||||
// normal:
|
// normal:
|
||||||
|
@ -873,21 +873,25 @@ fn codegen_msvc_try(
|
||||||
//
|
//
|
||||||
// #include <stdint.h>
|
// #include <stdint.h>
|
||||||
//
|
//
|
||||||
|
// struct rust_panic {
|
||||||
|
// uint64_t x[2];
|
||||||
|
// }
|
||||||
|
//
|
||||||
// int bar(void (*foo)(void), uint64_t *ret) {
|
// int bar(void (*foo)(void), uint64_t *ret) {
|
||||||
// try {
|
// try {
|
||||||
// foo();
|
// foo();
|
||||||
// return 0;
|
// return 0;
|
||||||
// } catch(uint64_t a[2]) {
|
// } catch(rust_panic a) {
|
||||||
// ret[0] = a[0];
|
// ret[0] = a.x[0];
|
||||||
// ret[1] = a[1];
|
// ret[1] = a.x[1];
|
||||||
// return 1;
|
// return 1;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// More information can be found in libstd's seh.rs implementation.
|
// More information can be found in libstd's seh.rs implementation.
|
||||||
let i64p = bx.type_ptr_to(bx.type_i64());
|
let i64_2 = bx.type_array(bx.type_i64(), 2);
|
||||||
let ptr_align = bx.tcx().data_layout.pointer_align.abi;
|
let i64_align = bx.tcx().data_layout.i64_align.abi;
|
||||||
let slot = bx.alloca(i64p, ptr_align);
|
let slot = bx.alloca(i64_2, i64_align);
|
||||||
bx.invoke(func, &[data], normal.llbb(), catchswitch.llbb(), None);
|
bx.invoke(func, &[data], normal.llbb(), catchswitch.llbb(), None);
|
||||||
|
|
||||||
normal.ret(bx.const_i32(0));
|
normal.ret(bx.const_i32(0));
|
||||||
|
@ -900,17 +904,10 @@ fn codegen_msvc_try(
|
||||||
None => bug!("msvc_try_filter not defined"),
|
None => bug!("msvc_try_filter not defined"),
|
||||||
};
|
};
|
||||||
let funclet = catchpad.catch_pad(cs, &[tydesc, bx.const_i32(0), slot]);
|
let funclet = catchpad.catch_pad(cs, &[tydesc, bx.const_i32(0), slot]);
|
||||||
let addr = catchpad.load(slot, ptr_align);
|
|
||||||
|
|
||||||
let i64_align = bx.tcx().data_layout.i64_align.abi;
|
let payload = catchpad.load(slot, i64_align);
|
||||||
let arg1 = catchpad.load(addr, i64_align);
|
let local_ptr = catchpad.bitcast(local_ptr, bx.type_ptr_to(i64_2));
|
||||||
let val1 = bx.const_i32(1);
|
catchpad.store(payload, local_ptr, i64_align);
|
||||||
let gep1 = catchpad.inbounds_gep(addr, &[val1]);
|
|
||||||
let arg2 = catchpad.load(gep1, i64_align);
|
|
||||||
let local_ptr = catchpad.bitcast(local_ptr, i64p);
|
|
||||||
let gep2 = catchpad.inbounds_gep(local_ptr, &[val1]);
|
|
||||||
catchpad.store(arg1, local_ptr, i64_align);
|
|
||||||
catchpad.store(arg2, gep2, i64_align);
|
|
||||||
catchpad.catch_ret(&funclet, caught.llbb());
|
catchpad.catch_ret(&funclet, caught.llbb());
|
||||||
|
|
||||||
caught.ret(bx.const_i32(1));
|
caught.ret(bx.const_i32(1));
|
||||||
|
|
|
@ -8,6 +8,7 @@ void println(const char* s) {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct exception {};
|
struct exception {};
|
||||||
|
struct rust_panic {};
|
||||||
|
|
||||||
struct drop_check {
|
struct drop_check {
|
||||||
bool* ok;
|
bool* ok;
|
||||||
|
@ -45,7 +46,7 @@ extern "C" {
|
||||||
x.ok = NULL;
|
x.ok = NULL;
|
||||||
try {
|
try {
|
||||||
cb();
|
cb();
|
||||||
} catch (exception e) {
|
} catch (rust_panic e) {
|
||||||
assert(false && "shouldn't be able to catch a rust panic");
|
assert(false && "shouldn't be able to catch a rust panic");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
println("caught foreign exception in catch (...)");
|
println("caught foreign exception in catch (...)");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue