Bridging Swift Handling Error Model with C++

Hello Swift Community! This is the second time that I'm writing a post here. The first one was to introduce myself and show my interest in the project "Bridging Swift Error Handling Model to C++" meant to be part of Google Summer of Code 2022. This time, I'm writing to present the results I achieved during its development, even after not being selected to receive the GSOC scholarship, but still greatly mentored by @Alex_L !

The two main goals of this project was:

  • Extend the C++ interface generator for a Swift module to emit C++ interfaces for Swift functions that throw, and a C++ class that represents Swift’s Error type and throw it.
  • Create a C++ class that resembles the proposed std::expected class, to provide error handling for clients that don’t use C++ exceptions.

Let's consider this Division function that can thwors as an example:

@_expose(Cxx)
public enum DivByZero : Error {
    case divisorIsZero
    case bothAreZero

    // Function to print the case thrown
    public func getMessage() {
        print(self)
    }
}

@_expose(Cxx)
public func division(_ a: Int, _ b: Int) throws -> Float {
    if a == 0 && b == 0 {
        throw DivByZero.bothAreZero
    } else if b == 0 {
        throw DivByZero.divisorIsZero
    } else {
        return Float(a / b)
    }
}

Handling Error Model with Exceptions

The first part of this project aimed to provide a minimal version of the Swift handling error model to C++, so a C++ function caller could know that a Swift function called throws an error. The second and final part of this project aimed to specialize that knowledge by providing a general Swift::Error representation, an infrastructure to recognize which error case the function threw, and a new C++ class Swift::Expected to return the error thrown.

One of the main contributions of this project was the mentioned general Swift::Error representation. This class implements its constructor and destructor using the functions swift_errorRetain and swift_errorRelease to handle the Swift Error properly avoiding unexpected behaviors and memory leaks. The main idea of this class was to provide a representation for the Swift error type that can be examined on the C++ side. This representation behaves as Swift protocol type in C++, and is able to represent all Swift error values in C++.

The C++ interface generated throws this Swift::Error if the Swift function throws an error during its execution. However, since the user would like to know which Swift Error and which case the function threw, a method to dynamically cast the Swift::Error to a concrete Swift type that represents the case thrown was implemented like as<T>(). This function is a method from the Swift::Error class and provides the user with a natural experience to handle errors in C++.

This dynamic cast return a "Swift::Optional" type within a value if the casting was successful or a "Swift::Optional::none()" if not.

// This example gets the correct value returned by the function
try {
    float result = Functions::division(4,0);
    printf("result = %f\n", result);
} catch (Swift::Error& e) {
    auto errorOpt = e.as<Functions::DivByZero>();
    assert(errorOpt.isSome());

    auto errorVal = errorOpt.get();
    errorVal.getMessage();
}

Handling Error Model without Exceptions

I developed and implemented my own version of the "std::Expected" proposal as "Swift::Expected" and implemented the support for the bridging header chose the proper way to handle a Swift thrown error according to the C++ user preference to use or not exceptions by passing the "-fno-exceptions" flag to the compiler.

I implemented the proposal with two important modifications:
The "Swift:Error" is the only type of error returned.

  • The same buffer is used to store either the error or the value of type T.
  • The last modification was crutial to optimize the memory allocation for objects from this class.

Finally, to avoid code repetition on the compiler and on the bridging header, I used an alias (ThrowingResult) and a Macro (SWIFT_RETURN_THUNK) to specify which implementation of the C++ interface would be executed to represent the Swift function regarding the error modeling choose by the C++ user. These workaroud are used as follow:

/// Example of using a Swift function that can throw an Error in C++ that can be caught.

inline Swift::ThrowingResult<float> division(swift::Int a, swift::Int b) {
  void* opaqueError = nullptr;
  void* _ctx = nullptr;
  auto returnValue = _impl::$s9Functions8divisionySfSi_SitKF(a,b,_ctx,&opaqueError);
  if (opaqueError != nullptr)
#ifdef __cpp_exceptions
    throw (Swift::Error(opaqueError));
#else
    return SWIFT_RETURN_THUNK(float, Swift::Error(opaqueError));
#endif

  return SWIFT_RETURN_THUNK(float, returnValue);
}

Where, assuming that a function returns a value, we throw or return a Swift::Error if any, regarding whether the "exception" feature is enabled. Otherwise, we just return the value within its type correctly replaced by the macro.

In this case, the real function that will be executed would looks like:

/// Example of using a Swift function that can throw an Error in C++ that can be caught.

inline float division(swift::Int a, swift::Int b) {
  void* opaqueError = nullptr;
  void* _ctx = nullptr;
  auto returnValue = _impl::$s9Functions8divisionySfSi_SitKF(a,b,_ctx,&opaqueError);
  if (opaqueError != nullptr)
    throw (Swift::Error(opaqueError));

  return returnValue;
}

On the other hand, using the Swift::Expected instead of C++ excetions the real function that will be executed would looks like:

inline Swift::Expected<float> division(swift::Int a, swift::Int b) {
  void* opaqueError = nullptr;
  void* _ctx = nullptr;
  auto returnValue = _impl::$s9Functions8divisionySfSi_SitKF(a,b,_ctx,&opaqueError);
  if (opaqueError != nullptr)
    return Swift::Expected<float>(Swift::Error(opaqueError));

  return Swift::Expected<float>(returnValue);
}

Using this new version in the user's C++ program doesn't need try-catch as expected. Instead, it takes the function's result and checks if it returns a value. If not, the function returns an error, and the user can work with it as if they had caught it.

auto result = Functions::division(0,0);
if (result.has_value()) {
    printf("result = %f\n", result.value());
} else {
    auto optionalError = result.error().as<Functions::DivByZero>();
    assert(optionalError.isSome());

    auto errorValue = optionalError.get();
    assert(errorValue == Functions::DivByZero::bothAreZero);
    errorValue.getMessage();
}

Example

This repository contains the full example mentioned here using use of both handling error models on C++: Swift and C++ Interop: Handling Error on Division Function

Presentation

This is the English version of the presentation I did as my final project to get my Computer Science bachelor: Bridging Swift Error Handling Model to C++ - Final Presentation [Portuguese] (the English version will be available soon!)

Those are the slides used in the presentation: Bridging Swift Error Handling Model to C++ [English]

9 Likes