Skip to content
Merged
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
111 changes: 108 additions & 3 deletions compiler/llvm_backend/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ impl<'a> CodegenEngine<'a> {
let dest_block = self.ctx.get_block(*dest).unwrap();
LLVMBuildBr(self.ctx.builder, dest_block);
}
TerminatorKind::Throw(_) => {
return Err("Exception handling (throw) is not yet implemented in the LLVM backend".to_string());
<
}
TerminatorKind::ConditionalJump {
cond,
Expand Down Expand Up @@ -1057,7 +1056,113 @@ impl<'a> CodegenEngine<'a> {
CString::new("cast").unwrap().as_ptr(),
)
}
Op::Try { .. } | Op::EndTry | Op::MakeDslBlock { .. } | Op::NoOp => return Ok(()),
Op::Try { catch_block, catch_var } => {
let i8_ptr_ty = LLVMPointerType(LLVMInt8TypeInContext(context), 0);
let fn_push = self.get_or_declare_runtime_fn("ts_try_push", i8_ptr_ty, &[]);
let buf_ptr = LLVMBuildCall2(
self.ctx.builder,
LLVMTypeOf(fn_push),
fn_push,
[].as_mut_ptr(),
0,
CString::new("jmp_buf").unwrap().as_ptr(),
);

let setjmp_fn = self.get_or_declare_runtime_fn(
#[cfg(target_os = "windows")]
"_setjmp",
#[cfg(not(target_os = "windows"))]
"setjmp",
i32_ty,
&[i8_ptr_ty],
);

let res = LLVMBuildCall2(
self.ctx.builder,
LLVMTypeOf(setjmp_fn),
setjmp_fn,
[buf_ptr].as_mut_ptr(),
1,
CString::new("setjmp_res").unwrap().as_ptr(),
);

let zero = LLVMConstInt(i32_ty, 0, 0);
let is_exception = LLVMBuildICmp(
self.ctx.builder,
llvm_sys::LLVMIntPredicate::LLVMIntNE,
res,
zero,
CString::new("is_exception").unwrap().as_ptr(),
);

// We need to split the block here because setjmp acts as a conditional branch point
let current_block = LLVMGetInsertBlock(self.ctx.builder);
let func = LLVMGetBasicBlockParent(current_block);

let cont_block = LLVMAppendBasicBlockInContext(
context,
func,
CString::new("try_continue").unwrap().as_ptr(),
);

let catch_target = self.ctx.get_block(*catch_block).unwrap();

// We create a dispatch block for the exception path
let dispatch_block = LLVMAppendBasicBlockInContext(
context,
func,
CString::new("try_dispatch").unwrap().as_ptr(),
);

LLVMBuildCondBr(self.ctx.builder, is_exception, dispatch_block, cont_block);

// exception path
LLVMPositionBuilderAtEnd(self.ctx.builder, dispatch_block);

// Clean up the jmp_buf since we arrived here via longjmp and ts_try_pop wasn't called
let fn_free_buf = self.get_or_declare_runtime_fn("ts_try_free", LLVMVoidTypeInContext(context), &[i8_ptr_ty]);
LLVMBuildCall2(
self.ctx.builder,
LLVMTypeOf(fn_free_buf),
fn_free_buf,
[buf_ptr].as_mut_ptr(),
1,
CString::new("").unwrap().as_ptr(),
);

let fn_get_ex = self.get_or_declare_runtime_fn("ts_get_exception", i8_ptr_ty, &[]);
let ex_val = LLVMBuildCall2(
self.ctx.builder,
LLVMTypeOf(fn_get_ex),
fn_get_ex,
[].as_mut_ptr(),
0,
CString::new("ex_val").unwrap().as_ptr(),
);

if let Some(target) = self.ctx.get_local(*catch_var) {
LLVMBuildStore(self.ctx.builder, ex_val, target);
}
LLVMBuildBr(self.ctx.builder, catch_target);

// continue path
LLVMPositionBuilderAtEnd(self.ctx.builder, cont_block);

return Ok(());
}
Op::EndTry => {
let fn_pop = self.get_or_declare_runtime_fn("ts_try_pop", LLVMVoidTypeInContext(context), &[]);
LLVMBuildCall2(
self.ctx.builder,
LLVMTypeOf(fn_pop),
fn_pop,
[].as_mut_ptr(),
0,
CString::new("").unwrap().as_ptr(),
);
return Ok(());
}
Op::MakeDslBlock { .. } | Op::NoOp => return Ok(()),
};

if let Some(res_id) = inst.result {
Expand Down
106 changes: 106 additions & 0 deletions runtime/native_runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,3 +1018,109 @@ pub unsafe extern "C" fn ts_await(val: *mut TsValue) -> *mut TsValue {
}
val
}

use std::cell::RefCell;
use std::ffi::c_int;

// `libc::jmp_buf` is not exposed in the `libc` crate consistently across platforms,
// so we define a generic opaque struct that is large enough and aligned.
// A size of 2048 bytes (256 * 8) is vastly larger than needed by any OS for jmp_buf.
#[repr(C, align(16))]
pub struct JmpBuf {
data: [u64; 256],
}

extern "C" {
#[cfg_attr(target_os = "windows", link_name = "_setjmp")]
pub fn setjmp(env: *mut JmpBuf) -> c_int;
pub fn longjmp(env: *mut JmpBuf, val: c_int) -> !;
}

thread_local! {
static TRY_STACK: RefCell<Vec<*mut JmpBuf>> = RefCell::new(Vec::new());
static PENDING_EXCEPTION: RefCell<*mut TsValue> = RefCell::new(std::ptr::null_mut());
}

#[no_mangle]
pub extern "C" fn ts_try_push() -> *mut JmpBuf {
// We allocate a jmp_buf on the heap and keep it on the stack.
let layout = std::alloc::Layout::new::<JmpBuf>();
let ptr = unsafe { std::alloc::alloc_zeroed(layout) as *mut JmpBuf };
TRY_STACK.with(|stack| {
stack.borrow_mut().push(ptr);
});
ptr
}

#[no_mangle]
pub extern "C" fn ts_try_pop() {
let ptr = TRY_STACK.with(|stack| stack.borrow_mut().pop());
if let Some(p) = ptr {
let layout = std::alloc::Layout::new::<JmpBuf>();
unsafe {
std::alloc::dealloc(p as *mut u8, layout);
}
}
}

#[no_mangle]
pub unsafe extern "C" fn ts_throw(val: *mut TsValue) -> ! {
PENDING_EXCEPTION.with(|exc| {
*exc.borrow_mut() = val;
});

let buf_ptr = TRY_STACK.with(|stack| {
let mut s = stack.borrow_mut();
if let Some(buf) = s.pop() {
buf
} else {
std::ptr::null_mut()
}
});

if buf_ptr.is_null() {
// Uncaught exception! We should abort or panic.
let msg = if val.is_null() {
"null".to_string()
} else {
let v = &*val;
if v.tag == TsTag::String as u32 {
let s = &*(v.data.pointer as *const String);
s.clone()
} else {
format!("exception tag {}", v.tag)
}
};
eprintln!("Uncaught Exception: {}", msg);
std::process::abort();
}

// Call longjmp (memory leak of the jmp buf happens here if we don't deallocate first!
// But since longjmp doesn't return, we can't deallocate it after.
// It's safe to deallocate before calling longjmp since setjmp execution is fully complete and we just need the values out of the struct. Wait, longjmp uses the buf ptr so we shouldn't dealloc before longjmp. We'll leave it allocated, but wait! longjmp jumps to the setjmp frame, which can deallocate the buffer. So we should NOT deallocate in try_pop if we arrived via longjmp, OR we deallocate the buffer in the Catch block via LLVM IR calling ts_free_buf, and remove ts_try_pop.
// Instead of leaking, let's keep it simple: ts_try_pop() frees it, so if an exception is thrown, ts_try_pop is skipped (because the instruction is after the Try block), so the Catch block needs to call ts_try_pop or equivalent?
// Let's create a dedicated function to clean it up after an exception.

// Call longjmp
longjmp(buf_ptr, 1);
}

#[no_mangle]
pub extern "C" fn ts_try_free(buf_ptr: *mut JmpBuf) {
if !buf_ptr.is_null() {
let layout = std::alloc::Layout::new::<JmpBuf>();
unsafe {
std::alloc::dealloc(buf_ptr as *mut u8, layout);
}
}
}

#[no_mangle]
pub extern "C" fn ts_get_exception() -> *mut TsValue {
PENDING_EXCEPTION.with(|exc| {
let mut e = exc.borrow_mut();
let val = *e;
*e = std::ptr::null_mut();
val
})
}
Binary file added test_libc
Binary file not shown.
4 changes: 4 additions & 0 deletions test_libc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extern crate libc;
fn main() {
println!("{}", std::mem::size_of::<libc::jmp_buf>());
}
Loading