Week 44 · Phase 6 — The Bridge
A flat C interface — the universal translator between C++ and every other language on the system.
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.
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.
// 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.
// 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.
// 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.
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.
The principle: keep traffic over the bridge infrequent and data-only. A few rules of thumb:
std::string doesn't survive the trip; convert to const char*.?????.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.
chess_bridge.h with the six functions above.chess_bridge.cpp wrapping your engine. Compile to a static library on macOS (libchess.a) or a DLL on Windows (chess.dll).chess_create, then loop calling chess_best_move and chess_apply_move until checkmate. This proves the bridge works independent of any UI.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.
Answer: B. Swift, C#, Python, Rust — every language can call C. Almost none can call C++ directly.
Answer: A. The pointer is just a bag of bits to the host. Only the C++ side knows what's behind it.
Answer: B. Even if Swift seemed to catch it, the unwinding through Swift frames is UB. The bridge must absorb every exception.