Swift 6.4 migration and exhancements (#1)

This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it.

Reviewed-on: #1
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-09-18 13:15:08 +00:00
committed by javier
parent d0a561b91f
commit c5037dd716
105 changed files with 1390 additions and 1397 deletions
@@ -4,11 +4,9 @@ internal import CPlaydate
private var httpAPI: UnsafePointer<playdate_http> { Playdate.httpAPI.unsafelyUnwrapped }
extension Network {
/// An HTTP connection to a server. Wraps `HTTPConnection`.
///
/// The binding stores a back-reference to each wrapper in the
/// underlying object's userdata slot so callbacks can recover the
/// wrapper; the C userdata slot is therefore reserved by the binding.
/// An HTTP connection. Wraps `HTTPConnection`; methods throw `Network.NetError`.
/// Callbacks don't retain it: keep it referenced until they fire, as `deinit`
/// drops pending callbacks and releases the C connection.
public final class HTTPConnection {
let pointer: OpaquePointer
@@ -18,8 +16,8 @@ extension Network {
var requestCompleteCallback: ((HTTPConnection) -> Void)?
var connectionClosedCallback: ((HTTPConnection) -> Void)?
/// Requests permission to connect to `server`. If the reply is
/// `.ask`, the completion is called later with the user's answer.
/// Asks to connect to `server` and its subdomains; call before `init`.
/// `purpose` appears in the dialog; `completion` runs only if the reply is `.ask`.
@discardableResult
public static func requestAccess(server: String, port: Int = 443, useSSL: Bool = true,
purpose: String? = nil,
@@ -30,10 +28,9 @@ extension Network {
completion: completion)
}
/// Opens a connection to `server`. Fails if access has not been
/// granted.
/// Sends nothing until a request. `nil` if access is denied or not yet 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 }
@@ -54,48 +51,48 @@ extension Network {
// MARK: Configuration
/// The time to wait for the connection to open, in milliseconds.
/// Connect timeout, in ms.
public func setConnectTimeout(milliseconds: Int) {
httpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// Whether to keep the connection open after a request completes.
/// Whether requests send `Connection: keep-alive`.
public func setKeepAlive(_ keepAlive: Bool) {
httpAPI.pointee.setKeepAlive.unsafelyUnwrapped(pointer, keepAlive)
}
/// Adds a `Range: bytes=start-end` header to future requests.
/// Adds a `Range: bytes=start-end` header.
public func setByteRange(start: Int, end: Int) {
httpAPI.pointee.setByteRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
}
/// The time to wait for incoming data, in milliseconds.
/// How long `read` waits for data, in ms (default 1000).
public func setReadTimeout(milliseconds: Int) {
httpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// The size of the connection's read buffer, in bytes.
/// Read buffer size, in bytes (default 64 KB).
public func setReadBufferSize(bytes: Int) {
httpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
}
// MARK: Requests
/// Sends a GET request for `path`. `headers` are raw header lines
/// (e.g. "Accept: text/html\r\n").
/// GETs `path`, opening the connection if needed. `headers` are extra 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)
}
}
try Network.check(error)
}
/// Sends a POST request for `path` with the given body.
/// POSTs `body` to `path`; otherwise like `get`.
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,
@@ -107,12 +104,12 @@ extension Network {
try Network.check(error)
}
/// Sends a request with an arbitrary HTTP method.
/// Sends a `method` request; otherwise like `post`.
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,
@@ -127,61 +124,63 @@ extension Network {
// MARK: Response
/// The last error on the connection, if any.
/// The connection's last error, if any.
public var error: NetError? {
Network.optionalError(httpAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The number of bytes read of the current response, and the total
/// expected (0 if the response has no Content-Length).
/// Response bytes read so far, and the total expected if known.
public var progress: (read: Int, total: Int) {
var read: Int32 = 0, total: Int32 = 0
httpAPI.pointee.getProgress.unsafelyUnwrapped(pointer, &read, &total)
return (Int(read), Int(total))
}
/// The HTTP status code of the response.
/// HTTP status code, valid once headers are parsed.
public var responseStatus: Int {
Int(httpAPI.pointee.getResponseStatus.unsafelyUnwrapped(pointer))
}
/// The number of response bytes available to read.
/// Response bytes available to read.
public var bytesAvailable: Int {
Int(httpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
}
/// Reads up to `buffer.count` response bytes. Returns the number of
/// bytes read.
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
let result = httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress,
UInt32(buffer.count))
/// Reads up to `buffer.count` bytes (capped by the read buffer size), waiting
/// up to the read timeout. Returns the count read.
public func read(into buffer: inout MutableSpan<UInt8>) throws(NetError) -> Int {
let result = buffer.withUnsafeMutableBufferPointer { buffer in
httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
return Int(result)
}
/// Reads up to `length` available response bytes.
/// Like `read(into:)`, returning the bytes read.
public func read(length: Int) throws(NetError) -> [UInt8] {
var bytes = [UInt8](repeating: 0, count: length)
let result = bytes.withUnsafeMutableBytes { buffer in
httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
try [UInt8](capacity: length) { output throws(NetError) in
let result = output.withUnsafeMutableBufferPointer { buffer, initializedCount in
let result = httpAPI.pointee.read.unsafelyUnwrapped(
pointer, buffer.baseAddress, UInt32(buffer.count))
initializedCount = max(Int(result), 0)
return result
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
bytes.removeLast(length - Int(result))
return bytes
}
/// Closes the connection.
/// Closes the connection; it can be reused for another request.
public func close() {
httpAPI.pointee.close.unsafelyUnwrapped(pointer)
}
// MARK: Callbacks
/// Called for each header line as it arrives.
/// Called per response header line. `nil` removes it.
public func setHeaderReceivedCallback(_ callback: ((HTTPConnection, _ key: String, _ value: String) -> Void)?) {
headerReceivedCallback = callback
if callback != nil {
@@ -196,7 +195,8 @@ extension Network {
}
}
/// Called when all headers have been read.
/// Called once headers are parsed, making `responseStatus` and `progress` valid.
/// `nil` removes it.
public func setHeadersReadCallback(_ callback: ((HTTPConnection) -> Void)?) {
headersReadCallback = callback
if callback != nil {
@@ -209,7 +209,7 @@ extension Network {
}
}
/// Called when response data is available to read.
/// Called when response data is available to read. `nil` removes it.
public func setResponseCallback(_ callback: ((HTTPConnection) -> Void)?) {
responseCallback = callback
if callback != nil {
@@ -222,7 +222,7 @@ extension Network {
}
}
/// Called when the request finishes.
/// Called when all data arrives (size known) or the request times out. `nil` removes it.
public func setRequestCompleteCallback(_ callback: ((HTTPConnection) -> Void)?) {
requestCompleteCallback = callback
if callback != nil {
@@ -235,7 +235,7 @@ extension Network {
}
}
/// Called when the connection closes.
/// Called when the server closes the connection. `nil` removes it.
public func setConnectionClosedCallback(_ callback: ((HTTPConnection) -> Void)?) {
connectionClosedCallback = callback
if callback != nil {
@@ -4,19 +4,17 @@ internal import CPlaydate
private var tcpAPI: UnsafePointer<playdate_tcp> { Playdate.tcpAPI.unsafelyUnwrapped }
extension Network {
/// A TCP connection to a server. Wraps `TCPConnection`.
///
/// The binding stores a back-reference to each wrapper in the
/// underlying object's userdata slot so callbacks can recover the
/// wrapper; the C userdata slot is therefore reserved by the binding.
/// A TCP connection. Wraps `TCPConnection`; methods throw `Network.NetError`.
/// Callbacks don't retain it: keep it referenced until they fire, as `deinit`
/// drops pending callbacks and releases the C connection.
public final class TCPConnection {
let pointer: OpaquePointer
var openCompletion: ((TCPConnection, NetError?) -> Void)?
var connectionClosedCallback: ((TCPConnection, NetError?) -> Void)?
/// Requests permission to connect to `server`. If the reply is
/// `.ask`, the completion is called later with the user's answer.
/// Asks to connect to `server`; call before `init`. `purpose` appears in
/// the dialog; `completion` runs only if the reply is `.ask`.
@discardableResult
public static func requestAccess(server: String, port: Int, useSSL: Bool = true,
purpose: String? = nil,
@@ -27,10 +25,9 @@ extension Network {
completion: completion)
}
/// Creates a connection to `server`. Fails if access has not been
/// granted. Call `open(_:)` to connect.
/// Does nothing until `open(_:)`. `nil` if access is denied or not yet granted.
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 }
@@ -49,17 +46,17 @@ extension Network {
return Unmanaged<TCPConnection>.fromOpaque(userdata).takeUnretainedValue()
}
/// The last error on the connection, if any.
/// The connection's last error, if any.
public var error: NetError? {
Network.optionalError(tcpAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The time to wait for the connection to open, in milliseconds.
/// Connect timeout, in ms.
public func setConnectTimeout(milliseconds: Int) {
tcpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// Opens the connection. The completion receives `nil` on success.
/// Errors are thrown immediately or passed to `completion` (`nil` on success).
public func open(_ completion: @escaping (TCPConnection, NetError?) -> Void) throws(NetError) {
openCompletion = completion
let error = tcpAPI.pointee.open.unsafelyUnwrapped(pointer, { connection, error, _ in
@@ -71,13 +68,12 @@ extension Network {
try Network.check(error)
}
/// Closes the connection.
/// Closes the connection; it can be reused.
public func close() throws(NetError) {
try Network.check(tcpAPI.pointee.close.unsafelyUnwrapped(pointer))
}
/// Called when the connection closes, with the reason if it closed
/// due to an error.
/// Called on close with the error, if any; `nil` removes it.
public func setConnectionClosedCallback(_ callback: ((TCPConnection, NetError?) -> Void)?) {
connectionClosedCallback = callback
if callback != nil {
@@ -90,65 +86,55 @@ extension Network {
}
}
/// The time to wait for incoming data, in milliseconds.
/// How long `read` waits for data, in ms (default 1000).
public func setReadTimeout(milliseconds: Int) {
tcpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// The size of the connection's read buffer, in bytes.
/// Read buffer size, in bytes (default 64 KB).
public func setReadBufferSize(bytes: Int) {
tcpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
}
/// The number of bytes available to read.
/// Bytes available to read.
public var bytesAvailable: Int {
Int(tcpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
}
/// The number of written bytes not yet sent on the wire.
/// Written bytes not yet sent.
public var sentBytesPending: Int {
Int(tcpAPI.pointee.getSentBytesPending.unsafelyUnwrapped(pointer))
}
/// Reads up to `buffer.count` bytes, waiting up to the read timeout.
/// Returns the number of bytes read.
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
let result = tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
return Int(result)
}
/// Reads up to `length` bytes, waiting up to the read timeout.
public func read(length: Int) throws(NetError) -> [UInt8] {
var bytes = [UInt8](repeating: 0, count: length)
let result = bytes.withUnsafeMutableBytes { buffer in
/// Reads up to `buffer.count` bytes within the read timeout; returns the count.
public func read(into buffer: inout MutableSpan<UInt8>) throws(NetError) -> Int {
let result = buffer.withUnsafeMutableBufferPointer { buffer in
tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
bytes.removeLast(length - Int(result))
return bytes
}
/// Writes the buffer to the connection. Returns the number of bytes
/// accepted.
@discardableResult
public func write(_ buffer: UnsafeRawBufferPointer) throws(NetError) -> Int {
let result = tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
return Int(result)
}
/// Writes the bytes to the connection. Returns the number of bytes
/// accepted.
/// Like `read(into:)`, returning the bytes read.
public func read(length: Int) throws(NetError) -> [UInt8] {
try [UInt8](capacity: length) { output throws(NetError) in
let result = output.withUnsafeMutableBufferPointer { buffer, initializedCount in
let result = tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
initializedCount = max(Int(result), 0)
return result
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
}
}
/// Queues `bytes`; returns the count handed to the network stack.
@discardableResult
public func write(_ bytes: [UInt8]) throws(NetError) -> Int {
let result = bytes.withUnsafeBytes { buffer in
public func write(_ bytes: Span<UInt8>) throws(NetError) -> Int {
let result = bytes.withUnsafeBufferPointer { buffer in
tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
}
if result < 0 {
@@ -156,5 +142,13 @@ extension Network {
}
return Int(result)
}
/// Same as the `Span` overload.
@discardableResult
public func write(_ bytes: [UInt8]) throws(NetError) -> Int {
try bytes.withUnsafeBufferPointer { buffer throws(NetError) in
try write(buffer.span)
}
}
}
}