From 89c8d7a04c28c904920def3a3e0fe6b0334fb2fb Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 18 Sep 2026 11:58:57 +0200 Subject: [PATCH] Adopted the standard `withCString` for C strings throughout the library. --- Sources/PlaydateKit/File/Classes/Handle.swift | 6 +- Sources/PlaydateKit/File/File.swift | 14 ++--- .../PlaydateKit/Graphics/Classes/Bitmap.swift | 4 +- .../Graphics/Classes/BitmapTable.swift | 4 +- .../PlaydateKit/Graphics/Classes/Font.swift | 10 ++-- .../Graphics/Classes/VideoPlayer.swift | 2 +- Sources/PlaydateKit/Graphics/Graphics.swift | 8 +-- .../PlaydateKit/JSON/Classes/Encoder.swift | 8 +-- Sources/PlaydateKit/JSON/JSON.swift | 2 +- Sources/PlaydateKit/Lua/Lua.swift | 12 ++-- .../Network/Classes/HTTPConnection.swift | 16 ++--- .../Network/Classes/TCPConnection.swift | 2 +- Sources/PlaydateKit/Network/Network.swift | 4 +- .../PlaydateKit/Scoreboards/Scoreboards.swift | 6 +- Sources/PlaydateKit/Sound/Sound.swift | 2 +- .../Sound/Source/Classes/AudioSample.swift | 4 +- .../Sound/Source/Classes/FilePlayer.swift | 2 +- .../Sound/Synth/Classes/Sequence.swift | 2 +- Sources/PlaydateKit/Support.swift | 59 +++---------------- .../PlaydateKit/System/Classes/MenuItem.swift | 2 +- Sources/PlaydateKit/System/System.swift | 14 ++--- Tests/PlaydateKit/PlaydateKitTests.swift | 3 +- 22 files changed, 71 insertions(+), 115 deletions(-) diff --git a/Sources/PlaydateKit/File/Classes/Handle.swift b/Sources/PlaydateKit/File/Classes/Handle.swift index 692dfb5..fc30ecf 100644 --- a/Sources/PlaydateKit/File/Classes/Handle.swift +++ b/Sources/PlaydateKit/File/Classes/Handle.swift @@ -9,7 +9,7 @@ extension File { /// Opens the file at `path`. public init(path: String, mode: Options) throws(PlaydateError) { - let pointer = path.withPlaydateCString { + let pointer = path.withCString { fileAPI.pointee.open.unsafelyUnwrapped($0, mode.cValue) } guard let pointer else { throw lastFileError() } @@ -71,8 +71,8 @@ extension File { /// Writes the string's UTF-8 to the file. Returns the bytes written. @discardableResult public func write(_ string: String) throws(PlaydateError) -> Int { - let result = string.withPlaydateUTF8 { bytes, count in - fileAPI.pointee.write.unsafelyUnwrapped(pointer, bytes, UInt32(count)) + let result = string.withCString { cString in + fileAPI.pointee.write.unsafelyUnwrapped(pointer, cString, UInt32(string.utf8.count)) } if result < 0 { throw lastFileError() } return Int(result) diff --git a/Sources/PlaydateKit/File/File.swift b/Sources/PlaydateKit/File/File.swift index b6a6380..23a6039 100644 --- a/Sources/PlaydateKit/File/File.swift +++ b/Sources/PlaydateKit/File/File.swift @@ -23,12 +23,12 @@ extension File { _ each: (String) -> Void) throws(PlaydateError) { let result = withoutActuallyEscaping(each) { each in var callback = each - return path.withPlaydateCString { cPath in + return path.withCString { cPath in withUnsafeMutablePointer(to: &callback) { callbackPointer in fileAPI.pointee.listfiles.unsafelyUnwrapped(cPath, { cName, userdata in guard let cName, let userdata else { return } let each = userdata.assumingMemoryBound(to: ((String) -> Void).self).pointee - each(String(playdateCString: cName)) + each(String(cString: cName)) }, callbackPointer, showHidden ? 1 : 0) } } @@ -39,7 +39,7 @@ extension File { /// Information about the file or directory at `path`. public static func stat(_ path: String) throws(PlaydateError) -> Stat { var stat = FileStat() - let result = path.withPlaydateCString { fileAPI.pointee.stat.unsafelyUnwrapped($0, &stat) } + let result = path.withCString { fileAPI.pointee.stat.unsafelyUnwrapped($0, &stat) } if result != 0 { throw lastFileError() } return Stat( isDirectory: stat.isdir != 0, @@ -51,14 +51,14 @@ extension File { /// Creates a directory (and intermediate directories) in the Data directory. public static func mkdir(_ path: String) throws(PlaydateError) { - let result = path.withPlaydateCString { fileAPI.pointee.mkdir.unsafelyUnwrapped($0) } + let result = path.withCString { fileAPI.pointee.mkdir.unsafelyUnwrapped($0) } if result != 0 { throw lastFileError() } } /// Deletes the file or directory at `path`. Directories require /// `recursive` to be deleted with their contents. public static func unlink(_ path: String, recursive: Bool = false) throws(PlaydateError) { - let result = path.withPlaydateCString { + let result = path.withCString { fileAPI.pointee.unlink.unsafelyUnwrapped($0, recursive ? 1 : 0) } if result != 0 { throw lastFileError() } @@ -67,8 +67,8 @@ extension File { /// Renames (moves) a file in the Data directory, overwriting any existing /// file at the destination. public static func rename(from: String, to: String) throws(PlaydateError) { - let result = from.withPlaydateCString { cFrom in - to.withPlaydateCString { cTo in + let result = from.withCString { cFrom in + to.withCString { cTo in fileAPI.pointee.rename.unsafelyUnwrapped(cFrom, cTo) } } diff --git a/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift b/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift index e40a8a8..9ef15d0 100644 --- a/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift +++ b/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift @@ -26,7 +26,7 @@ extension Graphics { /// Loads a bitmap from a file in the game's pdx or Data directory. public convenience init(path: String) throws(PlaydateError) { var error: UnsafePointer? - let pointer = path.withPlaydateCString { gfx.pointee.loadBitmap.unsafelyUnwrapped($0, &error) } + let pointer = path.withCString { gfx.pointee.loadBitmap.unsafelyUnwrapped($0, &error) } guard let pointer else { throw PlaydateError(cString: error) } self.init(pointer: pointer, isOwned: true) } @@ -77,7 +77,7 @@ extension Graphics { /// Replaces the bitmap's contents with the image at `path`. public func load(path: String) throws(PlaydateError) { var error: UnsafePointer? - path.withPlaydateCString { gfx.pointee.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) } + path.withCString { gfx.pointee.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) } cachedSize = nil if let error { throw PlaydateError(cString: error) } } diff --git a/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift b/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift index 9cbebe0..194bad1 100644 --- a/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift +++ b/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift @@ -18,7 +18,7 @@ extension Graphics { /// Loads an image table from a file. public convenience init(path: String) throws(PlaydateError) { var error: UnsafePointer? - let pointer = path.withPlaydateCString { gfx.pointee.loadBitmapTable.unsafelyUnwrapped($0, &error) } + let pointer = path.withCString { gfx.pointee.loadBitmapTable.unsafelyUnwrapped($0, &error) } guard let pointer else { throw PlaydateError(cString: error) } self.init(pointer: pointer) } @@ -30,7 +30,7 @@ extension Graphics { /// Replaces the table's contents with the image table at `path`. public func load(path: String) throws(PlaydateError) { var error: UnsafePointer? - path.withPlaydateCString { gfx.pointee.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) } + path.withCString { gfx.pointee.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) } if let error { throw PlaydateError(cString: error) } } diff --git a/Sources/PlaydateKit/Graphics/Classes/Font.swift b/Sources/PlaydateKit/Graphics/Classes/Font.swift index c156789..7ffa149 100644 --- a/Sources/PlaydateKit/Graphics/Classes/Font.swift +++ b/Sources/PlaydateKit/Graphics/Classes/Font.swift @@ -16,7 +16,7 @@ extension Graphics { /// Loads a font from a file. public convenience init(path: String) throws(PlaydateError) { var error: UnsafePointer? - let pointer = path.withPlaydateCString { gfx.pointee.loadFont.unsafelyUnwrapped($0, &error) } + let pointer = path.withCString { gfx.pointee.loadFont.unsafelyUnwrapped($0, &error) } guard let pointer else { throw PlaydateError(cString: error) } self.init(pointer: pointer) } @@ -48,8 +48,8 @@ extension Graphics { /// The width of `text` when drawn with this font. public func textWidth(_ text: String, tracking: Int = 0) -> Int { - text.withPlaydateUTF8 { bytes, count in - Int(gfx.pointee.getTextWidth.unsafelyUnwrapped(pointer, bytes, count, + text.withCString { cString in + Int(gfx.pointee.getTextWidth.unsafelyUnwrapped(pointer, cString, text.utf8.count, kUTF8Encoding, Int32(tracking))) } } @@ -57,9 +57,9 @@ extension Graphics { /// The height of `text` when wrapped to `maxWidth` with this font. public func textHeight(_ text: String, maxWidth: Int, wrap: TextWrappingMode = .word, tracking: Int = 0, extraLeading: Int = 0) -> Int { - text.withPlaydateUTF8 { bytes, count in + text.withCString { cString in Int(gfx.pointee.getTextHeightForMaxWidth.unsafelyUnwrapped( - pointer, bytes, count, Int32(maxWidth), kUTF8Encoding, + pointer, cString, text.utf8.count, Int32(maxWidth), kUTF8Encoding, wrap.cValue, Int32(tracking), Int32(extraLeading))) } } diff --git a/Sources/PlaydateKit/Graphics/Classes/VideoPlayer.swift b/Sources/PlaydateKit/Graphics/Classes/VideoPlayer.swift index 919c6ca..01f58a9 100644 --- a/Sources/PlaydateKit/Graphics/Classes/VideoPlayer.swift +++ b/Sources/PlaydateKit/Graphics/Classes/VideoPlayer.swift @@ -18,7 +18,7 @@ extension Graphics { /// Opens the .pdv file at `path`. public convenience init(path: String) throws(PlaydateError) { - let pointer = path.withPlaydateCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) } + let pointer = path.withCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) } guard let pointer else { throw PlaydateError(message: "unable to load video: \(path)") } diff --git a/Sources/PlaydateKit/Graphics/Graphics.swift b/Sources/PlaydateKit/Graphics/Graphics.swift index 2ccd4ad..1d7afdc 100644 --- a/Sources/PlaydateKit/Graphics/Graphics.swift +++ b/Sources/PlaydateKit/Graphics/Graphics.swift @@ -238,8 +238,8 @@ extension Graphics { /// Draws `text` at (x, y) using the current font. Returns the drawn width. @discardableResult public static func drawText(_ text: String, x: Int, y: Int) -> Int { - text.withPlaydateUTF8 { bytes, count in - Int(gfx.pointee.drawText.unsafelyUnwrapped(bytes, count, + text.withCString { cString in + Int(gfx.pointee.drawText.unsafelyUnwrapped(cString, text.utf8.count, kUTF8Encoding, Int32(x), Int32(y))) } } @@ -247,8 +247,8 @@ extension Graphics { /// Draws `text` wrapped and aligned inside the given rectangle. public static func drawText(_ text: String, x: Int, y: Int, width: Int, height: Int, wrap: TextWrappingMode = .word, align: TextAlignment = .left) { - text.withPlaydateUTF8 { bytes, count in - gfx.pointee.drawTextInRect.unsafelyUnwrapped(bytes, count, kUTF8Encoding, + text.withCString { cString in + gfx.pointee.drawTextInRect.unsafelyUnwrapped(cString, text.utf8.count, kUTF8Encoding, Int32(x), Int32(y), Int32(width), Int32(height), wrap.cValue, align.cValue) } diff --git a/Sources/PlaydateKit/JSON/Classes/Encoder.swift b/Sources/PlaydateKit/JSON/Classes/Encoder.swift index e0e32b2..dd78978 100644 --- a/Sources/PlaydateKit/JSON/Classes/Encoder.swift +++ b/Sources/PlaydateKit/JSON/Classes/Encoder.swift @@ -43,10 +43,10 @@ extension JSON { /// Call before writing each table value. public func addTableMember(name: String) { - name.withPlaydateUTF8 { bytes, count in + name.withCString { cString in withUnsafeMutablePointer(to: &encoder) { $0.pointee.addTableMember.unsafelyUnwrapped( - $0, bytes.assumingMemoryBound(to: CChar.self), Int32(count)) + $0, cString, Int32(name.utf8.count)) } } } @@ -80,10 +80,10 @@ extension JSON { /// Writes a string value. public func writeString(_ value: String) { - value.withPlaydateUTF8 { bytes, count in + value.withCString { cString in withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeString.unsafelyUnwrapped( - $0, bytes.assumingMemoryBound(to: CChar.self), Int32(count)) + $0, cString, Int32(value.utf8.count)) } } } diff --git a/Sources/PlaydateKit/JSON/JSON.swift b/Sources/PlaydateKit/JSON/JSON.swift index 88ccea0..71f8751 100644 --- a/Sources/PlaydateKit/JSON/JSON.swift +++ b/Sources/PlaydateKit/JSON/JSON.swift @@ -103,7 +103,7 @@ extension JSON { let unmanaged = Unmanaged.passUnretained(context) var decoder = makeDecoder(context: unmanaged) var outval = json_value() - let ok = jsonString.withPlaydateCString { cString in + let ok = jsonString.withCString { cString in withExtendedLifetime(context) { jsonAPI.pointee.decodeString.unsafelyUnwrapped(&decoder, cString, &outval) != 0 } diff --git a/Sources/PlaydateKit/Lua/Lua.swift b/Sources/PlaydateKit/Lua/Lua.swift index c38dd23..d5d7fea 100644 --- a/Sources/PlaydateKit/Lua/Lua.swift +++ b/Sources/PlaydateKit/Lua/Lua.swift @@ -30,7 +30,7 @@ extension Lua { /// for namespacing, e.g. "mylib.myfunc"). public static func addFunction(_ function: CFunction, name: String) throws(PlaydateError) { var error: UnsafePointer? - let ok = name.withPlaydateCString { + let ok = name.withCString { luaAPI.pointee.addFunction.unsafelyUnwrapped(function, $0, &error) != 0 } if !ok { throw PlaydateError(cString: error) } @@ -72,7 +72,7 @@ extension Lua { retainedBuffers.append(UnsafeMutableRawPointer(constantsBuffer)) var error: UnsafePointer? - let ok = name.withPlaydateCString { + let ok = name.withCString { luaAPI.pointee.registerClass.unsafelyUnwrapped($0, registrationsBuffer, values.isEmpty ? nil : constantsBuffer, isStatic ? 1 : 0, &error) != 0 @@ -153,7 +153,7 @@ extension Lua { var userdataObject: OpaquePointer? // The C API takes a non-const class name but only reads it, so the // stack copy can be passed with a mutating cast. - let object = type.withPlaydateCString { cType in + let object = type.withCString { cType in luaAPI.pointee.getArgObject.unsafelyUnwrapped( Int32(position), UnsafeMutablePointer(mutating: cType), &userdataObject) } @@ -197,7 +197,7 @@ extension Lua { /// Pushes a string onto the stack. public static func push(_ value: String) { - value.withPlaydateCString { luaAPI.pointee.pushString.unsafelyUnwrapped($0) } + value.withCString { luaAPI.pointee.pushString.unsafelyUnwrapped($0) } } /// Pushes raw bytes (which may contain embedded zeros) onto the stack @@ -226,7 +226,7 @@ extension Lua { valueCount: Int = 0) -> UDObject? { // The C API takes a non-const class name but only reads it, so the // stack copy can be passed with a mutating cast. - let pointer = type.withPlaydateCString { cType in + let pointer = type.withCString { cType in luaAPI.pointee.pushObject.unsafelyUnwrapped( object, UnsafeMutablePointer(mutating: cType), Int32(valueCount)) } @@ -240,7 +240,7 @@ extension Lua { /// first. Calling Lua from Swift has overhead; use sparingly. public static func callFunction(_ name: String, argumentCount: Int = 0) throws(PlaydateError) { var error: UnsafePointer? - let ok = name.withPlaydateCString { + let ok = name.withCString { luaAPI.pointee.callFunction.unsafelyUnwrapped($0, Int32(argumentCount), &error) != 0 } if !ok { throw PlaydateError(cString: error) } diff --git a/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift b/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift index 1281d7c..15f81be 100644 --- a/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift +++ b/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift @@ -33,7 +33,7 @@ extension Network { /// Opens a connection to `server`. Fails if access has not been /// granted. public init?(server: String, port: Int = 443, useSSL: Bool = true) { - let pointer = server.withPlaydateCString { + let pointer = server.withCString { httpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL) } guard let pointer else { return nil } @@ -84,8 +84,8 @@ extension Network { /// Sends a GET request for `path`. `headers` are raw header lines /// (e.g. "Accept: text/html\r\n"). public func get(path: String, headers: String = "") throws(NetError) { - let error = path.withPlaydateCString { cPath in - headers.withPlaydateCString { cHeaders in + let error = path.withCString { cPath in + headers.withCString { cHeaders in httpAPI.pointee.get.unsafelyUnwrapped(pointer, cPath, cHeaders, headers.utf8.count) } } @@ -94,8 +94,8 @@ extension Network { /// Sends a POST request for `path` with the given body. public func post(path: String, headers: String = "", body: [UInt8]) throws(NetError) { - let error = path.withPlaydateCString { cPath in - headers.withPlaydateCString { cHeaders in + let error = path.withCString { cPath in + headers.withCString { cHeaders in body.withUnsafeBytes { bodyBuffer in httpAPI.pointee.post.unsafelyUnwrapped( pointer, cPath, cHeaders, headers.utf8.count, @@ -110,9 +110,9 @@ extension Network { /// Sends a request with an arbitrary HTTP method. public func query(method: String, path: String, headers: String = "", body: [UInt8] = []) throws(NetError) { - let error = method.withPlaydateCString { cMethod in - path.withPlaydateCString { cPath in - headers.withPlaydateCString { cHeaders in + let error = method.withCString { cMethod in + path.withCString { cPath in + headers.withCString { cHeaders in body.withUnsafeBytes { bodyBuffer in httpAPI.pointee.query.unsafelyUnwrapped( pointer, cMethod, cPath, cHeaders, headers.utf8.count, diff --git a/Sources/PlaydateKit/Network/Classes/TCPConnection.swift b/Sources/PlaydateKit/Network/Classes/TCPConnection.swift index 21e122b..ab59116 100644 --- a/Sources/PlaydateKit/Network/Classes/TCPConnection.swift +++ b/Sources/PlaydateKit/Network/Classes/TCPConnection.swift @@ -30,7 +30,7 @@ extension Network { /// Creates a connection to `server`. Fails if access has not been /// granted. Call `open(_:)` to connect. public init?(server: String, port: Int, useSSL: Bool = true) { - let pointer = server.withPlaydateCString { + let pointer = server.withCString { tcpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL) } guard let pointer else { return nil } diff --git a/Sources/PlaydateKit/Network/Network.swift b/Sources/PlaydateKit/Network/Network.swift index 7f67483..d4de017 100644 --- a/Sources/PlaydateKit/Network/Network.swift +++ b/Sources/PlaydateKit/Network/Network.swift @@ -57,9 +57,9 @@ extension Network { guard let userdata else { return } Unmanaged.fromOpaque(userdata).takeRetainedValue().body(allowed) } - let reply = server.withPlaydateCString { cServer in + let reply = server.withCString { cServer in if let purpose { - return purpose.withPlaydateCString { cPurpose in + return purpose.withCString { cPurpose in rawRequest(cServer, Int32(port), useSSL, cPurpose, trampoline, box.toOpaque()) } } else { diff --git a/Sources/PlaydateKit/Scoreboards/Scoreboards.swift b/Sources/PlaydateKit/Scoreboards/Scoreboards.swift index 8026588..2946740 100644 --- a/Sources/PlaydateKit/Scoreboards/Scoreboards.swift +++ b/Sources/PlaydateKit/Scoreboards/Scoreboards.swift @@ -22,7 +22,7 @@ extension Scoreboards { public static func addScore(boardID: String, value: UInt32, completion: @escaping (Result) -> Void) -> Bool { addScoreCompletion = completion - return boardID.withPlaydateCString { cBoardID in + return boardID.withCString { cBoardID in scoreboardsAPI.pointee.addScore.unsafelyUnwrapped(cBoardID, value, { score, errorMessage in let completion = Scoreboards.addScoreCompletion Scoreboards.addScoreCompletion = nil @@ -36,7 +36,7 @@ extension Scoreboards { public static func getPersonalBest(boardID: String, completion: @escaping (Result) -> Void) -> Bool { personalBestCompletion = completion - return boardID.withPlaydateCString { cBoardID in + return boardID.withCString { cBoardID in scoreboardsAPI.pointee.getPersonalBest.unsafelyUnwrapped(cBoardID, { score, errorMessage in let completion = Scoreboards.personalBestCompletion Scoreboards.personalBestCompletion = nil @@ -67,7 +67,7 @@ extension Scoreboards { public static func getScores(boardID: String, completion: @escaping (Result) -> Void) -> Bool { scoresCompletion = completion - return boardID.withPlaydateCString { cBoardID in + return boardID.withCString { cBoardID in scoreboardsAPI.pointee.getScores.unsafelyUnwrapped(cBoardID, { scores, errorMessage in let completion = Scoreboards.scoresCompletion Scoreboards.scoresCompletion = nil diff --git a/Sources/PlaydateKit/Sound/Sound.swift b/Sources/PlaydateKit/Sound/Sound.swift index 658af3d..e69ff0a 100644 --- a/Sources/PlaydateKit/Sound/Sound.swift +++ b/Sources/PlaydateKit/Sound/Sound.swift @@ -84,7 +84,7 @@ extension Sound { } let reply: accessReply if let purpose { - reply = purpose.withPlaydateCString { + reply = purpose.withCString { snd.pointee.requestMicAccess.unsafelyUnwrapped($0, trampoline, box.toOpaque()) } } else { diff --git a/Sources/PlaydateKit/Sound/Source/Classes/AudioSample.swift b/Sources/PlaydateKit/Sound/Source/Classes/AudioSample.swift index e39d583..f56a33f 100644 --- a/Sources/PlaydateKit/Sound/Source/Classes/AudioSample.swift +++ b/Sources/PlaydateKit/Sound/Source/Classes/AudioSample.swift @@ -21,7 +21,7 @@ extension Sound { /// Loads the wav or aiff file at `path`. public convenience init(path: String) throws(PlaydateError) { - let pointer = path.withPlaydateCString { AudioSample.api.pointee.load.unsafelyUnwrapped($0) } + let pointer = path.withCString { AudioSample.api.pointee.load.unsafelyUnwrapped($0) } guard let pointer else { throw PlaydateError(message: "unable to load sample: \(path)") } @@ -49,7 +49,7 @@ extension Sound { /// Loads the file at `path` into this sample's buffer. public func load(path: String) throws(PlaydateError) { - let loaded = path.withPlaydateCString { + let loaded = path.withCString { AudioSample.api.pointee.loadIntoSample.unsafelyUnwrapped(pointer, $0) != 0 } if !loaded { diff --git a/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift b/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift index b41dd42..42ce46f 100644 --- a/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift +++ b/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift @@ -33,7 +33,7 @@ extension Sound { /// Prepares the player to stream the file at `path`. public func load(path: String) throws(PlaydateError) { - let loaded = path.withPlaydateCString { + let loaded = path.withCString { FilePlayer.api.pointee.loadIntoPlayer.unsafelyUnwrapped(pointer, $0) != 0 } if !loaded { diff --git a/Sources/PlaydateKit/Sound/Synth/Classes/Sequence.swift b/Sources/PlaydateKit/Sound/Synth/Classes/Sequence.swift index cc72c78..1059321 100644 --- a/Sources/PlaydateKit/Sound/Synth/Classes/Sequence.swift +++ b/Sources/PlaydateKit/Sound/Synth/Classes/Sequence.swift @@ -25,7 +25,7 @@ extension Sound { } public func loadMIDIFile(path: String) throws(PlaydateError) { - let loaded = path.withPlaydateCString { + let loaded = path.withCString { Sequence.api.pointee.loadMIDIFile.unsafelyUnwrapped(pointer, $0) != 0 } if !loaded { diff --git a/Sources/PlaydateKit/Support.swift b/Sources/PlaydateKit/Support.swift index 2b2be21..6932038 100644 --- a/Sources/PlaydateKit/Support.swift +++ b/Sources/PlaydateKit/Support.swift @@ -2,67 +2,24 @@ internal import CPlaydate /// Internal C-string helpers shared by the wrappers. /// -/// The conversions are implemented manually (rather than with -/// `String(cString:)` / `withCString`) so the module stays within the -/// Embedded Swift subset used for device builds. +/// Passing strings to C goes through the standard `withCString`, which hands +/// out a pointer to the string's own null-terminated storage without copying. extension String { - /// Creates a string by copying a null-terminated UTF-8 C string. - init(playdateCString pointer: UnsafePointer) { - var count = 0 - while pointer[count] != 0 { count += 1 } - let bytes = UnsafeRawBufferPointer(start: pointer, count: count) - self = String(decoding: bytes, as: UTF8.self) - } - /// Creates a string from a nullable C string, or `nil` if the pointer is null. init?(playdateCString pointer: UnsafePointer?) { guard let pointer else { return nil } - self.init(playdateCString: pointer) - } - - /// Calls `body` with a temporary null-terminated UTF-8 copy of the - /// string. The copy lives on the stack for short strings, so calling - /// this in the update loop does not churn the heap. - func withPlaydateCString(_ body: (UnsafePointer) -> Result) -> Result { - let count = utf8.count - return withUnsafeTemporaryAllocation(of: CChar.self, capacity: count + 1) { buffer in - var index = 0 - for byte in utf8 { - buffer[index] = CChar(bitPattern: byte) - index += 1 - } - buffer[count] = 0 - return body(buffer.baseAddress.unsafelyUnwrapped) - } - } - - /// Calls `body` with a temporary buffer of the string's UTF-8 bytes (not - /// null-terminated) and its length, for the `(const void*, size_t)` text - /// APIs. Stack-allocated for short strings. - func withPlaydateUTF8(_ body: (UnsafeRawPointer, Int) -> Result) -> Result { - let count = utf8.count - return withUnsafeTemporaryAllocation(of: UInt8.self, capacity: count + 1) { buffer in - var index = 0 - for byte in utf8 { - buffer[index] = byte - index += 1 - } - return body(UnsafeRawPointer(buffer.baseAddress.unsafelyUnwrapped), count) - } + self.init(cString: pointer) } /// Copies the string into a newly allocated null-terminated C string. /// The caller owns the memory and must free it with `deallocate()`. func copiedPlaydateCString() -> UnsafeMutablePointer { - let count = utf8.count - let buffer = UnsafeMutablePointer.allocate(capacity: count + 1) - var index = 0 - for byte in utf8 { - buffer[index] = CChar(bitPattern: byte) - index += 1 + withCString { cString in + let count = utf8.count + 1 + let buffer = UnsafeMutablePointer.allocate(capacity: count) + buffer.initialize(from: cString, count: count) + return buffer } - buffer[count] = 0 - return buffer } } diff --git a/Sources/PlaydateKit/System/Classes/MenuItem.swift b/Sources/PlaydateKit/System/Classes/MenuItem.swift index 48dcbe0..67bc972 100644 --- a/Sources/PlaydateKit/System/Classes/MenuItem.swift +++ b/Sources/PlaydateKit/System/Classes/MenuItem.swift @@ -28,7 +28,7 @@ extension System { String(playdateCString: Playdate.systemAPI.pointee.getMenuItemTitle.unsafelyUnwrapped(pointer)) ?? "" } set { - newValue.withPlaydateCString { + newValue.withCString { Playdate.systemAPI.pointee.setMenuItemTitle.unsafelyUnwrapped(pointer, $0) } } diff --git a/Sources/PlaydateKit/System/System.swift b/Sources/PlaydateKit/System/System.swift index 3b687ce..c343841 100644 --- a/Sources/PlaydateKit/System/System.swift +++ b/Sources/PlaydateKit/System/System.swift @@ -25,12 +25,12 @@ extension System { /// Logs a message to the console (device serial or simulator console). public static func log(_ message: String) { - message.withPlaydateCString { cplaydate_log(Playdate.apiPointer, $0) } + message.withCString { cplaydate_log(Playdate.apiPointer, $0) } } /// Stops execution and displays the message as a fatal error. public static func error(_ message: String) { - message.withPlaydateCString { cplaydate_error(Playdate.apiPointer, $0) } + message.withCString { cplaydate_error(Playdate.apiPointer, $0) } } // MARK: - Time @@ -208,7 +208,7 @@ extension System { @discardableResult public static func addMenuItem(title: String, onSelect: @escaping (MenuItem) -> Void) -> MenuItem? { var item: MenuItem? - title.withPlaydateCString { cTitle in + title.withCString { cTitle in let pointer = api.pointee.addMenuItem.unsafelyUnwrapped(cTitle, menuItemTrampoline, nil) item = MenuItem(pointer: pointer, onSelect: onSelect) } @@ -220,7 +220,7 @@ extension System { public static func addCheckmarkMenuItem(title: String, isChecked: Bool = false, onSelect: @escaping (MenuItem) -> Void) -> MenuItem? { var item: MenuItem? - title.withPlaydateCString { cTitle in + title.withCString { cTitle in let pointer = api.pointee.addCheckmarkMenuItem.unsafelyUnwrapped( cTitle, isChecked ? 1 : 0, menuItemTrampoline, nil) item = MenuItem(pointer: pointer, onSelect: onSelect) @@ -237,7 +237,7 @@ extension System { let copies = options.map { $0.copiedPlaydateCString() } var cOptions: [UnsafePointer?] = copies.map { UnsafePointer($0) } var item: MenuItem? - title.withPlaydateCString { cTitle in + title.withCString { cTitle in cOptions.withUnsafeMutableBufferPointer { buffer in let pointer = api.pointee.addOptionsMenuItem.unsafelyUnwrapped( cTitle, buffer.baseAddress, Int32(options.count), menuItemTrampoline, nil) @@ -291,7 +291,7 @@ extension System { /// Quits the current game and restarts it with the given launch arguments. public static func restartGame(launchArguments: String? = nil) { if let launchArguments { - launchArguments.withPlaydateCString { api.pointee.restartGame.unsafelyUnwrapped($0) } + launchArguments.withCString { api.pointee.restartGame.unsafelyUnwrapped($0) } } else { api.pointee.restartGame.unsafelyUnwrapped(nil) } @@ -321,7 +321,7 @@ extension System { /// Looks up a localized string by key from the game's strings files. public static func localizedText(forKey key: String, language: Language = .system) -> String? { - key.withPlaydateCString { cKey in + key.withCString { cKey in guard let cString = api.pointee.getLocalizedText.unsafelyUnwrapped(cKey, language.cValue) else { return nil } diff --git a/Tests/PlaydateKit/PlaydateKitTests.swift b/Tests/PlaydateKit/PlaydateKitTests.swift index 8b06c17..07bd131 100644 --- a/Tests/PlaydateKit/PlaydateKitTests.swift +++ b/Tests/PlaydateKit/PlaydateKitTests.swift @@ -69,8 +69,7 @@ import Testing defer { copy.deallocate() } #expect(String(playdateCString: copy) == original) - let viaClosure = original.withPlaydateCString { String(playdateCString: $0) } - #expect(viaClosure == original) + #expect(String(playdateCString: nil) == nil) } @Test func dateTimeMirrorsCStruct() {