9
$\begingroup$

What are the most common ways of compiling code of a language with first-class support for exceptions, such as C++? Panics in Rust, which have similar behavior, shouldn't be too different.

From my experience, "local jumps" such as continue or break doesn't seem too hard. The problem with exception appears when one is deep in the call stack. How do common implementations handle this?

$\endgroup$
1
  • $\begingroup$ How to compile exceptions is partly dependent on the semantics of exceptions that you want. See, for example, this C++ proposal which seeks to make exceptions as efficient as return codes by compiling them like return codes. open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0709r4.pdf $\endgroup$
    – Pseudonym
    May 18, 2023 at 1:04

3 Answers 3

6
$\begingroup$

As syntax sugar for something else

Swift uses this to interface with Objective-C's out-param errors. For example, this set of functions:

func foo() throws -> Int {
  if .random() {
    return 0
  } else {
    throw MyError()
  }
}

func bar() throws {
  _ = try foo()
}

func baz() -> Int {
  do {
    return try foo()
  } catch {
    print(error)
    return 2
  }
}

Would be compiled to something vaguely equivalent to this:

func foo(error: inout Error?) -> Int {
  if .random() {
    return 0
  } else {
    error = MyError()
    return /* some irrelevant value */
  }
}

func bar(error: inout Error?) {
  _ = foo(error: &error)
}

func baz() -> Int {
  var error: Error?
  let result = foo(error: &error)
  if let error {
    print(error)
    return 2
  }
  return result
}

NB: I don't know if Swift does this in general, or only for @objc methods.

Edit to add: It might be even more straightforward to compile this as a "result" type, if you have one:

func foo() -> Result<Int, Error> {
  if .random() {
    return .success(0)
  } else {
    return .failure(MyError())
  }
}

func bar() -> Result<Void, Error> {
  foo().map { _ in }
}

func baz() -> Int {
  switch foo() {
  case .success(let result):
    return result
  case .failure(let error):
    print(error)
    return 2
  }
}
$\endgroup$
5
$\begingroup$

LLVM has a pretty good explanation for exception handling.

Let's start with a simple C++ routine:

int bar() {
  return 1713;
}

int foo() {
  return bar();
}

Using Clang, we can pass the arguments -emit-llvm -S to see that the first function becomes:

  ret i32 1713, !dbg !16    # return 1713

and the second function becomes:

  %1 = call noundef i32 @_Z3barv(), !dbg !18    # call bar()
  ret i32 %1, !dbg !19

I've embedded my own comments above to make the assembly easier to follow.


Now let's introduce some exceptions:

int bar() {
  throw(1713);
  return 0;
}

int foo() {
  try {
    return bar();
  }
  catch (...) {
    return 2723;
  }
}

The critical part of the first function becomes:

  %1 = call ptr @__cxa_allocate_exception(i64 4) #2, !dbg !16
  store i32 1713, ptr %1, align 16, !dbg !16
  call void @__cxa_throw(ptr %1, ptr @_ZTIi, ptr null) #3, !dbg !16

It allocates an exception structure via __cxa_allocate_exception and then passes this structure to __cxa_throw.

The try block becomes:

  %4 = invoke noundef i32 @_Z3barv() to label %5 unwind label %6, !dbg !18

5:                                                ; preds = %0
  store i32 %4, ptr %1, align 4, !dbg !20
  br label %14, !dbg !20

Instead of call, we now have invoke, which includes the goto label for success (%5 above) and the goto label for failure (%6 below).

Finally, the catch becomes:

6:                                                ; preds = %0
  %7 = landingpad { ptr, i32 } catch ptr null, !dbg !21
  %8 = extractvalue { ptr, i32 } %7, 0, !dbg !21
  store ptr %8, ptr %2, align 8, !dbg !21
  %9 = extractvalue { ptr, i32 } %7, 1, !dbg !21
  store i32 %9, ptr %3, align 4, !dbg !21
  br label %10, !dbg !21

10:                                               ; preds = %6
  %11 = load ptr, ptr %2, align 8, !dbg !22
  %12 = call ptr @__cxa_begin_catch(ptr %11) #2, !dbg !22
  store i32 2723, ptr %1, align 4, !dbg !23    # return 2723
  call void @__cxa_end_catch(), !dbg !25
  br label %14

The "landing pad" is similar to a function entry point. It must select which catch block to invoke based on the type of the exception. The actual code of the handler is between __cxa_begin_catch and __cxa_end_catch.

$\endgroup$
4
$\begingroup$

setjmp and longjmp are two C standard library functions for non-local jumps like this. setjmp saves the current environment into a buffer, including the stack and program counter, and longjmp takes the program back to the corresponding setjmp call at a later time. setjmp returns zero the first time, and something else when you longjmp back to it, so the code can branch how it handles the two cases just like a fork.

These functions are available in the C standard library, and consequently for many native-code compilation targets on common platforms, regardless of whether C itself is involved in the implementation. Each try-catch calls setjmp and puts the buffer on a stack, and when an exception is thrown longjmp is called to jump to the nearest exception handler.

The exception-handling code needs to deal with any cleanup required by the stack frames jumped over, re-jumping to the next handler if the exception doesn't match, and maintaining the stack of jump buffers whether an exception was thrown or not. In many cases, however, these two functions do exactly all of the hard work of exception handling and come free with the platform.

I have used these for exception handlers in the past. They're not the most efficient mechanism if you're expecting a large amount of exception handling, but they work very well for fairly little complication in your program. In the first instance this is likely to be the most common mechanism used for a language compiler targetting just the local C ABI.


Formally, you can only longjmp up the stack and not down, so this approach doesn't help if you want to have "resumable" exceptions that can continue from the original throw site. In practice, you may be able to get away with it on common platforms, though I wouldn't want to rely on that. If you want to have a more unusual sort of exception like that, this probably won't be the reliable approach for you.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .