Skip to Content
Course content

143: Swift/C++ Interop (Experimental)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

For years, if you wanted to use a C++ library in Swift, you had to play a tedious game of "telephone." You couldn't just import the C++ header; instead, you had to write a C-compatible wrapper—essentially a middleman layer of extern "C" functions—to bridge the gap. It was a lot of boilerplate for very little reward, and it completely stripped away the benefits of C++'s object-oriented nature. You'd be passing around opaque pointers (void*) and manually managing memory with malloc and free, praying you didn't leave a leak behind.

The C-Bridge Tax

Let's say we have a C++ class called PhysicsEngine that handles complex collision detection. In the naive, "old" way, your workflow looked like this: you'd write a C header that defines a function like PhysicsEngine_create(), which returns an opaque pointer, and PhysicsEngine_update(void* engine, float dt). In Swift, you'd call these functions, but you'd have no type safety. To Swift, your engine was just a pointer. If you accidentally passed a pointer to a TextureManager into PhysicsEngine_update, the compiler wouldn't blink, but your app would certainly crash.

// The naive C-Wrapper approach
// C++ Header
extern "C" {
    void* create_engine() { return new PhysicsEngine(); }
    void update_engine(void* ptr, float dt) { static_cast<PhysicsEngine*(ptr)>->update(dt); }
    void destroy_engine(void* ptr) { delete static_cast<PhysicsEngine*(ptr)>(); }
}

This approach is stable, but it's a productivity killer. You're writing the same logic three times: once in C++, once in the C wrapper, and once in the Swift call site. I've spent far too many afternoons debugging a segfault only to realize I forgot to call a destroy function in a Swift deinit.

Direct Interop and the C++ Type System

The "better" way—and the one we're exploring here—is the experimental Swift/C++ Interop. By enabling the -swift-interop-mode=CXX flag in your build settings, Swift can actually "read" C++ headers. It doesn't just see them as C; it understands C++ classes, namespaces, and even some templates.

Now, instead of that opaque pointer, Swift sees PhysicsEngine as a type. You can instantiate it directly, call its methods, and access its properties. The compiler handles the mapping. It's a massive leap in ergonomics because you're no longer fighting the language; you're just using the library. I should warn you, though: it is still marked as experimental. While it's incredibly powerful, you'll occasionally run into a C++ construct that Swift doesn't quite know how to map yet, which might force you back to a wrapper for that specific edge case.

// The Direct Interop approach
// PhysicsEngine.hpp
class PhysicsEngine {
public:
    void update(float dt);
    float getGravity() const { return -9.81f; }
};

// Swift code (with CXX interop enabled)
let engine = PhysicsEngine() 
engine.update(0.016)
print("Gravity is \(engine.getGravity())")

Where the Direct Path Gets Messy

The trade-off here isn't about stability—it's about mental overhead. When you use a C-wrapper, the boundaries are explicit. When you use direct interop, the boundaries blur, and that's where you have to be careful. For example, Swift and C++ have different ideas about memory. A C++ object created on the stack is handled differently than one created via new.

One specific pain point is std::string. Swift's String is a sophisticated, Unicode-compliant value type. C++'s std::string is essentially a mutable buffer of bytes. Swift tries to bridge these, but you'll find yourself dealing with std::string types inside your Swift code. You can't just treat them as Swift strings; you often have to perform a conversion. If you're passing strings back and forth in a tight loop, this conversion cost can add up. It's a classic engineering trade-off: you trade a bit of raw performance and a few "experimental" hiccups for a massive increase in developer velocity and type safety.




📋 Practical Task

Refactoring a Collision Library to Direct Interop

You have been handed a legacy project where a C++ CollisionDetector class is being accessed via an old-school C wrapper. Your task is to remove the wrapper and implement direct Swift/C++ interop.

The Setup: You have a C++ header CollisionDetector.hpp with a class that has a method bool checkCollision(float x1, float y1, float x2, float y2) and a property float tolerance.

Your Goal:

  1. Remove the extern "C" wrapper functions (create_detector, detector_check, etc.).
  2. Configure the Swift project to use -swift-interop-mode=CXX.
  3. Rewrite the Swift CollisionManager class to instantiate CollisionDetector directly as a C++ type.
  4. Implement a Swift function that updates the C++ tolerance value and returns the result of checkCollision without using any void* casting.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.