Skip to content
C.W.K.
Stream
Lesson 05 of 05 · published

A SwiftPM C Target

~14 min · swiftpm, c, modules, build, swift

Level 0Blank Canvas
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Four Targets, One Direction

Everything in this track lives in one SwiftPM package, AriandelKit, and its arrows point one way: CJSONC is used by CMyPaint, which is used by CAriandelSurface, which is used by AriandelEngine, which is used by the ariandel-icon executable and the tests. (AriandelEngine also depends on CMyPaint directly, because it calls mypaint_brush_* itself, and on the family kit's CwkKitCore.) The package exports exactly one product, the AriandelEngine library. The app, generated by XcodeGen, takes only that product from this package (its other links are the family kit's CwkKitCore, CwkKitMedia and CwkKitWeb), and no Swift file in the app imports a C module. C is something the engine has, never something the app sees.

The platforms are iOS 18 and macOS 15, and the macOS floor is not decoration: swift test on a Mac is the fast engine suite, where brushes, tiles, undo, documents and the renderer run with no simulator. Swift 6 language mode for the Swift targets and C11 for the C targets are set once, at the bottom of the manifest, so no target can quietly drift to another mode.

include/ Is the Public Face

A SwiftPM C target is a directory of .c files with an include/ subdirectory. Whatever sits in include/ is the target's public interface. If there is no module.modulemap there (Ariandel has none), SwiftPM writes one for you: an umbrella over the whole directory, which is what the generated map in the second block shows. That is how a single import CAriandelSurface in Swift sees all four public headers, and how this crosses the boundary:

  • Functions arrive as Swift functions with C types: int becomes Int32, uint16_t * becomes UnsafeMutablePointer<UInt16>.
  • Nullability comes from the header. ariandel_surface.h wraps itself in #pragma clang assume_nonnull, so the buffer in ArdTileRelease arrives non-optional, while ArdTileAcquire's return, marked _Nullable, arrives optional. The Swift callbacks from lesson 2 match those signatures exactly.
  • Simple macros arrive as constants: Tile.size is Int(ARD_TILE_SIZE).
  • A struct declared but never defined arrives as OpaquePointer, which is BrushSurface.handle.
  • A function-pointer typedef arrives as a @convention(c) function type, which is why a capture-free func can be passed straight in.

Public headers may include their dependencies' public headers: ariandel_surface.h includes mypaint-surface.h, found because CMyPaint is a dependency. And one header is there purely for layout: ariandel_shader_types.h declares the Metal buffer structs in C so Swift gets C layout, the shader source repeats them field for field, and the package suite pins the C side of all three structs, their strides (40, 112 and 144 bytes) and key field offsets, with MemoryLayout, at the numbers the shader expects. That catches drift on the C side only. The test never reads the shader source, a Swift string compiled at run time, so an edit to the Metal copy alone still gets past it; keeping that copy in step is left to whoever edits it.

cSettings, and the Headers That Must Stay Private

Both vendored libraries include a config.h, unconditionally. libmypaint's is Ariandel's own three lines (no glib, the package names); json-c's is upstream's CMake output with one guard added. Each sits at its target's root, outside include/, reached by .headerSearchPath("."), which applies only while compiling that target's own files. The house gotcha says why the placement is a rule: the two are different files in different targets, and putting either in a public include/ would make it part of the module, so Swift would import the wrong macros. Everything in include/ is a promise to every importer; a build configuration is not.

CMyPaint also sets .define("HAVE_CONFIG_H"), the autotools convention. Read the vendored code before trusting it, though: at this upstream commit no file in either library tests that macro, and config.h is included regardless. The search path is what does the work; the define is a harmless habit. (The sketch below guards its own include with it, so you can see the convention working where a source does test it.)

The exclude lists keep non-sources out of the target: COPYING and PROVENANCE.md stay beside the code they describe, upstream's fastapprox/Makefile.am.local belongs to a build system Ariandel does not use, and glib/mypaint-brush.c is a fragment that mypaint-brush.c pulls in with #include, so it must never be compiled as a file of its own.

Generated, Never Hand-Edited

Some headers in the vendored targets are generated, not copied. libmypaint's brushsettings-gen.h (private) and include/mypaint-brush-settings-gen.h (public) come from upstream's generate.py run over brushsettings.json at the same commit; json-c's json.h and json_config.h come from upstream's CMake configured for iOS. Each target's PROVENANCE.md records the exact command. Treat them like the vendored code they describe: regenerate from the recorded commit, never patch by hand. Track 1 tells the vendoring story in full.

An Executable That Paints Its Own Icon

ariandel-icon is a small command-line target: swift run ariandel-icon <brushes-dir> <out.png>. It loads MyPaint brushes from the directory it is given (the CC0 set the app bundles) and paints what its header comment calls "A night of the Painted World, one copper-red stroke (Pippa's hair) and falling snow", through the same Brush, BrushSurface and CanvasRenderer.flatten the app uses. A fixed-seed generator places the snow, so the icon is identical on every run. It then redraws the picture without an alpha channel, because App Store icons may not carry one (ITMS-90717).

Why make the icon a target rather than an asset someone drew once? It runs the whole engine end to end on a Mac, so a broken brush path shows up as a broken icon. It makes the icon reproducible from source. And it is honest dogfood: each of its strokes starts with PencilMapping.strokeStartInterval as the first deltaTime, the same constant the app feeds libmypaint when a real pen lands (track 4). The manifest lists only the library as a product, and swift run still works, because SwiftPM can run any executable target in the root package.

A C target is a directory with a public face. Put the interface in include/ and keep everything else (configuration, private generated headers, fragments, licenses) out of it. Then let exactly one Swift target stand between the C and everyone else, so the rest of your code imports Swift and never learns there was C underneath.

Code

AriandelKit's Package.swift (verbatim)·swift
// swift-tools-version:6.0
//
// Ariandel's engine: libmypaint (vendored, unmodified) behind a Swift engine
// that owns tiles, layers, undo, documents and Metal rendering.
// `swift test` here is the fast engine suite and runs on macOS.
import PackageDescription

let package = Package(
    name: "AriandelKit",
    platforms: [
        .iOS(.v18),
        .macOS(.v15),
    ],
    products: [
        .library(name: "AriandelEngine", targets: ["AriandelEngine"]),
    ],
    dependencies: [
        // The family kit: the vendored-file drift check runs in this package's suite.
        .package(name: "CwkKit", path: "../../../cwkSiblingKit/CwkKit"),
    ],
    targets: [
        // json-c, vendored byte-for-byte (see Sources/CJSONC/PROVENANCE.md).
        .target(
            name: "CJSONC",
            path: "Sources/CJSONC",
            exclude: ["COPYING", "PROVENANCE.md"],
            cSettings: [.headerSearchPath(".")]
        ),
        // libmypaint, vendored byte-for-byte (see Sources/CMyPaint/PROVENANCE.md).
        .target(
            name: "CMyPaint",
            dependencies: ["CJSONC"],
            path: "Sources/CMyPaint",
            exclude: [
                "COPYING",
                "PROVENANCE.md",
                // Compiled only through mypaint-brush.c's #include.
                "glib/mypaint-brush.c",
                "fastapprox/Makefile.am.local",
            ],
            cSettings: [
                .define("HAVE_CONFIG_H"),
                .headerSearchPath("."),
            ]
        ),
        // Ariandel's own C: the tiled surface libmypaint paints into, pixel kernels.
        .target(
            name: "CAriandelSurface",
            dependencies: ["CMyPaint"],
            path: "Sources/CAriandelSurface"
        ),
        .target(
            name: "AriandelEngine",
            dependencies: ["CAriandelSurface", "CMyPaint", .product(name: "CwkKitCore", package: "CwkKit")],
            path: "Sources/AriandelEngine"
        ),
        // Paints the app icon with the engine itself (`swift run ariandel-icon`).
        .executableTarget(
            name: "ariandel-icon",
            dependencies: ["AriandelEngine"],
            path: "Sources/ariandel-icon"
        ),
        .testTarget(
            name: "AriandelEngineTests",
            dependencies: [
                "AriandelEngine",
                .product(name: "CwkKitTesting", package: "CwkKit"),
            ],
            path: "Tests/AriandelEngineTests",
            resources: [.copy("Fixtures")]
        ),
    ],
    swiftLanguageModes: [.v6],
    cLanguageStandard: .c11
)
The package's layout (trimmed, with comments) and the module map SwiftPM generated for the sketch's C target·text
AriandelKit's layout (trimmed; comments added)

Packages/AriandelKit/
  Package.swift
  Sources/
    CJSONC/                  json-c, vendored byte for byte
      config.h               private: CMake output, reached by .headerSearchPath(".")
      include/               public: json.h and json_config.h (generated by CMake), json_object.h, ...
      arraylist.c ...        12 sources
    CMyPaint/                libmypaint, vendored byte for byte
      config.h               private: three defines
      brushsettings-gen.h    private, generated by upstream generate.py
      include/               public: mypaint-surface.h, mypaint-tiled-surface.h,
                             mypaint-brush-settings-gen.h (generated), ...
      glib/mypaint-brush.c   excluded: compiled only through mypaint-brush.c's #include
      fastapprox/            headers (its Makefile.am.local is excluded)
      mypaint-brush.c ...    16 sources
    CAriandelSurface/        Ariandel's own C
      include/               public: ariandel_surface.h, ariandel_kernels.h,
                             ariandel_pixels.h, ariandel_shader_types.h
      ariandel_surface.c  ariandel_kernels.c  ariandel_pixels.c
    AriandelEngine/          Swift: brushes, tiles, layers, documents, undo, files, Metal
    ariandel-icon/main.swift paints the app icon with the engine itself
  Tests/AriandelEngineTests/

The module map SwiftPM generated for the sketch's C target (no module.modulemap
in the source tree, so it writes one: the whole include/ is the module):

module CEngine {
umbrella "<package>/Sources/CEngine/include"
export *
}
A minimal sketch you can reproduce: a C target with a public include/, a private config.h, a Swift wrapper and an executable (run in an empty directory; built and run with Swift 6.4)·bash
# A minimal sketch of AriandelKit's shape: a C target whose include/ is its
# public face, a private config.h, a Swift target over it, and an executable.
# Run in an empty directory.
mkdir -p Sources/CEngine/include Sources/Engine Sources/mini-icon
cat > Package.swift <<'EOF'
// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "MiniKit",
    platforms: [.macOS(.v15)],
    products: [.library(name: "Engine", targets: ["Engine"])],
    targets: [
        .target(
            name: "CEngine",
            path: "Sources/CEngine",
            exclude: ["NOTES.md"],
            cSettings: [
                .define("HAVE_CONFIG_H"),
                .headerSearchPath("."),      // reaches config.h; it is never public
            ]
        ),
        .target(name: "Engine", dependencies: ["CEngine"], path: "Sources/Engine"),
        .executableTarget(name: "mini-icon", dependencies: ["Engine"], path: "Sources/mini-icon"),
    ],
    swiftLanguageModes: [.v6],
    cLanguageStandard: .c11
)
EOF
echo "Not a source file; excluded." > Sources/CEngine/NOTES.md
echo '#define ENGINE_NAME "mini"' > Sources/CEngine/config.h
cat > Sources/CEngine/include/ce_tiles.h <<'EOF'
#ifndef CE_TILES_H
#define CE_TILES_H
#include <stdint.h>
#define CE_TILE_SIZE 64
int ce_fill(uint16_t *values, int count, uint16_t value);
const char *ce_name(void);
#endif
EOF
cat > Sources/CEngine/ce_tiles.c <<'EOF'
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "ce_tiles.h"
int ce_fill(uint16_t *values, int count, uint16_t value) {
    for (int i = 0; i < count; i++) values[i] = value;
    return count;
}
const char *ce_name(void) { return ENGINE_NAME; }
EOF
cat > Sources/Engine/Engine.swift <<'EOF'
import CEngine

/// The Swift face of the C target; callers import Engine, never CEngine.
public enum Engine {
    public static let tileSize = Int(CE_TILE_SIZE)          // a C macro, imported as a constant
    public static var name: String { String(cString: ce_name()) }

    public static func filledTile(_ value: UInt16) -> [UInt16] {
        var tile = [UInt16](repeating: 0, count: tileSize * tileSize)
        let written = tile.withUnsafeMutableBufferPointer { ce_fill($0.baseAddress!, Int32($0.count), value) }
        precondition(written == tile.count)
        return tile
    }
}
EOF
cat > Sources/mini-icon/main.swift <<'EOF'
import Engine
let tile = Engine.filledTile(1 << 15)
print("\(Engine.name): \(Engine.tileSize)x\(Engine.tileSize) tile, first value \(tile[0])")
EOF
swift run mini-icon

External links

Exercise

Take a small C library you use, or one C file from your own project, and give it AriandelKit's shape: a C target with its public header in include/, any configuration header at the target root behind .headerSearchPath("."), every non-source file excluded, and one Swift target that wraps it so nothing else imports the C module. Build it, then find the module map SwiftPM generated and list every header it made public.
Hint
If the list surprises you, something private is sitting in include/. Move it to the target root and add the search path. And before you copy a define like HAVE_CONFIG_H from a build system, grep the sources for it: if nothing tests it, it is doing nothing.

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 in — Please sign in to comment.

No comments yet — be the first.