Welcome to Phase 6. The three games run beautifully in a terminal — but no one will use them there. To ship a real macOS app or Windows app, the C++ engine has to talk to a UI written in some other language. On macOS that's Swift. On Windows that's C#. Both can call into C++ — but only through a narrow doorway, and that doorway is plain C.

The trick is a bridge header: a small C-style API on top of the C++ engine. Every host language can call C, because every operating system is built on it. The C++ classes hide behind opaque pointers and a handful of free functions; from the Swift or C# side it looks like a friendly C library.

Why C, not C++

C++ has classes, namespaces, templates, name mangling, exceptions, and ABIs that change between compiler versions. Swift, C#, Python, Rust, and JavaScript don't speak any of those things. They speak C — flat function calls, primitive types, opaque pointers, no exceptions, predictable layouts. C is the lingua franca of operating systems, and the language every other language has bindings for.

So we don't try to expose ChessEngine::best_move(Board&, int) directly. We expose chess_best_move(void* engine, int depth, char* out_move). The C++ implementation lives behind it, but the surface is pure C.

The bridge header

// chess_bridge.h — pure C, included from C++, Swift (via modulemap),
// and C# (via [DllImport]).
#ifndef CHESS_BRIDGE_H
#define CHESS_BRIDGE_H

#ifdef __cplusplus
extern "C" {
#endif

typedef struct ChessEngine ChessEngine;     // opaque

ChessEngine* chess_create(void);
void         chess_destroy(ChessEngine*);

// FEN string in / out — the standard chess position notation.
void         chess_set_position_fen(ChessEngine*, const char* fen);
void         chess_get_position_fen(ChessEngine*, char* out_buf, int buf_len);

// "e2e4" / "e7e8q" style moves.
int          chess_apply_move(ChessEngine*, const char* move);     // 0=ok, 1=illegal
void         chess_best_move(ChessEngine*, int time_ms,
                              char* out_buf, int buf_len);

#ifdef __cplusplus
}     // extern "C"
#endif

#endif

Six functions. Pure C types: pointers, const char*, ints, output buffers. The opaque typedef struct ChessEngine ChessEngine means callers can hold an engine handle without knowing what's inside. The extern "C" wrapper, on the C++ side, tells the compiler to drop name mangling so the symbols look like plain C functions in the linker output.

The C++ implementation behind the bridge

// chess_bridge.cpp — implemented in C++, compiled into the same binary.
#include "chess_bridge.h"
#include "engine.hpp"     // real C++ engine

struct ChessEngine {
    engine::Engine impl;
};

extern "C" ChessEngine* chess_create(void) {
    return new ChessEngine{};
}

extern "C" void chess_destroy(ChessEngine* e) {
    delete e;
}

extern "C" int chess_apply_move(ChessEngine* e, const char* m) {
    try {
        return e->impl.apply(m) ? 0 : 1;
    } catch (...) {
        return 2;
    }
}

// …and similar wrappers around best_move, get_position_fen, etc.

Note the try/catch(...). C exception unwinding across language boundaries is undefined behaviour — Swift and C# can't reliably catch a C++ exception. So the bridge swallows them and converts to a status code. That's the contract: this is a C function and never throws.

One more rule: every pointer that crosses the boundary has clear ownership. chess_create returns a pointer the caller must release with chess_destroy. Output buffers are allocated by the caller and filled by the function. The C side never frees memory the C++ side allocated, and vice versa. Cross-allocator memory transfer is the most common source of crashes in language bridges.

How Swift sees it

// In Swift — given a modulemap that imports chess_bridge.h
import ChessBridge

final class Chess {
    private let handle: OpaquePointer

    init() {
        guard let h = chess_create() else { fatalError() }
        self.handle = h
    }
    deinit { chess_destroy(handle) }

    func apply(_ move: String) -> Bool {
        return chess_apply_move(handle, move) == 0
    }
}

Swift wraps the opaque pointer in a class. init creates the engine, deinit destroys it — Swift's automatic reference counting takes care of the lifetime. The C function is directly callable: no glue code, no marshalling layer. That's the magic of the bridge.

How C# sees it

using System;
using System.Runtime.InteropServices;

internal static class ChessNative {
    [DllImport("chess.dll")]
    public static extern IntPtr chess_create();

    [DllImport("chess.dll")]
    public static extern void chess_destroy(IntPtr e);

    [DllImport("chess.dll", CharSet = CharSet.Ansi)]
    public static extern int  chess_apply_move(IntPtr e, string move);
}

C# calls native code through P/Invoke (DllImport). The strings get marshalled automatically. The IntPtr is C#'s name for an opaque pointer. Same shape as the Swift side, different syntax. The exact same chess_bridge.h, the exact same C++ engine, two host languages, two native apps.

What flows across the boundary

The principle: keep traffic over the bridge infrequent and data-only. A few rules of thumb:

Why this is the architecture

You write the engine once, in C++, and ship it to every platform. Apple? Compile for arm64+x86_64, link from a Swift app. Windows? Compile to a DLL, P/Invoke from C#. iOS? Same engine. Linux? Same engine. Android via NDK? Same engine. The logic is platform-independent; only the UI is platform-specific. That's why nearly every cross-platform app you've ever used has a C or C++ core: VS Code (Electron over a web stack, but its native pieces are C++), Photoshop, Spotify (libspotify is C), every video game (the engine is C++; the platform shell isn't). The bridge header is the seam.

Write the hard part in C++. Write the pretty part in whatever the OS prefers. Connect them with a flat C surface.

Try it yourself

What's next

The bridge is one half of the puzzle: how does C++ expose itself? Next week is the other half on Apple: how does Swift consume it? We'll write Objective-C++ wrappers (the dialect that's both C++ and Objective-C — yes, really), expose Swift-friendly types, and build a chess UI in SwiftUI that drives the C++ engine.

Week 45 is macOS Native.

Quick check

1. Why is the bridge interface plain C, not C++?
  1. C is faster
  2. C has a stable ABI that every other language has bindings for; C++ has name mangling, exceptions, and ABI churn
  3. Apple requires it
Reveal Answer

Answer: B. Swift, C#, Python, Rust — every language can call C. Almost none can call C++ directly.

2. What does an opaque pointer (typedef struct ChessEngine ChessEngine;) let the host language do?
  1. Hold a handle to the C++ object without knowing its internal layout
  2. Skip memory allocation
  3. Run faster
Reveal Answer

Answer: A. The pointer is just a bag of bits to the host. Only the C++ side knows what's behind it.

3. Why does the bridge implementation wrap its body in try/catch(...)?
  1. For logging
  2. C++ exception unwinding across language boundaries is undefined behaviour — convert to a status code instead
  3. The C standard requires it
Reveal Answer

Answer: B. Even if Swift seemed to catch it, the unwinding through Swift frames is UB. The bridge must absorb every exception.