Swift bindings to the Playdate C API

Wraps all ten subsystems of the Playdate SDK 3.1.1 C API (system, display,
graphics, sprites, sound, file, JSON, Lua, scoreboards, network) in
idiomatic Swift: namespaced APIs, wrapper types with ownership semantics,
closures for callbacks, and typed throws. Written within the Embedded Swift
subset so the same code can target the device.

The SDK headers are resolved through a "playdate" pkg-config module
(Scripts/install-pkgconfig.sh), so the package works as a normal SwiftPM
dependency without unsafe build flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 10:33:45 +02:00
co-authored by Claude Fable 5
commit 09760f99b0
29 changed files with 6533 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
import Testing
@testable import PlayDate
// Most of the binding requires a running Playdate (the API struct is handed
// to the game at launch), so these tests cover the pure value types and the
// inline C helpers that work without it.
@Test func buttonsOptionSetMatchesCMasks() {
#expect(Playdate.System.Buttons.left.rawValue == 1 << 0)
#expect(Playdate.System.Buttons.right.rawValue == 1 << 1)
#expect(Playdate.System.Buttons.up.rawValue == 1 << 2)
#expect(Playdate.System.Buttons.down.rawValue == 1 << 3)
#expect(Playdate.System.Buttons.b.rawValue == 1 << 4)
#expect(Playdate.System.Buttons.a.rawValue == 1 << 5)
let combined: Playdate.System.Buttons = [.a, .up]
#expect(combined.contains(.a))
#expect(!combined.contains(.b))
}
@Test func graphicsRectConvertsBetweenOriginSizeAndEdges() {
let rect = Playdate.Graphics.Rect(x: 10, y: 20, width: 30, height: 40)
#expect(rect.left == 10)
#expect(rect.right == 40)
#expect(rect.top == 20)
#expect(rect.bottom == 60)
let translated = rect.translated(dx: 5, dy: -5)
#expect(translated.left == 15)
#expect(translated.top == 15)
let roundTripped = Playdate.Graphics.Rect(rect.cValue)
#expect(roundTripped.left == rect.left && roundTripped.bottom == rect.bottom)
}
@Test func spriteRectRoundTripsThroughC() {
let rect = Playdate.Rect(x: 1.5, y: 2.5, width: 3, height: 4)
let roundTripped = Playdate.Rect(rect.cValue)
#expect(roundTripped.x == 1.5)
#expect(roundTripped.y == 2.5)
#expect(roundTripped.width == 3)
#expect(roundTripped.height == 4)
}
@Test func soundFormatPropertiesMatchCMacros() {
#expect(!Playdate.Sound.Format.mono8bit.isStereo)
#expect(Playdate.Sound.Format.stereo16bit.isStereo)
#expect(Playdate.Sound.Format.mono16bit.is16bit)
#expect(!Playdate.Sound.Format.monoADPCM.is16bit)
#expect(Playdate.Sound.Format.mono8bit.bytesPerFrame == 1)
#expect(Playdate.Sound.Format.stereo8bit.bytesPerFrame == 2)
#expect(Playdate.Sound.Format.mono16bit.bytesPerFrame == 2)
#expect(Playdate.Sound.Format.stereo16bit.bytesPerFrame == 4)
}
@Test func midiNoteFrequencyConversionRoundTrips() {
// A4 (MIDI 69) is 440 Hz.
let a4 = Playdate.Sound.frequency(forNote: 69)
#expect(abs(a4 - 440) < 0.01)
let note = Playdate.Sound.note(forFrequency: 440)
#expect(abs(note - 69) < 0.001)
}
@Test func cStringHelpersRoundTripUTF8() {
let original = "héllo, wörld"
let copy = original.copiedPlaydateCString()
defer { copy.deallocate() }
#expect(String(playdateCString: copy) == original)
let viaClosure = original.withPlaydateCString { String(playdateCString: $0) }
#expect(viaClosure == original)
}
@Test func dateTimeMirrorsCStruct() {
let dateTime = Playdate.System.DateTime(year: 2026, month: 7, day: 24, weekday: 5,
hour: 12, minute: 34, second: 56)
let roundTripped = Playdate.System.DateTime(dateTime.cValue)
#expect(roundTripped.year == 2026)
#expect(roundTripped.month == 7)
#expect(roundTripped.day == 24)
#expect(roundTripped.weekday == 5)
#expect(roundTripped.hour == 12)
#expect(roundTripped.minute == 34)
#expect(roundTripped.second == 56)
}