C.W.K.
Stream
Lesson 02 of 05 · published

The Narrow C Boundary

~14 min · embed-engine, c-interop, boundary, swift

Level 0Reel Novice
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Sources/CLibMPV/ — the narrow C module boundary; no product decisions."

Where the Danger Lives

When you embed a C library in a Swift app, all the sharp edges live at the seam: raw pointers, manual memory, NULL-terminated arrays, integer error codes, undefined behavior if you hold a handle wrong. That danger is unavoidable — it's the price of reuse. What you can control is how much of your app has to touch it.

Ashen Reel's answer is a module called CLibMPV: a single, small, boring boundary whose only job is to let Swift call libmpv's C API safely. It maps the C header, wraps the handful of calls the app needs into safe Swift signatures, and stops there. Everything above it speaks Swift and never sees a raw pointer.

No Product Decisions at the Seam

The rule that keeps this boundary healthy: no product logic lives in the C module. Should a seek be exact or fast? Should resume override a deep link? What counts as a successful seek? None of that belongs at the C seam — it belongs in AshenReelCore, in pure, testable Swift. The C module only knows how to say one thing to libmpv and translate the answer back.

Keep the unsafe boundary thin, dumb, and single-purpose. The more logic you let leak into the interop layer, the more of your product's brain runs in the one place that's hardest to test and easiest to crash.

Here's the whole philosophy in one function — the only place raw mpv pointers get touched:

This is the same shape as Cinder's batchPlay quarantine. In Dad's Photoshop sidecar, Pippa never emits raw Photoshop descriptors — the raw, dangerous API is sealed behind a thin translation layer, and the intelligent layer above speaks only in safe intents. Same instinct here: the raw C engine is sealed behind CLibMPV, and the smart Swift above it never touches a pointer.

Why 'Boring' Is the Goal

A good interop boundary is boring on purpose. It has few functions, each does one call, each cleans up after itself, and none of them make a judgment. When something goes wrong deep in playback, you want to be debugging Swift logic with real types and tests — not bisecting a crash that could be anywhere because pointers and product decisions got tangled together. Boring at the seam buys you sanity everywhere else.

Code

The only place raw mpv pointers are touched — safe surface, no product logic·swift
import CLibMPV   // umbrella module map over <mpv/client.h>

struct MPVError: Error { let code: Int32; let message: String }

/// Send one command to libmpv. This is interop plumbing, not a decision:
/// it knows how to CALL, never whether the call was the right thing to do.
func mpvCommand(_ handle: OpaquePointer, _ args: [String]) throws {
    // libmpv wants a NULL-terminated array of C strings.
    var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
    cargs.append(nil)
    defer { cargs.forEach { free($0) } }   // clean up across the seam, always

    let rc = mpv_command(handle, &cargs)
    guard rc >= 0 else {
        throw MPVError(code: rc, message: String(cString: mpv_error_string(rc)))
    }
}

// Callers say: try mpvCommand(handle, ["seek", "41.0", "absolute+exact"])
// WHETHER to seek exactly, and what 'success' means, is decided in Swift
// above this line -- never in here.

External links

Exercise

Find a place in your own code where you call into something 'raw' — a C library, a shell command, a low-level socket, a database driver. Ask: how much decision-making happens right at that boundary versus in testable code above it? Sketch the thinnest possible wrapper that does the call and nothing else, and move every judgment one layer up. What got easier to test once the seam went dumb?
Hint
A healthy seam has functions you could describe as 'it does exactly one call and cleans up.' If a wrapper function also decides WHETHER to make the call, or what to do with a weird result, that decision wants to live in your tested logic — not tangled in the plumbing.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.