From c1dd0ba311841d0525bbbc8955e3db9fec8a3a4a Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 01:06:04 +0530 Subject: [PATCH 01/17] attempt to add swift spm --- .github/workflows/swift.yml | 52 ++ platforms/swift/.gitignore | 6 + platforms/swift/Package.swift | 52 ++ .../Sources/Colyseus/ClientContext.swift | 24 + .../Sources/Colyseus/ColyseusClient.swift | 168 +++++++ .../Sources/Colyseus/ColyseusMessage.swift | 172 +++++++ .../swift/Sources/Colyseus/ColyseusRoom.swift | 149 ++++++ .../Sources/Colyseus/ColyseusSchema.swift | 130 +++++ .../Sources/Colyseus/ColyseusSettings.swift | 83 ++++ .../swift/Sources/Colyseus/RoomContext.swift | 30 ++ platforms/swift/build.sh | 169 +++++++ platforms/swift/build.zig | 464 ++++++++++++++++++ platforms/swift/build.zig.zon | 12 + .../ColyseusExample/ColyseusExampleApp.swift | 11 + .../example/ColyseusExample/ContentView.swift | 97 ++++ platforms/swift/example/Package.swift | 24 + platforms/swift/include/colyseus_swift.h | 21 + 17 files changed, 1664 insertions(+) create mode 100644 .github/workflows/swift.yml create mode 100644 platforms/swift/.gitignore create mode 100644 platforms/swift/Package.swift create mode 100644 platforms/swift/Sources/Colyseus/ClientContext.swift create mode 100644 platforms/swift/Sources/Colyseus/ColyseusClient.swift create mode 100644 platforms/swift/Sources/Colyseus/ColyseusMessage.swift create mode 100644 platforms/swift/Sources/Colyseus/ColyseusRoom.swift create mode 100644 platforms/swift/Sources/Colyseus/ColyseusSchema.swift create mode 100644 platforms/swift/Sources/Colyseus/ColyseusSettings.swift create mode 100644 platforms/swift/Sources/Colyseus/RoomContext.swift create mode 100755 platforms/swift/build.sh create mode 100644 platforms/swift/build.zig create mode 100644 platforms/swift/build.zig.zon create mode 100644 platforms/swift/example/ColyseusExample/ColyseusExampleApp.swift create mode 100644 platforms/swift/example/ColyseusExample/ContentView.swift create mode 100644 platforms/swift/example/Package.swift create mode 100644 platforms/swift/include/colyseus_swift.h diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..444a5e1 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,52 @@ +name: Swift Platform + +on: + push: + pull_request: + workflow_call: + +jobs: + build: + name: Build xcframework + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Show tool versions + run: | + zig version + swift --version + xcodebuild -version + + - name: Build xcframework (all slices) + working-directory: platforms/swift + run: bash build.sh release + + - name: Verify xcframework structure + run: | + echo "=== xcframework contents ===" + find platforms/swift/build/Colyseus.xcframework -type f | sort + echo "" + echo "=== Checksum ===" + cat platforms/swift/build/Colyseus.xcframework.zip.sha256 + + - name: Build example (macOS) + working-directory: platforms/swift/example + run: | + swift build -c release 2>&1 | tail -5 + + - name: Upload xcframework artifact + uses: actions/upload-artifact@v4 + with: + name: Colyseus.xcframework + path: platforms/swift/build/Colyseus.xcframework.zip + retention-days: 30 diff --git a/platforms/swift/.gitignore b/platforms/swift/.gitignore new file mode 100644 index 0000000..868e0d4 --- /dev/null +++ b/platforms/swift/.gitignore @@ -0,0 +1,6 @@ +build/ +.build/ +zig-out/ +zig-cache/ +*.xcframework.zip +*.xcframework.zip.sha256 diff --git a/platforms/swift/Package.swift b/platforms/swift/Package.swift new file mode 100644 index 0000000..4c108fd --- /dev/null +++ b/platforms/swift/Package.swift @@ -0,0 +1,52 @@ +// swift-tools-version: 5.9 +import PackageDescription + +// MARK: - Package +// +// This package distributes the Colyseus Swift SDK in two modes: +// +// 1. SOURCE mode (default during development): +// The Swift wrapper sources are compiled directly. The pre-built +// xcframework is linked as a binary dependency. +// Use: .package(path: "../..") +// +// 2. RELEASE mode (tagged GitHub releases): +// Both the Swift sources and the libcolyseus static library are +// bundled in a single xcframework and distributed as a binary target. +// Consumers add: +// .package(url: "https://github.com/colyseus/colyseus-sdk", from: "0.17.0") +// +// For source-mode development the xcframework must be built first: +// cd platforms/swift && ./build.sh + +let package = Package( + name: "Colyseus", + platforms: [ + .macOS(.v12), + .iOS(.v15), + .tvOS(.v15), + ], + products: [ + .library( + name: "Colyseus", + targets: ["Colyseus"] + ), + ], + targets: [ + // Swift wrapper layer + .target( + name: "Colyseus", + dependencies: ["CColyseus"], + path: "Sources/Colyseus", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + + // C library xcframework (pre-built via build.sh) + .binaryTarget( + name: "CColyseus", + path: "build/Colyseus.xcframework" + ), + ] +) diff --git a/platforms/swift/Sources/Colyseus/ClientContext.swift b/platforms/swift/Sources/Colyseus/ClientContext.swift new file mode 100644 index 0000000..52af525 --- /dev/null +++ b/platforms/swift/Sources/Colyseus/ClientContext.swift @@ -0,0 +1,24 @@ + +import CColyseus +import Foundation + +/// Heap-allocated context for client matchmaking callbacks. +/// Holds the success/error closures and is released after the callback fires. +final class ClientContext { + let onSuccess: (ColyseusRoom) -> Void + let onError: (Int32, String) -> Void + + init(onSuccess: @escaping (ColyseusRoom) -> Void, + onError: @escaping (Int32, String) -> Void) { + self.onSuccess = onSuccess + self.onError = onError + } + + func retain() -> UnsafeMutableRawPointer { + Unmanaged.passRetained(self).toOpaque() + } + + static func consume(_ ptr: UnsafeMutableRawPointer) -> ClientContext { + Unmanaged.fromOpaque(ptr).takeRetainedValue() + } +} diff --git a/platforms/swift/Sources/Colyseus/ColyseusClient.swift b/platforms/swift/Sources/Colyseus/ColyseusClient.swift new file mode 100644 index 0000000..4388fd2 --- /dev/null +++ b/platforms/swift/Sources/Colyseus/ColyseusClient.swift @@ -0,0 +1,168 @@ + +import CColyseus +import Foundation + +/// Top-level Colyseus client. Manages matchmaking and room lifecycle. +public final class ColyseusClient { + + private let raw: UnsafeMutablePointer + private let settings: ColyseusSettings // retain settings + + // MARK: - Lifecycle + + public init(settings: ColyseusSettings) { + self.settings = settings + guard let ptr = colyseus_client_create(settings.raw) else { + fatalError("colyseus_client_create returned nil") + } + raw = ptr + } + + deinit { + colyseus_client_free(raw) + } + + // MARK: - Matchmaking + + public func joinOrCreate( + _ roomName: String, + options: String = "{}", + onSuccess: @escaping (ColyseusRoom) -> Void, + onError: @escaping (Int32, String) -> Void + ) { + let ctx = ClientContext(onSuccess: onSuccess, onError: onError) + colyseus_client_join_or_create( + raw, roomName, options, + Self.roomSuccessCallback, Self.roomErrorCallback, + ctx.retain() + ) + } + + public func create( + _ roomName: String, + options: String = "{}", + onSuccess: @escaping (ColyseusRoom) -> Void, + onError: @escaping (Int32, String) -> Void + ) { + let ctx = ClientContext(onSuccess: onSuccess, onError: onError) + colyseus_client_create_room( + raw, roomName, options, + Self.roomSuccessCallback, Self.roomErrorCallback, + ctx.retain() + ) + } + + public func join( + _ roomName: String, + options: String = "{}", + onSuccess: @escaping (ColyseusRoom) -> Void, + onError: @escaping (Int32, String) -> Void + ) { + let ctx = ClientContext(onSuccess: onSuccess, onError: onError) + colyseus_client_join( + raw, roomName, options, + Self.roomSuccessCallback, Self.roomErrorCallback, + ctx.retain() + ) + } + + public func joinById( + _ roomId: String, + options: String = "{}", + onSuccess: @escaping (ColyseusRoom) -> Void, + onError: @escaping (Int32, String) -> Void + ) { + let ctx = ClientContext(onSuccess: onSuccess, onError: onError) + colyseus_client_join_by_id( + raw, roomId, options, + Self.roomSuccessCallback, Self.roomErrorCallback, + ctx.retain() + ) + } + + public func reconnect( + token: String, + onSuccess: @escaping (ColyseusRoom) -> Void, + onError: @escaping (Int32, String) -> Void + ) { + let ctx = ClientContext(onSuccess: onSuccess, onError: onError) + colyseus_client_reconnect( + raw, token, + Self.roomSuccessCallback, Self.roomErrorCallback, + ctx.retain() + ) + } + + // MARK: - Async/await wrappers + + public func joinOrCreate(_ roomName: String, options: String = "{}") async throws -> ColyseusRoom { + try await withCheckedThrowingContinuation { cont in + joinOrCreate(roomName, options: options, + onSuccess: { cont.resume(returning: $0) }, + onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) } + ) + } + } + + public func create(_ roomName: String, options: String = "{}") async throws -> ColyseusRoom { + try await withCheckedThrowingContinuation { cont in + create(roomName, options: options, + onSuccess: { cont.resume(returning: $0) }, + onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) } + ) + } + } + + public func join(_ roomName: String, options: String = "{}") async throws -> ColyseusRoom { + try await withCheckedThrowingContinuation { cont in + join(roomName, options: options, + onSuccess: { cont.resume(returning: $0) }, + onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) } + ) + } + } + + public func joinById(_ roomId: String, options: String = "{}") async throws -> ColyseusRoom { + try await withCheckedThrowingContinuation { cont in + joinById(roomId, options: options, + onSuccess: { cont.resume(returning: $0) }, + onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) } + ) + } + } + + public func reconnect(token: String) async throws -> ColyseusRoom { + try await withCheckedThrowingContinuation { cont in + reconnect(token: token, + onSuccess: { cont.resume(returning: $0) }, + onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) } + ) + } + } + + // MARK: - C trampolines (must be @convention(c)) + + private static let roomSuccessCallback: colyseus_client_room_callback_t = { rawRoom, userdata in + guard let rawRoom = rawRoom, let userdata = userdata else { return } + let ctx = ClientContext.consume(userdata) + let room = ColyseusRoom(raw: rawRoom, owns: true) + room.installCallbacks() + DispatchQueue.main.async { ctx.onSuccess(room) } + } + + private static let roomErrorCallback: colyseus_client_error_callback_t = { code, message, userdata in + guard let userdata = userdata else { return } + let ctx = ClientContext.consume(userdata) + let msg = message.map { String(cString: $0) } ?? "" + DispatchQueue.main.async { ctx.onError(code, msg) } + } +} + +// MARK: - ColyseusError + +public struct ColyseusError: Error, LocalizedError { + public let code: Int32 + public let message: String + + public var errorDescription: String? { "Colyseus error \(code): \(message)" } +} diff --git a/platforms/swift/Sources/Colyseus/ColyseusMessage.swift b/platforms/swift/Sources/Colyseus/ColyseusMessage.swift new file mode 100644 index 0000000..e3bcb82 --- /dev/null +++ b/platforms/swift/Sources/Colyseus/ColyseusMessage.swift @@ -0,0 +1,172 @@ +import CColyseus +import Foundation + +public indirect enum MessageValue { + case `nil` + case bool(Bool) + case int(Int64) + case uint(UInt64) + case float(Double) + case string(String) + case binary(Data) + case array([MessageValue]) + case map([String: MessageValue]) +} + +extension MessageValue: CustomStringConvertible { + public var description: String { + switch self { + case .nil: return "nil" + case .bool(let v): return String(v) + case .int(let v): return String(v) + case .uint(let v): return String(v) + case .float(let v): return String(v) + case .string(let v): return v + case .binary(let v): return "<\(v.count) bytes>" + case .array(let v): return String(describing: v) + case .map(let v): return String(describing: v) + } + } +} + +public enum MessageType { + case `nil`, bool, int, uint, float, string, binary, array, map + + init(_ raw: colyseus_message_type_t) { + switch raw { + case COLYSEUS_MESSAGE_TYPE_NIL: self = .nil + case COLYSEUS_MESSAGE_TYPE_BOOL: self = .bool + case COLYSEUS_MESSAGE_TYPE_INT: self = .int + case COLYSEUS_MESSAGE_TYPE_UINT: self = .uint + case COLYSEUS_MESSAGE_TYPE_FLOAT: self = .float + case COLYSEUS_MESSAGE_TYPE_STR: self = .string + case COLYSEUS_MESSAGE_TYPE_BIN: self = .binary + case COLYSEUS_MESSAGE_TYPE_ARRAY: self = .array + case COLYSEUS_MESSAGE_TYPE_MAP: self = .map + default: self = .nil + } + } +} + +public final class MessageReader { + let raw: OpaquePointer + init(_ raw: OpaquePointer) { self.raw = raw } + deinit { colyseus_message_reader_free(raw) } + + public var type: MessageType { MessageType(colyseus_message_reader_get_type(raw)) } + public var isNil: Bool { colyseus_message_reader_is_nil(raw) } + public var isBool: Bool { colyseus_message_reader_is_bool(raw) } + public var isInt: Bool { colyseus_message_reader_is_int(raw) } + public var isFloat: Bool { colyseus_message_reader_is_float(raw) } + public var isString: Bool { colyseus_message_reader_is_str(raw) } + public var isBinary: Bool { colyseus_message_reader_is_bin(raw) } + public var isArray: Bool { colyseus_message_reader_is_array(raw) } + public var isMap: Bool { colyseus_message_reader_is_map(raw) } + + public var boolValue: Bool { colyseus_message_reader_get_bool(raw) } + public var intValue: Int64 { colyseus_message_reader_get_int(raw) } + public var uintValue: UInt64 { colyseus_message_reader_get_uint(raw) } + public var floatValue: Double { colyseus_message_reader_get_float(raw) } + + public var stringValue: String? { + var len = 0 + guard let ptr = colyseus_message_reader_get_str(raw, &len) else { return nil } + return String(bytes: UnsafeBufferPointer(start: ptr, count: len), encoding: .utf8) + } + + public var binaryValue: Data? { + var len = 0 + guard let ptr = colyseus_message_reader_get_bin(raw, &len) else { return nil } + return Data(bytes: ptr, count: len) + } + + public var arrayCount: Int { colyseus_message_reader_get_array_size(raw) } + + public func arrayElement(at index: Int) -> MessageReader? { + colyseus_message_reader_get_array_element(raw, index).map { MessageReader($0) } + } + + public var mapCount: Int { colyseus_message_reader_get_map_size(raw) } + + public func mapValue(forKey key: String) -> MessageReader? { + colyseus_message_reader_map_get(raw, key).map { MessageReader($0) } + } + + public func mapString(forKey key: String) -> String? { + var ptr: UnsafePointer? = nil + var len = 0 + guard colyseus_message_reader_map_get_str(raw, key, &ptr, &len), let p = ptr else { return nil } + return String(bytes: UnsafeBufferPointer(start: p, count: len), encoding: .utf8) + } + + public func mapInt(forKey key: String) -> Int64? { var v: Int64 = 0; return colyseus_message_reader_map_get_int(raw, key, &v) ? v : nil } + public func mapUInt(forKey key: String) -> UInt64? { var v: UInt64 = 0; return colyseus_message_reader_map_get_uint(raw, key, &v) ? v : nil } + public func mapFloat(forKey key: String) -> Double? { var v: Double = 0; return colyseus_message_reader_map_get_float(raw, key, &v) ? v : nil } + public func mapBool(forKey key: String) -> Bool? { var v = false; return colyseus_message_reader_map_get_bool(raw, key, &v) ? v : nil } + + public func decode() -> MessageValue { + switch type { + case .nil: return .nil + case .bool: return .bool(boolValue) + case .int: return .int(intValue) + case .uint: return .uint(uintValue) + case .float: return .float(floatValue) + case .string: return .string(stringValue ?? "") + case .binary: return .binary(binaryValue ?? Data()) + case .array: + return .array((0 ..< arrayCount).compactMap { arrayElement(at: $0)?.decode() }) + case .map: + var result: [String: MessageValue] = [:] + var iter = colyseus_message_reader_map_iterator(raw) + while true { + var kp: OpaquePointer? = nil + var vp: OpaquePointer? = nil + guard colyseus_message_map_iterator_next(&iter, &kp, &vp), + let k = kp, let v = vp else { break } + let kr = MessageReader(k) + let vr = MessageReader(v) + result[kr.stringValue ?? String(kr.intValue)] = vr.decode() + } + return .map(result) + } + } +} + +public final class MessageBuilder { + let raw: OpaquePointer + private init(_ raw: OpaquePointer) { self.raw = raw } + deinit { colyseus_message_free(raw) } + + public static func map() -> MessageBuilder { MessageBuilder(colyseus_message_map_create()!) } + public static func array() -> MessageBuilder { MessageBuilder(colyseus_message_array_create()!) } + public static func nil_() -> MessageBuilder { MessageBuilder(colyseus_message_nil_create()!) } + public static func value(_ v: Bool) -> MessageBuilder { MessageBuilder(colyseus_message_bool_create(v)!) } + public static func value(_ v: Int64) -> MessageBuilder { MessageBuilder(colyseus_message_int_create(v)!) } + public static func value(_ v: UInt64) -> MessageBuilder { MessageBuilder(colyseus_message_uint_create(v)!) } + public static func value(_ v: Double) -> MessageBuilder { MessageBuilder(colyseus_message_float_create(v)!) } + public static func value(_ v: String) -> MessageBuilder { MessageBuilder(colyseus_message_str_create(v)!) } + + @discardableResult public func set(_ k: String, _ v: String) -> MessageBuilder { colyseus_message_map_put_str(raw, k, v); return self } + @discardableResult public func set(_ k: String, _ v: Int64) -> MessageBuilder { colyseus_message_map_put_int(raw, k, v); return self } + @discardableResult public func set(_ k: String, _ v: UInt64) -> MessageBuilder { colyseus_message_map_put_uint(raw, k, v); return self } + @discardableResult public func set(_ k: String, _ v: Double) -> MessageBuilder { colyseus_message_map_put_float(raw, k, v); return self } + @discardableResult public func set(_ k: String, _ v: Bool) -> MessageBuilder { colyseus_message_map_put_bool(raw, k, v); return self } + @discardableResult public func setNil(_ k: String) -> MessageBuilder { colyseus_message_map_put_nil(raw, k); return self } + @discardableResult public func set(_ k: String, _ v: MessageBuilder) -> MessageBuilder { colyseus_message_map_put(raw, k, v.raw); return self } + + @discardableResult public func push(_ v: String) -> MessageBuilder { colyseus_message_array_push_str(raw, v); return self } + @discardableResult public func push(_ v: Int64) -> MessageBuilder { colyseus_message_array_push_int(raw, v); return self } + @discardableResult public func push(_ v: UInt64) -> MessageBuilder { colyseus_message_array_push_uint(raw, v); return self } + @discardableResult public func push(_ v: Double) -> MessageBuilder { colyseus_message_array_push_float(raw, v); return self } + @discardableResult public func push(_ v: Bool) -> MessageBuilder { colyseus_message_array_push_bool(raw, v); return self } + @discardableResult public func pushNil() -> MessageBuilder { colyseus_message_array_push_nil(raw); return self } + @discardableResult public func push(_ v: MessageBuilder) -> MessageBuilder { colyseus_message_array_push(raw, v.raw); return self } + + public func encode() -> Data { + var length = 0 + guard let ptr = colyseus_message_encode(raw, &length) else { return Data() } + let data = Data(bytes: ptr, count: length) + colyseus_message_encoded_free(ptr, length) + return data + } +} diff --git a/platforms/swift/Sources/Colyseus/ColyseusRoom.swift b/platforms/swift/Sources/Colyseus/ColyseusRoom.swift new file mode 100644 index 0000000..c9798f4 --- /dev/null +++ b/platforms/swift/Sources/Colyseus/ColyseusRoom.swift @@ -0,0 +1,149 @@ + +import CColyseus +import Foundation + +/// Wraps a `colyseus_room_t*`. +/// All public event closures are invoked on the **main queue**. +public final class ColyseusRoom { + + let raw: UnsafeMutablePointer + private let ownsRaw: Bool + private var ctxPtr: UnsafeMutableRawPointer? + private var dynamicVtable: UnsafeMutablePointer? + + // MARK: - Public callbacks + + public var onJoin: (() -> Void)? + public var onLeave: ((Int32, String) -> Void)? + public var onError: ((Int32, String) -> Void)? + public var onStateChange: ((SchemaState) -> Void)? + public var onMessage: ((String, MessageValue) -> Void)? + public var onMessageRaw: ((String, Data) -> Void)? + + // MARK: - Lifecycle + + init(raw: UnsafeMutablePointer, owns: Bool = true) { + self.raw = raw + self.ownsRaw = owns + } + + deinit { + if let p = ctxPtr { RoomContext.release(p) } + if ownsRaw { colyseus_room_free(raw) } + if let v = dynamicVtable { colyseus_dynamic_vtable_free(v) } + } + + // MARK: - Internal setup + + func installCallbacks() { + let ctx = RoomContext(self) + let ptr = ctx.retain() + ctxPtr = ptr + + colyseus_room_on_join(raw, { ud in + guard let r = RoomContext.from(ud)?.room else { return } + DispatchQueue.main.async { r.onJoin?() } + }, ptr) + + colyseus_room_on_leave(raw, { code, reason, ud in + guard let r = RoomContext.from(ud)?.room else { return } + let s = reason.map { String(cString: $0) } ?? "" + DispatchQueue.main.async { r.onLeave?(code, s) } + }, ptr) + + colyseus_room_on_error(raw, { code, msg, ud in + guard let r = RoomContext.from(ud)?.room else { return } + let s = msg.map { String(cString: $0) } ?? "" + DispatchQueue.main.async { r.onError?(code, s) } + }, ptr) + + colyseus_room_on_state_change(raw, { ud in + guard let r = RoomContext.from(ud)?.room else { return } + guard let statePtr = colyseus_room_get_state(r.raw) else { return } + let state = SchemaWalker.walk(statePtr) + DispatchQueue.main.async { r.onStateChange?(state) } + }, ptr) + + // Single wildcard handler for all message types (encoded bytes path). + colyseus_room_on_message_any_with_type_encoded(raw, { msgType, data, length, ud in + guard let r = RoomContext.from(ud)?.room else { return } + let t = msgType.map { String(cString: $0) } ?? "" + guard let data = data else { return } + let bytes = Data(bytes: data, count: length) + if let readerPtr = colyseus_message_reader_create(data, length) { + let decoded = MessageReader(readerPtr).decode() + DispatchQueue.main.async { + r.onMessage?(t, decoded) + r.onMessageRaw?(t, bytes) + } + } else { + DispatchQueue.main.async { r.onMessageRaw?(t, bytes) } + } + }, ptr) + } + + // MARK: - Dynamic schema + + /// Opt-in to dynamic schema decoding. Call before the room connects. + /// After joining, `state` and `onStateChange` will be populated. + public func enableDynamicSchema() { + guard dynamicVtable == nil else { return } + guard let vtable = colyseus_dynamic_vtable_create("SwiftDynamic") else { return } + dynamicVtable = vtable + withUnsafePointer(to: vtable.pointee.base) { basePtr in + colyseus_room_set_state_type(raw, basePtr) + } + } + + // MARK: - Properties + + public var roomId: String? { colyseus_room_get_id(raw).map { String(cString: $0) } } + public var sessionId: String? { colyseus_room_get_session_id(raw).map { String(cString: $0) } } + public var name: String? { colyseus_room_get_name(raw).map { String(cString: $0) } } + public var reconnectionToken: String?{ colyseus_room_get_reconnection_token(raw).map { String(cString: $0) } } + public var isConnected: Bool { colyseus_room_is_connected(raw) } + + public var state: SchemaState? { + colyseus_room_get_state(raw).map { SchemaWalker.walk($0) } + } + + // MARK: - Send + + public func send(type: String, _ message: MessageBuilder) { + colyseus_room_send(raw, type, message.raw) + } + + public func send(type: Int32, _ message: MessageBuilder) { + colyseus_room_send_int(raw, type, message.raw) + } + + public func send(type: String, encoded data: Data) { + data.withUnsafeBytes { + colyseus_room_send_encoded(raw, type, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) + } + } + + public func send(type: Int32, encoded data: Data) { + data.withUnsafeBytes { + colyseus_room_send_int_encoded(raw, type, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) + } + } + + public func sendBytes(type: String, _ data: Data) { + data.withUnsafeBytes { + colyseus_room_send_bytes(raw, type, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) + } + } + + public func sendBytes(type: Int32, _ data: Data) { + data.withUnsafeBytes { + colyseus_room_send_int_bytes(raw, type, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) + } + } + + // MARK: - Leave + + public func leave(consented: Bool = true) { + colyseus_room_leave(raw, consented) + } +} diff --git a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift new file mode 100644 index 0000000..084bcd0 --- /dev/null +++ b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift @@ -0,0 +1,130 @@ + +import CColyseus +import Foundation + +/// Dynamic state as a nested dictionary. +/// Primitive fields -> Swift native types. +/// ref fields -> nested SchemaState. +/// array/map fields -> [Any] or [String: Any]. +public typealias SchemaState = [String: Any] + +// MARK: - Internal state walker + +enum SchemaWalker { + + /// Walk a colyseus_dynamic_schema_t* and return a SchemaState. + static func walk(_ ptr: UnsafeMutableRawPointer) -> SchemaState { + let schema = ptr.assumingMemoryBound(to: colyseus_dynamic_schema_t.self) + var result = SchemaState() + + // Iterate over all fields stored in the hash table. + typealias Ctx = (result: SchemaState, schema: UnsafeMutablePointer) + var ctx: Ctx = (result: [:], schema: schema) + + withUnsafeMutablePointer(to: &ctx) { ctxPtr in + colyseus_dynamic_schema_foreach( + schema, + { idx, name, value, userdata in + guard let name = name, + let value = value, + let userdata = userdata else { return } + let key = String(cString: name) + let ctx = userdata.assumingMemoryBound(to: Ctx.self) + ctx.pointee.result[key] = SchemaWalker.valueToSwift(value) + }, + ctxPtr + ) + } + return ctx.result + } + + static func valueToSwift(_ v: UnsafeMutablePointer) -> Any { + switch v.pointee.type { + case COLYSEUS_FIELD_STRING: + return v.pointee.data.str.map { String(cString: $0) } as Any? ?? NSNull() + case COLYSEUS_FIELD_BOOLEAN: + return v.pointee.data.boolean + case COLYSEUS_FIELD_INT8: return Int(v.pointee.data.i8) + case COLYSEUS_FIELD_UINT8: return Int(v.pointee.data.u8) + case COLYSEUS_FIELD_INT16: return Int(v.pointee.data.i16) + case COLYSEUS_FIELD_UINT16: return Int(v.pointee.data.u16) + case COLYSEUS_FIELD_INT32: return Int(v.pointee.data.i32) + case COLYSEUS_FIELD_UINT32: return Int(v.pointee.data.u32) + case COLYSEUS_FIELD_INT64: return v.pointee.data.i64 + case COLYSEUS_FIELD_UINT64: return v.pointee.data.u64 + case COLYSEUS_FIELD_NUMBER, COLYSEUS_FIELD_FLOAT64: + return v.pointee.data.num + case COLYSEUS_FIELD_FLOAT32: + return Double(v.pointee.data.f32) + case COLYSEUS_FIELD_REF: + if let ref = v.pointee.data.ref { + return walk(UnsafeMutableRawPointer(ref)) + } + return SchemaState() + case COLYSEUS_FIELD_ARRAY: + return decodeArray(v.pointee.data.array) + case COLYSEUS_FIELD_MAP: + return decodeMap(v.pointee.data.map) + default: + return NSNull() + } + } + + private static func decodeArray(_ arr: UnsafeMutablePointer?) -> [Any] { + guard let arr = arr else { return [] } + var result = [Any]() + colyseus_array_schema_foreach( + arr, + { _, value, userdata in + guard let value = value, let userdata = userdata else { return } + let list = userdata.assumingMemoryBound(to: [Any].self) + if arr.pointee.has_schema_child { + list.pointee.append(SchemaWalker.walk(value)) + } else { + list.pointee.append(SchemaWalker.primitiveFromPtr(value, arr.pointee.child_primitive_type)) + } + }, + &result + ) + return result + } + + private static func decodeMap(_ map: UnsafeMutablePointer?) -> SchemaState { + guard let map = map else { return [:] } + var result = SchemaState() + colyseus_map_schema_foreach( + map, + { key, value, userdata in + guard let key = key, let value = value, let userdata = userdata else { return } + let dict = userdata.assumingMemoryBound(to: SchemaState.self) + let k = String(cString: key) + if map.pointee.has_schema_child { + dict.pointee[k] = SchemaWalker.walk(value) + } else { + dict.pointee[k] = SchemaWalker.primitiveFromPtr(value, map.pointee.child_primitive_type) + } + }, + &result + ) + return result + } + + // Decode a raw void* primitive value using the type string stored in the collection. + private static func primitiveFromPtr(_ ptr: UnsafeMutableRawPointer, _ typeStr: UnsafePointer?) -> Any { + let t = typeStr.map { String(cString: $0) } ?? "number" + switch t { + case "string": return ptr.assumingMemoryBound(to: CChar.self).map { String(cString: $0) } as Any? ?? "" + case "boolean": return ptr.load(as: Bool.self) + case "int8": return Int(ptr.load(as: Int8.self)) + case "uint8": return Int(ptr.load(as: UInt8.self)) + case "int16": return Int(ptr.load(as: Int16.self)) + case "uint16": return Int(ptr.load(as: UInt16.self)) + case "int32": return Int(ptr.load(as: Int32.self)) + case "uint32": return Int(ptr.load(as: UInt32.self)) + case "int64": return ptr.load(as: Int64.self) + case "uint64": return ptr.load(as: UInt64.self) + case "float32": return Double(ptr.load(as: Float.self)) + default: return ptr.load(as: Double.self) + } + } +} diff --git a/platforms/swift/Sources/Colyseus/ColyseusSettings.swift b/platforms/swift/Sources/Colyseus/ColyseusSettings.swift new file mode 100644 index 0000000..77617ee --- /dev/null +++ b/platforms/swift/Sources/Colyseus/ColyseusSettings.swift @@ -0,0 +1,83 @@ +import CColyseus +import Foundation + +/// Wraps `colyseus_settings_t` with a Swift-friendly interface. +public final class ColyseusSettings { + + let raw: UnsafeMutablePointer + + public init() { + guard let ptr = colyseus_settings_create() else { + fatalError("colyseus_settings_create returned nil") + } + raw = ptr + } + + deinit { + colyseus_settings_free(raw) + } + + public var address: String { + get { raw.pointee.server_address.map { String(cString: $0) } ?? "localhost" } + set { colyseus_settings_set_address(raw, newValue) } + } + + public var port: String { + get { raw.pointee.server_port.map { String(cString: $0) } ?? "2567" } + set { colyseus_settings_set_port(raw, newValue) } + } + + public var secure: Bool { + get { raw.pointee.use_secure_protocol } + set { colyseus_settings_set_secure(raw, newValue) } + } + + public var tlsSkipVerification: Bool { + get { raw.pointee.tls_skip_verification } + set { raw.pointee.tls_skip_verification = newValue } + } + + public func setHeader(_ value: String, forKey key: String) { + colyseus_settings_add_header(raw, key, value) + } + + public func removeHeader(forKey key: String) { + colyseus_settings_remove_header(raw, key) + } + + public func header(forKey key: String) -> String? { + guard let ptr = colyseus_settings_get_header(raw, key) else { return nil } + return String(cString: ptr) + } + + public static func localhost(port: String = "2567", secure: Bool = false) -> ColyseusSettings { + let s = ColyseusSettings() + s.address = "localhost" + s.port = port + s.secure = secure + return s + } + + public static func from(endpoint: String) -> ColyseusSettings { + let s = ColyseusSettings() + var rest = endpoint + if rest.hasPrefix("wss://") { + s.secure = true + rest = String(rest.dropFirst(6)) + } else if rest.hasPrefix("ws://") { + s.secure = false + rest = String(rest.dropFirst(5)) + } + if let slash = rest.firstIndex(of: "/") { + rest = String(rest[.. UnsafeMutableRawPointer { + Unmanaged.passRetained(self).toOpaque() + } + + /// Releases the context previously retained by `retain()`. + static func release(_ ptr: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(ptr).release() + } + + static func from(_ ptr: UnsafeMutableRawPointer?) -> RoomContext? { + guard let p = ptr else { return nil } + return Unmanaged.fromOpaque(p).takeUnretainedValue() + } +} diff --git a/platforms/swift/build.sh b/platforms/swift/build.sh new file mode 100755 index 0000000..83ad82d --- /dev/null +++ b/platforms/swift/build.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# build.sh — Build Colyseus.xcframework for all Apple slices. +# +# Usage: +# ./build.sh [release|debug] # default: release +# ./build.sh release --skip-zip # skip zip archiving +# +# Outputs: +# build/Colyseus.xcframework # xcframework bundle +# build/Colyseus.xcframework.zip # distributable archive (for SPM) +# build/Colyseus.xcframework.zip.sha256 # checksum for Package.swift +# +# Requirements: zig 0.15+, Xcode Command Line Tools + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +BUILD_DIR="$SCRIPT_DIR/build" +ZIG_OUT="$BUILD_DIR/zig-out" +XCF_DIR="$BUILD_DIR/Colyseus.xcframework" + +OPTIMIZE="${1:-release}" +SKIP_ZIP="${2:-}" + +case "$OPTIMIZE" in + release) ZIG_OPT="-Doptimize=ReleaseFast" ;; + debug) ZIG_OPT="-Doptimize=Debug" ;; + *) echo "Unknown mode: $OPTIMIZE (use release or debug)"; exit 1 ;; +esac + +# Slices to build. +# Each entry: "zig-target sdk-name slice-dir-name" +declare -a SLICES=( + "aarch64-macos macosx macos-arm64" + "x86_64-macos macosx macos-x86_64" + "aarch64-ios iphoneos ios-arm64" + "aarch64-ios-simulator iphonesimulator ios-arm64-simulator" + "x86_64-ios-simulator iphonesimulator ios-x86_64-simulator" + "aarch64-tvos appletvos tvos-arm64" + "aarch64-tvos-simulator appletvsimulator tvos-arm64-simulator" +) + +rm -rf "$XCF_DIR" "$ZIG_OUT" +mkdir -p "$BUILD_DIR" + +echo "=== Building libcolyseus for all slices ===" + +declare -a XCFRAMEWORK_ARGS=() + +# Track which sdk/slice-groups we've built so we can lipo if needed. +declare -A SDK_LIBS # sdk-name -> "lib1 lib2 ..." +declare -A SDK_HDRS # sdk-name -> headers dir + +build_slice() { + local ZIG_TARGET="$1" + local SDK_NAME="$2" + local SLICE_NAME="$3" + + local SLICE_OUT="$ZIG_OUT/$SLICE_NAME" + mkdir -p "$SLICE_OUT" + + echo " Building $ZIG_TARGET ($SDK_NAME) ..." + + local SDK_PATH + SDK_PATH="$(xcrun --sdk "$SDK_NAME" --show-sdk-path 2>/dev/null || true)" + local SDK_ARG="" + if [[ -n "$SDK_PATH" ]]; then + SDK_ARG="-Dapple-sdk=$SDK_PATH" + fi + + ( + cd "$SCRIPT_DIR" + zig build \ + -Dtarget="$ZIG_TARGET" \ + $ZIG_OPT \ + $SDK_ARG \ + --prefix "$SLICE_OUT" \ + 2>&1 | sed "s/^/ [$SLICE_NAME] /" + ) + + echo " Done: $SLICE_OUT/lib/libcolyseus.a" +} + +for SLICE_SPEC in "${SLICES[@]}"; do + read -r ZIG_TARGET SDK_NAME SLICE_NAME <<< "$SLICE_SPEC" + build_slice "$ZIG_TARGET" "$SDK_NAME" "$SLICE_NAME" +done + +echo "" +echo "=== Assembling xcframework ===" + +# Group slices by SDK for lipo (fat binary per SDK variant). +# macOS: lipo arm64 + x86_64 -> universal +# iOS device: single arm64 +# iOS simulator: lipo arm64-sim + x86_64-sim -> universal +# tvOS device: single arm64 +# tvOS simulator: single arm64-sim + +lipo_or_copy() { + local OUT_LIB="$1"; shift + local INPUTS=("$@") + if [[ ${#INPUTS[@]} -gt 1 ]]; then + lipo -create "${INPUTS[@]}" -output "$OUT_LIB" + else + cp "${INPUTS[0]}" "$OUT_LIB" + fi +} + +# Copy headers from any slice (they're identical). +# zig-out installs colyseus headers into include/colyseus/; we copy the +# entire include tree plus our umbrella header and module map. +HEADERS_SRC="$ZIG_OUT/macos-arm64/include" +HEADERS_DST="$BUILD_DIR/Headers" +rm -rf "$HEADERS_DST" +cp -R "$HEADERS_SRC" "$HEADERS_DST" +# Umbrella header and module map must live at the top of the Headers dir. +cp "$SCRIPT_DIR/include/colyseus_swift.h" "$HEADERS_DST/" +cp "$SCRIPT_DIR/include/module.modulemap" "$HEADERS_DST/" + +build_variant() { + local VARIANT_NAME="$1"; shift + local LIB_DIR="$BUILD_DIR/libs/$VARIANT_NAME" + mkdir -p "$LIB_DIR" + local OUT_LIB="$LIB_DIR/libcolyseus.a" + lipo_or_copy "$OUT_LIB" "$@" + # xcframework needs headers alongside each library. + local HDR_DIR="$LIB_DIR/Headers" + cp -R "$HEADERS_DST" "$HDR_DIR" + XCFRAMEWORK_ARGS+=("-library" "$OUT_LIB" "-headers" "$HDR_DIR") +} + +build_variant "macos" \ + "$ZIG_OUT/macos-arm64/lib/libcolyseus.a" \ + "$ZIG_OUT/macos-x86_64/lib/libcolyseus.a" + +build_variant "ios" \ + "$ZIG_OUT/ios-arm64/lib/libcolyseus.a" + +build_variant "ios-simulator" \ + "$ZIG_OUT/ios-arm64-simulator/lib/libcolyseus.a" \ + "$ZIG_OUT/ios-x86_64-simulator/lib/libcolyseus.a" + +build_variant "tvos" \ + "$ZIG_OUT/tvos-arm64/lib/libcolyseus.a" + +build_variant "tvos-simulator" \ + "$ZIG_OUT/tvos-arm64-simulator/lib/libcolyseus.a" + +xcodebuild -create-xcframework "${XCFRAMEWORK_ARGS[@]}" -output "$XCF_DIR" + +echo "" +echo "=== xcframework built: $XCF_DIR ===" + +if [[ "$SKIP_ZIP" != "--skip-zip" ]]; then + echo "" + echo "=== Archiving ===" + ZIP_PATH="$BUILD_DIR/Colyseus.xcframework.zip" + (cd "$BUILD_DIR" && zip -qr "$(basename "$ZIP_PATH")" Colyseus.xcframework) + CHECKSUM=$(swift package compute-checksum "$ZIP_PATH" 2>/dev/null || shasum -a 256 "$ZIP_PATH" | awk '{print $1}') + echo "$CHECKSUM" > "$ZIP_PATH.sha256" + echo "Archive: $ZIP_PATH" + echo "Checksum: $CHECKSUM" + echo "" + echo "Update Package.swift binaryTarget checksum with: $CHECKSUM" +fi + +echo "" +echo "Done." diff --git a/platforms/swift/build.zig b/platforms/swift/build.zig new file mode 100644 index 0000000..49e2f2f --- /dev/null +++ b/platforms/swift/build.zig @@ -0,0 +1,464 @@ +const std = @import("std"); + +// Builds libcolyseus as a static library for each Apple slice. +// The build.sh wrapper calls this once per slice then uses xcodebuild +// to assemble the xcframework. +// +// Supported targets (pass via -Dtarget=…): +// aarch64-macos – macOS Apple Silicon +// x86_64-macos – macOS Intel +// aarch64-ios – iOS device +// aarch64-ios-simulator – iOS Simulator (Apple Silicon host) +// x86_64-ios-simulator – iOS Simulator (Intel host) +// aarch64-tvos – tvOS device +// aarch64-tvos-simulator – tvOS Simulator + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const os_tag = target.result.os.tag; + const is_ios = os_tag == .ios; + const is_tvos = os_tag == .tvos; + const is_apple = os_tag == .macos or is_ios or is_tvos; + + // Auto-detect Apple SDK path via xcrun + const apple_sdk_path: ?[]const u8 = b.option( + []const u8, + "apple-sdk", + "Path to Apple SDK (auto-detected via xcrun if omitted)", + ) orelse blk: { + if (!is_apple) break :blk null; + const sdk_name: []const u8 = switch (os_tag) { + .macos => "macosx", + .tvos => "appletvos", + else => "iphoneos", + }; + // For simulators, use the simulator SDK + const is_sim = target.result.abi == .simulator; + const sdk: []const u8 = if (is_sim) switch (os_tag) { + .ios => "iphonesimulator", + .tvos => "appletvsimulator", + else => sdk_name, + } else sdk_name; + + const result = std.process.Child.run(.{ + .allocator = b.allocator, + .argv = &.{ "xcrun", "--sdk", sdk, "--show-sdk-path" }, + }) catch break :blk null; + defer b.allocator.free(result.stdout); + defer b.allocator.free(result.stderr); + if (result.term.Exited == 0 and result.stdout.len > 0) { + const trimmed = std.mem.trimRight(u8, result.stdout, "\n\r"); + break :blk b.allocator.dupe(u8, trimmed) catch null; + } + break :blk null; + }; + + // ------------------------------------------------------------------------- + // wslay (WebSocket framing) + // ------------------------------------------------------------------------- + const wslay_config_h = b.addConfigHeader(.{ + .style = .blank, + .include_path = "config.h", + }, .{ + .HAVE_ARPA_INET_H = 1, + .HAVE_NETINET_IN_H = 1, + }); + + const wslay_version_h = b.addConfigHeader(.{ + .style = .{ .cmake = b.path("../../third_party/wslay/lib/includes/wslay/wslayver.h.in") }, + .include_path = "wslay/wslayver.h", + }, .{ .PACKAGE_VERSION = "1.1.1" }); + + const wslay = b.addLibrary(.{ + .name = "wslay", + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + .linkage = .static, + }); + appleLibc(wslay, apple_sdk_path); + wslay.addIncludePath(b.path("../../third_party/wslay/lib/includes")); + wslay.addIncludePath(b.path("../../third_party/wslay/lib")); + wslay.addConfigHeader(wslay_config_h); + wslay.addConfigHeader(wslay_version_h); + wslay.addCSourceFiles(.{ + .files = &.{ + "../../third_party/wslay/lib/wslay_event.c", + "../../third_party/wslay/lib/wslay_frame.c", + "../../third_party/wslay/lib/wslay_net.c", + "../../third_party/wslay/lib/wslay_queue.c", + }, + .flags = &.{ "-Wall", "-std=c11", "-DHAVE_CONFIG_H" }, + }); + + // ------------------------------------------------------------------------- + // mbedTLS + // ------------------------------------------------------------------------- + const mbedcrypto = buildMbedcrypto(b, target, optimize, apple_sdk_path); + const mbedx509 = buildMbedx509(b, target, optimize, apple_sdk_path, mbedcrypto); + const mbedtls = buildMbedtls(b, target, optimize, apple_sdk_path, mbedx509, mbedcrypto); + + // ------------------------------------------------------------------------- + // Zig helper modules + // ------------------------------------------------------------------------- + const msgpack_dep = b.dependency("zig_msgpack", .{ + .target = target, + .optimize = optimize, + }); + const msgpack_module = msgpack_dep.module("msgpack"); + + const msgpack_builder_mod = b.createModule(.{ + .root_source_file = b.path("../../src/msgpack/msgpack_builder.zig"), + .target = target, + .optimize = optimize, + }); + msgpack_builder_mod.addImport("msgpack", msgpack_module); + const msgpack_builder = b.addLibrary(.{ + .name = "msgpack_builder", + .root_module = msgpack_builder_mod, + .linkage = .static, + }); + appleLibc(msgpack_builder, apple_sdk_path); + + const msgpack_reader_mod = b.createModule(.{ + .root_source_file = b.path("../../src/msgpack/msgpack_reader.zig"), + .target = target, + .optimize = optimize, + }); + msgpack_reader_mod.addImport("msgpack", msgpack_module); + const msgpack_reader = b.addLibrary(.{ + .name = "msgpack_reader", + .root_module = msgpack_reader_mod, + .linkage = .static, + }); + appleLibc(msgpack_reader, apple_sdk_path); + + const strutil_mod = b.createModule(.{ + .root_source_file = b.path("../../src/utils/strUtil.zig"), + .target = target, + .optimize = optimize, + }); + const strutil = b.addLibrary(.{ + .name = "strutil_zig", + .root_module = strutil_mod, + .linkage = .static, + }); + appleLibc(strutil, apple_sdk_path); + + const http_mod = b.createModule(.{ + .root_source_file = b.path("../../src/network/http.zig"), + .target = target, + .optimize = optimize, + }); + http_mod.addIncludePath(b.path("../../include")); + http_mod.addIncludePath(b.path("../../third_party/uthash/src")); + const http_zig = b.addLibrary(.{ + .name = "http_zig", + .root_module = http_mod, + .linkage = .static, + }); + appleLibc(http_zig, apple_sdk_path); + + const syscerts_mod = b.createModule(.{ + .root_source_file = b.path("../../src/certs/system_certs.zig"), + .target = target, + .optimize = optimize, + }); + const syscerts = b.addLibrary(.{ + .name = "system_certs_zig", + .root_module = syscerts_mod, + .linkage = .static, + }); + appleLibc(syscerts, apple_sdk_path); + + // ------------------------------------------------------------------------- + // Core colyseus C library + // ------------------------------------------------------------------------- + const colyseus = b.addLibrary(.{ + .name = "colyseus", + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + .linkage = .static, + }); + appleLibc(colyseus, apple_sdk_path); + + colyseus.addIncludePath(b.path("../../include")); + colyseus.addIncludePath(b.path("../../src")); + colyseus.addIncludePath(b.path("../../third_party/sds")); + colyseus.addIncludePath(b.path("../../third_party/uthash/src")); + colyseus.addIncludePath(b.path("../../third_party/cJSON")); + colyseus.addIncludePath(b.path("../../third_party/wslay/lib/includes")); + colyseus.addIncludePath(b.path("../../third_party/mbedtls/include")); + colyseus.addIncludePath(wslay_config_h.getOutput().dirname()); + colyseus.addIncludePath(wslay_version_h.getOutput().dirname().dirname()); + + colyseus.addCSourceFiles(.{ + .files = &.{ + "../../src/common/settings.c", + "../../src/client.c", + "../../src/room.c", + "../../src/network/websocket_transport.c", + "../../src/schema/decode.c", + "../../src/schema/ref_tracker.c", + "../../src/schema/collections.c", + "../../src/schema/decoder.c", + "../../src/schema/serializer.c", + "../../src/schema/callbacks.c", + "../../src/schema/dynamic_schema.c", + "../../src/utils/strUtil.c", + "../../src/utils/sha1_c.c", + "../../src/auth/auth.c", + "../../src/auth/secure_storage.c", + "../../src/certs/ca_bundle.c", + "../../third_party/sds/sds.c", + "../../third_party/cJSON/cJSON.c", + }, + .flags = &.{ "-Wall", "-Wextra", "-std=c11" }, + }); + + colyseus.linkLibrary(mbedtls); + colyseus.linkLibrary(mbedx509); + colyseus.linkLibrary(mbedcrypto); + colyseus.linkLibrary(wslay); + colyseus.linkLibrary(http_zig); + colyseus.linkLibrary(syscerts); + colyseus.linkLibrary(strutil); + colyseus.linkLibrary(msgpack_builder); + colyseus.linkLibrary(msgpack_reader); + + // Frameworks are needed at final link time (by the Swift consumer), not + // when building the static library. We add the SDK framework search path + // so that Zig can resolve them if it needs to, but we don't force-link + // them into the static archive. + if (apple_sdk_path) |sdk| { + const fw_path = b.pathJoin(&.{ sdk, "System/Library/Frameworks" }); + colyseus.addFrameworkPath(.{ .cwd_relative = fw_path }); + } + // Record the framework dependencies so they are propagated to the linker + // when the static lib is consumed. + colyseus.linkFramework("CoreFoundation"); + colyseus.linkFramework("Security"); + if (os_tag == .macos) { + colyseus.linkSystemLibrary("pthread"); + } + + b.installArtifact(colyseus); + + // Install colyseus public headers so build.sh can copy them into the xcframework. + const header_pairs = [_][2][]const u8{ + .{ "../../include/colyseus/client.h", "colyseus/client.h" }, + .{ "../../include/colyseus/room.h", "colyseus/room.h" }, + .{ "../../include/colyseus/settings.h", "colyseus/settings.h" }, + .{ "../../include/colyseus/protocol.h", "colyseus/protocol.h" }, + .{ "../../include/colyseus/transport.h", "colyseus/transport.h" }, + .{ "../../include/colyseus/messages.h", "colyseus/messages.h" }, + .{ "../../include/colyseus/schema.h", "colyseus/schema.h" }, + .{ "../../include/colyseus/http.h", "colyseus/http.h" }, + .{ "../../include/colyseus/auth/auth.h", "colyseus/auth/auth.h" }, + .{ "../../include/colyseus/auth/secure_storage.h", "colyseus/auth/secure_storage.h" }, + .{ "../../include/colyseus/schema/types.h", "colyseus/schema/types.h" }, + .{ "../../include/colyseus/schema/decode.h", "colyseus/schema/decode.h" }, + .{ "../../include/colyseus/schema/decoder.h", "colyseus/schema/decoder.h" }, + .{ "../../include/colyseus/schema/collections.h", "colyseus/schema/collections.h" }, + .{ "../../include/colyseus/schema/callbacks.h", "colyseus/schema/callbacks.h" }, + .{ "../../include/colyseus/schema/dynamic_schema.h", "colyseus/schema/dynamic_schema.h" }, + .{ "../../include/colyseus/schema/ref_tracker.h", "colyseus/schema/ref_tracker.h" }, + .{ "../../third_party/uthash/src/uthash.h", "uthash.h" }, + }; + for (header_pairs) |pair| { + const install_hdr = b.addInstallHeaderFile(b.path(pair[0]), pair[1]); + b.getInstallStep().dependOn(&install_hdr.step); + } +} + +// ─── helpers ───────────────────────────────────────────────────────────────── + +fn appleLibc(lib: *std.Build.Step.Compile, sdk_path: ?[]const u8) void { + lib.linkLibC(); + if (sdk_path) |sdk| { + const b = lib.step.owner; + const inc = b.pathJoin(&.{ sdk, "usr/include" }); + lib.addSystemIncludePath(.{ .cwd_relative = inc }); + } +} + +fn buildMbedcrypto( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + sdk_path: ?[]const u8, +) *std.Build.Step.Compile { + const lib = b.addLibrary(.{ + .name = "mbedcrypto", + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + .linkage = .static, + }); + appleLibc(lib, sdk_path); + lib.addIncludePath(b.path("../../third_party/mbedtls/include")); + lib.addIncludePath(b.path("../../third_party/mbedtls/library")); + lib.addCSourceFiles(.{ + .files = &MBEDCRYPTO_SOURCES, + .flags = &.{ "-Wall", "-std=c11" }, + }); + return lib; +} + +fn buildMbedx509( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + sdk_path: ?[]const u8, + mbedcrypto: *std.Build.Step.Compile, +) *std.Build.Step.Compile { + const lib = b.addLibrary(.{ + .name = "mbedx509", + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + .linkage = .static, + }); + appleLibc(lib, sdk_path); + lib.addIncludePath(b.path("../../third_party/mbedtls/include")); + lib.addIncludePath(b.path("../../third_party/mbedtls/library")); + lib.addCSourceFiles(.{ + .files = &.{ + "../../third_party/mbedtls/library/x509.c", + "../../third_party/mbedtls/library/x509_create.c", + "../../third_party/mbedtls/library/x509_crl.c", + "../../third_party/mbedtls/library/x509_crt.c", + "../../third_party/mbedtls/library/x509_csr.c", + "../../third_party/mbedtls/library/x509write.c", + "../../third_party/mbedtls/library/x509write_crt.c", + "../../third_party/mbedtls/library/x509write_csr.c", + }, + .flags = &.{ "-Wall", "-std=c11" }, + }); + lib.linkLibrary(mbedcrypto); + return lib; +} + +fn buildMbedtls( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + sdk_path: ?[]const u8, + mbedx509: *std.Build.Step.Compile, + mbedcrypto: *std.Build.Step.Compile, +) *std.Build.Step.Compile { + const lib = b.addLibrary(.{ + .name = "mbedtls", + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + .linkage = .static, + }); + appleLibc(lib, sdk_path); + lib.addIncludePath(b.path("../../third_party/mbedtls/include")); + lib.addIncludePath(b.path("../../third_party/mbedtls/library")); + lib.addCSourceFiles(.{ + .files = &.{ + "../../third_party/mbedtls/library/debug.c", + "../../third_party/mbedtls/library/net_sockets.c", + "../../third_party/mbedtls/library/ssl_cache.c", + "../../third_party/mbedtls/library/ssl_ciphersuites.c", + "../../third_party/mbedtls/library/ssl_client.c", + "../../third_party/mbedtls/library/ssl_cookie.c", + "../../third_party/mbedtls/library/ssl_debug_helpers_generated.c", + "../../third_party/mbedtls/library/ssl_msg.c", + "../../third_party/mbedtls/library/ssl_ticket.c", + "../../third_party/mbedtls/library/ssl_tls.c", + "../../third_party/mbedtls/library/ssl_tls12_client.c", + "../../third_party/mbedtls/library/ssl_tls12_server.c", + "../../third_party/mbedtls/library/ssl_tls13_client.c", + "../../third_party/mbedtls/library/ssl_tls13_generic.c", + "../../third_party/mbedtls/library/ssl_tls13_keys.c", + "../../third_party/mbedtls/library/ssl_tls13_server.c", + }, + .flags = &.{ "-Wall", "-std=c11" }, + }); + lib.linkLibrary(mbedx509); + lib.linkLibrary(mbedcrypto); + return lib; +} + +const MBEDCRYPTO_SOURCES = [_][]const u8{ + "../../third_party/mbedtls/library/aes.c", + "../../third_party/mbedtls/library/aesce.c", + "../../third_party/mbedtls/library/aesni.c", + "../../third_party/mbedtls/library/aria.c", + "../../third_party/mbedtls/library/asn1parse.c", + "../../third_party/mbedtls/library/asn1write.c", + "../../third_party/mbedtls/library/base64.c", + "../../third_party/mbedtls/library/bignum.c", + "../../third_party/mbedtls/library/bignum_core.c", + "../../third_party/mbedtls/library/bignum_mod.c", + "../../third_party/mbedtls/library/bignum_mod_raw.c", + "../../third_party/mbedtls/library/block_cipher.c", + "../../third_party/mbedtls/library/camellia.c", + "../../third_party/mbedtls/library/ccm.c", + "../../third_party/mbedtls/library/chacha20.c", + "../../third_party/mbedtls/library/chachapoly.c", + "../../third_party/mbedtls/library/cipher.c", + "../../third_party/mbedtls/library/cipher_wrap.c", + "../../third_party/mbedtls/library/cmac.c", + "../../third_party/mbedtls/library/constant_time.c", + "../../third_party/mbedtls/library/ctr_drbg.c", + "../../third_party/mbedtls/library/des.c", + "../../third_party/mbedtls/library/dhm.c", + "../../third_party/mbedtls/library/ecdh.c", + "../../third_party/mbedtls/library/ecdsa.c", + "../../third_party/mbedtls/library/ecjpake.c", + "../../third_party/mbedtls/library/ecp.c", + "../../third_party/mbedtls/library/ecp_curves.c", + "../../third_party/mbedtls/library/ecp_curves_new.c", + "../../third_party/mbedtls/library/entropy.c", + "../../third_party/mbedtls/library/entropy_poll.c", + "../../third_party/mbedtls/library/error.c", + "../../third_party/mbedtls/library/gcm.c", + "../../third_party/mbedtls/library/hkdf.c", + "../../third_party/mbedtls/library/hmac_drbg.c", + "../../third_party/mbedtls/library/lmots.c", + "../../third_party/mbedtls/library/lms.c", + "../../third_party/mbedtls/library/md.c", + "../../third_party/mbedtls/library/md5.c", + "../../third_party/mbedtls/library/memory_buffer_alloc.c", + "../../third_party/mbedtls/library/mps_reader.c", + "../../third_party/mbedtls/library/mps_trace.c", + "../../third_party/mbedtls/library/nist_kw.c", + "../../third_party/mbedtls/library/oid.c", + "../../third_party/mbedtls/library/padlock.c", + "../../third_party/mbedtls/library/pem.c", + "../../third_party/mbedtls/library/pk.c", + "../../third_party/mbedtls/library/pk_ecc.c", + "../../third_party/mbedtls/library/pk_wrap.c", + "../../third_party/mbedtls/library/pkcs12.c", + "../../third_party/mbedtls/library/pkcs5.c", + "../../third_party/mbedtls/library/pkcs7.c", + "../../third_party/mbedtls/library/pkparse.c", + "../../third_party/mbedtls/library/pkwrite.c", + "../../third_party/mbedtls/library/platform.c", + "../../third_party/mbedtls/library/platform_util.c", + "../../third_party/mbedtls/library/poly1305.c", + "../../third_party/mbedtls/library/psa_crypto.c", + "../../third_party/mbedtls/library/psa_crypto_aead.c", + "../../third_party/mbedtls/library/psa_crypto_cipher.c", + "../../third_party/mbedtls/library/psa_crypto_client.c", + "../../third_party/mbedtls/library/psa_crypto_driver_wrappers_no_static.c", + "../../third_party/mbedtls/library/psa_crypto_ecp.c", + "../../third_party/mbedtls/library/psa_crypto_ffdh.c", + "../../third_party/mbedtls/library/psa_crypto_hash.c", + "../../third_party/mbedtls/library/psa_crypto_mac.c", + "../../third_party/mbedtls/library/psa_crypto_pake.c", + "../../third_party/mbedtls/library/psa_crypto_rsa.c", + "../../third_party/mbedtls/library/psa_crypto_se.c", + "../../third_party/mbedtls/library/psa_crypto_slot_management.c", + "../../third_party/mbedtls/library/psa_crypto_storage.c", + "../../third_party/mbedtls/library/psa_its_file.c", + "../../third_party/mbedtls/library/psa_util.c", + "../../third_party/mbedtls/library/ripemd160.c", + "../../third_party/mbedtls/library/rsa.c", + "../../third_party/mbedtls/library/rsa_alt_helpers.c", + "../../third_party/mbedtls/library/sha1.c", + "../../third_party/mbedtls/library/sha256.c", + "../../third_party/mbedtls/library/sha3.c", + "../../third_party/mbedtls/library/sha512.c", + "../../third_party/mbedtls/library/threading.c", + "../../third_party/mbedtls/library/timing.c", + "../../third_party/mbedtls/library/version.c", + "../../third_party/mbedtls/library/version_features.c", +}; diff --git a/platforms/swift/build.zig.zon b/platforms/swift/build.zig.zon new file mode 100644 index 0000000..03ec549 --- /dev/null +++ b/platforms/swift/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .colyseus_swift, + .version = "0.17.0", + .fingerprint = 0x4820f20cdd762ba, + .dependencies = .{ + .zig_msgpack = .{ + .url = "git+https://github.com/zigcc/zig-msgpack?ref=main#47c15d217efebd17a585dd04125f9b6b6f659d5a", + .hash = "zig_msgpack-0.0.14-evvueGolBQBs_m726tBf4ISJQqC926AM96EtxZy9LUaI", + }, + }, + .paths = .{""}, +} diff --git a/platforms/swift/example/ColyseusExample/ColyseusExampleApp.swift b/platforms/swift/example/ColyseusExample/ColyseusExampleApp.swift new file mode 100644 index 0000000..a357433 --- /dev/null +++ b/platforms/swift/example/ColyseusExample/ColyseusExampleApp.swift @@ -0,0 +1,11 @@ + +import SwiftUI + +@main +struct ColyseusExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/platforms/swift/example/ColyseusExample/ContentView.swift b/platforms/swift/example/ColyseusExample/ContentView.swift new file mode 100644 index 0000000..e951da7 --- /dev/null +++ b/platforms/swift/example/ColyseusExample/ContentView.swift @@ -0,0 +1,97 @@ + +import SwiftUI +import Colyseus + +@MainActor +final class GameViewModel: ObservableObject { + @Published var status: String = "Disconnected" + @Published var stateLog: [String] = [] + @Published var messages: [String] = [] + + private var client: ColyseusClient? + private var room: ColyseusRoom? + + func connect() { + status = "Connecting..." + let settings = ColyseusSettings.localhost(port: "2567") + client = ColyseusClient(settings: settings) + + Task { + do { + let r = try await client!.joinOrCreate("my_room") + r.enableDynamicSchema() + + r.onJoin = { [weak self] in + self?.status = "Joined room: \(r.roomId ?? "?")" + } + r.onLeave = { [weak self] code, reason in + self?.status = "Left (\(code)): \(reason)" + } + r.onError = { [weak self] code, msg in + self?.status = "Error \(code): \(msg)" + } + r.onStateChange = { [weak self] state in + let desc = state.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ") + self?.stateLog.insert(desc, at: 0) + if (self?.stateLog.count ?? 0) > 20 { self?.stateLog.removeLast() } + } + r.onMessage = { [weak self] type, value in + self?.messages.insert("[\(type)] \(value)", at: 0) + if (self?.messages.count ?? 0) > 20 { self?.messages.removeLast() } + } + room = r + } catch let e as ColyseusError { + status = "Failed (\(e.code)): \(e.message)" + } + } + } + + func sendPing() { + let msg = MessageBuilder.map().set("action", "ping") + room?.send(type: "test", msg) + } + + func disconnect() { + room?.leave() + room = nil + client = nil + status = "Disconnected" + } +} + +struct ContentView: View { + @StateObject private var vm = GameViewModel() + + var body: some View { + NavigationStack { + VStack(alignment: .leading, spacing: 12) { + Text(vm.status) + .font(.headline) + .padding(.horizontal) + + HStack { + Button("Connect", action: vm.connect) + Button("Ping", action: vm.sendPing) + Button("Disconnect", action: vm.disconnect) + .foregroundStyle(.red) + } + .buttonStyle(.bordered) + .padding(.horizontal) + + Divider() + + Text("State").font(.subheadline).bold().padding(.horizontal) + List(vm.stateLog, id: \.self) { Text($0).font(.caption) } + .frame(maxHeight: 200) + + Text("Messages").font(.subheadline).bold().padding(.horizontal) + List(vm.messages, id: \.self) { Text($0).font(.caption) } + } + .navigationTitle("Colyseus Example") + } + } +} + +#Preview { + ContentView() +} diff --git a/platforms/swift/example/Package.swift b/platforms/swift/example/Package.swift new file mode 100644 index 0000000..9cc35d8 --- /dev/null +++ b/platforms/swift/example/Package.swift @@ -0,0 +1,24 @@ + +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "ColyseusExample", + platforms: [ + .macOS(.v12), + .iOS(.v15), + ], + dependencies: [ + // Point at the local SDK package. + .package(path: ".."), + ], + targets: [ + .executableTarget( + name: "ColyseusExample", + dependencies: [ + .product(name: "Colyseus", package: "Colyseus"), + ], + path: "ColyseusExample" + ), + ] +) diff --git a/platforms/swift/include/colyseus_swift.h b/platforms/swift/include/colyseus_swift.h new file mode 100644 index 0000000..1a791ab --- /dev/null +++ b/platforms/swift/include/colyseus_swift.h @@ -0,0 +1,21 @@ +#ifndef COLYSEUS_SWIFT_H +#define COLYSEUS_SWIFT_H + +// Umbrella header for Swift C interop. +// Swift imports this via the module map bundled in the xcframework. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif /* COLYSEUS_SWIFT_H */ From 888dbdab81cb73a6ef4856a0daf38830400131c3 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 01:33:51 +0530 Subject: [PATCH 02/17] remove declare from build.sh --- platforms/swift/build.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/platforms/swift/build.sh b/platforms/swift/build.sh index 83ad82d..f6ab4b3 100755 --- a/platforms/swift/build.sh +++ b/platforms/swift/build.sh @@ -31,7 +31,7 @@ esac # Slices to build. # Each entry: "zig-target sdk-name slice-dir-name" -declare -a SLICES=( +SLICES=( "aarch64-macos macosx macos-arm64" "x86_64-macos macosx macos-x86_64" "aarch64-ios iphoneos ios-arm64" @@ -46,11 +46,7 @@ mkdir -p "$BUILD_DIR" echo "=== Building libcolyseus for all slices ===" -declare -a XCFRAMEWORK_ARGS=() - -# Track which sdk/slice-groups we've built so we can lipo if needed. -declare -A SDK_LIBS # sdk-name -> "lib1 lib2 ..." -declare -A SDK_HDRS # sdk-name -> headers dir +XCFRAMEWORK_ARGS=() build_slice() { local ZIG_TARGET="$1" From 8adf7ef88ca09eb5b12c619d421760e7ca19abaf Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 01:49:17 +0530 Subject: [PATCH 03/17] fixing build errors, removing tvos support --- platforms/swift/build.sh | 15 ++--- platforms/swift/build.zig | 128 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 13 deletions(-) diff --git a/platforms/swift/build.sh b/platforms/swift/build.sh index f6ab4b3..7189eb8 100755 --- a/platforms/swift/build.sh +++ b/platforms/swift/build.sh @@ -37,8 +37,9 @@ SLICES=( "aarch64-ios iphoneos ios-arm64" "aarch64-ios-simulator iphonesimulator ios-arm64-simulator" "x86_64-ios-simulator iphonesimulator ios-x86_64-simulator" - "aarch64-tvos appletvos tvos-arm64" - "aarch64-tvos-simulator appletvsimulator tvos-arm64-simulator" + # tvOS disabled — Zig 0.15 stdlib has incomplete tvOS aarch64 DWARF support + # "aarch64-tvos appletvos tvos-arm64" + # "aarch64-tvos-simulator appletvsimulator tvos-arm64-simulator" ) rm -rf "$XCF_DIR" "$ZIG_OUT" @@ -137,11 +138,11 @@ build_variant "ios-simulator" \ "$ZIG_OUT/ios-arm64-simulator/lib/libcolyseus.a" \ "$ZIG_OUT/ios-x86_64-simulator/lib/libcolyseus.a" -build_variant "tvos" \ - "$ZIG_OUT/tvos-arm64/lib/libcolyseus.a" - -build_variant "tvos-simulator" \ - "$ZIG_OUT/tvos-arm64-simulator/lib/libcolyseus.a" +#build_variant "tvos" \ +# "$ZIG_OUT/tvos-arm64/lib/libcolyseus.a" +# +#build_variant "tvos-simulator" \ +# "$ZIG_OUT/tvos-arm64-simulator/lib/libcolyseus.a" xcodebuild -create-xcframework "${XCFRAMEWORK_ARGS[@]}" -output "$XCF_DIR" diff --git a/platforms/swift/build.zig b/platforms/swift/build.zig index 49e2f2f..edaab38 100644 --- a/platforms/swift/build.zig +++ b/platforms/swift/build.zig @@ -287,6 +287,9 @@ fn buildMbedcrypto( optimize: std.builtin.OptimizeMode, sdk_path: ?[]const u8, ) *std.Build.Step.Compile { + const is_aarch64_sim = target.result.cpu.arch == .aarch64 and + target.result.abi == .simulator; + const lib = b.addLibrary(.{ .name = "mbedcrypto", .root_module = b.createModule(.{ .target = target, .optimize = optimize }), @@ -295,10 +298,21 @@ fn buildMbedcrypto( appleLibc(lib, sdk_path); lib.addIncludePath(b.path("../../third_party/mbedtls/include")); lib.addIncludePath(b.path("../../third_party/mbedtls/library")); - lib.addCSourceFiles(.{ - .files = &MBEDCRYPTO_SOURCES, - .flags = &.{ "-Wall", "-std=c11" }, - }); + + if (is_aarch64_sim) { + // aarch64-ios-simulator: Zig 0.15's baseline mcpu conflicts with + // mbedTLS hardware AES intrinsics. Disable AESCE entirely — the + // software AES fallback is sufficient for simulator builds. + lib.addCSourceFiles(.{ + .files = &MBEDCRYPTO_SOURCES_NO_AESCE, + .flags = &.{ "-Wall", "-std=c11", "-U__ARM_NEON", "-UMBEDTLS_AESCE_C" }, + }); + } else { + lib.addCSourceFiles(.{ + .files = &MBEDCRYPTO_SOURCES, + .flags = &.{ "-Wall", "-std=c11" }, + }); + } return lib; } @@ -309,6 +323,9 @@ fn buildMbedx509( sdk_path: ?[]const u8, mbedcrypto: *std.Build.Step.Compile, ) *std.Build.Step.Compile { + const is_aarch64_sim = target.result.cpu.arch == .aarch64 and + target.result.abi == .simulator; + const lib = b.addLibrary(.{ .name = "mbedx509", .root_module = b.createModule(.{ .target = target, .optimize = optimize }), @@ -317,6 +334,10 @@ fn buildMbedx509( appleLibc(lib, sdk_path); lib.addIncludePath(b.path("../../third_party/mbedtls/include")); lib.addIncludePath(b.path("../../third_party/mbedtls/library")); + + const sim_flags = &[_][]const u8{ "-Wall", "-std=c11", "-U__ARM_NEON", "-UMBEDTLS_AESCE_C" }; + const base_flags = &[_][]const u8{ "-Wall", "-std=c11" }; + lib.addCSourceFiles(.{ .files = &.{ "../../third_party/mbedtls/library/x509.c", @@ -328,7 +349,7 @@ fn buildMbedx509( "../../third_party/mbedtls/library/x509write_crt.c", "../../third_party/mbedtls/library/x509write_csr.c", }, - .flags = &.{ "-Wall", "-std=c11" }, + .flags = if (is_aarch64_sim) sim_flags else base_flags, }); lib.linkLibrary(mbedcrypto); return lib; @@ -342,6 +363,9 @@ fn buildMbedtls( mbedx509: *std.Build.Step.Compile, mbedcrypto: *std.Build.Step.Compile, ) *std.Build.Step.Compile { + const is_aarch64_sim = target.result.cpu.arch == .aarch64 and + target.result.abi == .simulator; + const lib = b.addLibrary(.{ .name = "mbedtls", .root_module = b.createModule(.{ .target = target, .optimize = optimize }), @@ -350,6 +374,10 @@ fn buildMbedtls( appleLibc(lib, sdk_path); lib.addIncludePath(b.path("../../third_party/mbedtls/include")); lib.addIncludePath(b.path("../../third_party/mbedtls/library")); + + const sim_flags = &[_][]const u8{ "-Wall", "-std=c11", "-U__ARM_NEON", "-UMBEDTLS_AESCE_C" }; + const base_flags = &[_][]const u8{ "-Wall", "-std=c11" }; + lib.addCSourceFiles(.{ .files = &.{ "../../third_party/mbedtls/library/debug.c", @@ -369,13 +397,101 @@ fn buildMbedtls( "../../third_party/mbedtls/library/ssl_tls13_keys.c", "../../third_party/mbedtls/library/ssl_tls13_server.c", }, - .flags = &.{ "-Wall", "-std=c11" }, + .flags = if (is_aarch64_sim) sim_flags else base_flags, }); lib.linkLibrary(mbedx509); lib.linkLibrary(mbedcrypto); return lib; } +const MBEDCRYPTO_SOURCES_NO_AESCE = [_][]const u8{ + "../../third_party/mbedtls/library/aes.c", + // aesce.c excluded — requires crypto CPU features incompatible with + // Zig 0.15 aarch64-ios-simulator baseline target + "../../third_party/mbedtls/library/aesni.c", + "../../third_party/mbedtls/library/aria.c", + "../../third_party/mbedtls/library/asn1parse.c", + "../../third_party/mbedtls/library/asn1write.c", + "../../third_party/mbedtls/library/base64.c", + "../../third_party/mbedtls/library/bignum.c", + "../../third_party/mbedtls/library/bignum_core.c", + "../../third_party/mbedtls/library/bignum_mod.c", + "../../third_party/mbedtls/library/bignum_mod_raw.c", + "../../third_party/mbedtls/library/block_cipher.c", + "../../third_party/mbedtls/library/camellia.c", + "../../third_party/mbedtls/library/ccm.c", + "../../third_party/mbedtls/library/chacha20.c", + "../../third_party/mbedtls/library/chachapoly.c", + "../../third_party/mbedtls/library/cipher.c", + "../../third_party/mbedtls/library/cipher_wrap.c", + "../../third_party/mbedtls/library/cmac.c", + "../../third_party/mbedtls/library/constant_time.c", + "../../third_party/mbedtls/library/ctr_drbg.c", + "../../third_party/mbedtls/library/des.c", + "../../third_party/mbedtls/library/dhm.c", + "../../third_party/mbedtls/library/ecdh.c", + "../../third_party/mbedtls/library/ecdsa.c", + "../../third_party/mbedtls/library/ecjpake.c", + "../../third_party/mbedtls/library/ecp.c", + "../../third_party/mbedtls/library/ecp_curves.c", + "../../third_party/mbedtls/library/ecp_curves_new.c", + "../../third_party/mbedtls/library/entropy.c", + "../../third_party/mbedtls/library/entropy_poll.c", + "../../third_party/mbedtls/library/error.c", + "../../third_party/mbedtls/library/gcm.c", + "../../third_party/mbedtls/library/hkdf.c", + "../../third_party/mbedtls/library/hmac_drbg.c", + "../../third_party/mbedtls/library/lmots.c", + "../../third_party/mbedtls/library/lms.c", + "../../third_party/mbedtls/library/md.c", + "../../third_party/mbedtls/library/md5.c", + "../../third_party/mbedtls/library/memory_buffer_alloc.c", + "../../third_party/mbedtls/library/mps_reader.c", + "../../third_party/mbedtls/library/mps_trace.c", + "../../third_party/mbedtls/library/nist_kw.c", + "../../third_party/mbedtls/library/oid.c", + "../../third_party/mbedtls/library/padlock.c", + "../../third_party/mbedtls/library/pem.c", + "../../third_party/mbedtls/library/pk.c", + "../../third_party/mbedtls/library/pk_ecc.c", + "../../third_party/mbedtls/library/pk_wrap.c", + "../../third_party/mbedtls/library/pkcs12.c", + "../../third_party/mbedtls/library/pkcs5.c", + "../../third_party/mbedtls/library/pkcs7.c", + "../../third_party/mbedtls/library/pkparse.c", + "../../third_party/mbedtls/library/pkwrite.c", + "../../third_party/mbedtls/library/platform.c", + "../../third_party/mbedtls/library/platform_util.c", + "../../third_party/mbedtls/library/poly1305.c", + "../../third_party/mbedtls/library/psa_crypto.c", + "../../third_party/mbedtls/library/psa_crypto_aead.c", + "../../third_party/mbedtls/library/psa_crypto_cipher.c", + "../../third_party/mbedtls/library/psa_crypto_client.c", + "../../third_party/mbedtls/library/psa_crypto_driver_wrappers_no_static.c", + "../../third_party/mbedtls/library/psa_crypto_ecp.c", + "../../third_party/mbedtls/library/psa_crypto_ffdh.c", + "../../third_party/mbedtls/library/psa_crypto_hash.c", + "../../third_party/mbedtls/library/psa_crypto_mac.c", + "../../third_party/mbedtls/library/psa_crypto_pake.c", + "../../third_party/mbedtls/library/psa_crypto_rsa.c", + "../../third_party/mbedtls/library/psa_crypto_se.c", + "../../third_party/mbedtls/library/psa_crypto_slot_management.c", + "../../third_party/mbedtls/library/psa_crypto_storage.c", + "../../third_party/mbedtls/library/psa_its_file.c", + "../../third_party/mbedtls/library/psa_util.c", + "../../third_party/mbedtls/library/ripemd160.c", + "../../third_party/mbedtls/library/rsa.c", + "../../third_party/mbedtls/library/rsa_alt_helpers.c", + "../../third_party/mbedtls/library/sha1.c", + "../../third_party/mbedtls/library/sha256.c", + "../../third_party/mbedtls/library/sha3.c", + "../../third_party/mbedtls/library/sha512.c", + "../../third_party/mbedtls/library/threading.c", + "../../third_party/mbedtls/library/timing.c", + "../../third_party/mbedtls/library/version.c", + "../../third_party/mbedtls/library/version_features.c", +}; + const MBEDCRYPTO_SOURCES = [_][]const u8{ "../../third_party/mbedtls/library/aes.c", "../../third_party/mbedtls/library/aesce.c", From be94dde9a2d8789ab71b940da3571f3d25d4d90b Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 01:57:15 +0530 Subject: [PATCH 04/17] added tvos support --- platforms/swift/build.sh | 15 +++++++-------- platforms/swift/build.zig | 10 ++++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/platforms/swift/build.sh b/platforms/swift/build.sh index 7189eb8..ca1efd4 100755 --- a/platforms/swift/build.sh +++ b/platforms/swift/build.sh @@ -37,9 +37,8 @@ SLICES=( "aarch64-ios iphoneos ios-arm64" "aarch64-ios-simulator iphonesimulator ios-arm64-simulator" "x86_64-ios-simulator iphonesimulator ios-x86_64-simulator" - # tvOS disabled — Zig 0.15 stdlib has incomplete tvOS aarch64 DWARF support - # "aarch64-tvos appletvos tvos-arm64" - # "aarch64-tvos-simulator appletvsimulator tvos-arm64-simulator" + "aarch64-tvos appletvos tvos-arm64" + "aarch64-tvos-simulator appletvsimulator tvos-arm64-simulator" ) rm -rf "$XCF_DIR" "$ZIG_OUT" @@ -138,11 +137,11 @@ build_variant "ios-simulator" \ "$ZIG_OUT/ios-arm64-simulator/lib/libcolyseus.a" \ "$ZIG_OUT/ios-x86_64-simulator/lib/libcolyseus.a" -#build_variant "tvos" \ -# "$ZIG_OUT/tvos-arm64/lib/libcolyseus.a" -# -#build_variant "tvos-simulator" \ -# "$ZIG_OUT/tvos-arm64-simulator/lib/libcolyseus.a" +build_variant "tvos" \ + "$ZIG_OUT/tvos-arm64/lib/libcolyseus.a" + +build_variant "tvos-simulator" \ + "$ZIG_OUT/tvos-arm64-simulator/lib/libcolyseus.a" xcodebuild -create-xcframework "${XCFRAMEWORK_ARGS[@]}" -output "$XCF_DIR" diff --git a/platforms/swift/build.zig b/platforms/swift/build.zig index edaab38..359e48b 100644 --- a/platforms/swift/build.zig +++ b/platforms/swift/build.zig @@ -111,6 +111,8 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("../../src/msgpack/msgpack_builder.zig"), .target = target, .optimize = optimize, + .strip = if (is_tvos) true else null, + .unwind_tables = if (is_tvos) .none else null, }); msgpack_builder_mod.addImport("msgpack", msgpack_module); const msgpack_builder = b.addLibrary(.{ @@ -124,6 +126,8 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("../../src/msgpack/msgpack_reader.zig"), .target = target, .optimize = optimize, + .strip = if (is_tvos) true else null, + .unwind_tables = if (is_tvos) .none else null, }); msgpack_reader_mod.addImport("msgpack", msgpack_module); const msgpack_reader = b.addLibrary(.{ @@ -137,6 +141,8 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("../../src/utils/strUtil.zig"), .target = target, .optimize = optimize, + .strip = if (is_tvos) true else null, + .unwind_tables = if (is_tvos) .none else null, }); const strutil = b.addLibrary(.{ .name = "strutil_zig", @@ -149,6 +155,8 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("../../src/network/http.zig"), .target = target, .optimize = optimize, + .strip = if (is_tvos) true else null, + .unwind_tables = if (is_tvos) .none else null, }); http_mod.addIncludePath(b.path("../../include")); http_mod.addIncludePath(b.path("../../third_party/uthash/src")); @@ -163,6 +171,8 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("../../src/certs/system_certs.zig"), .target = target, .optimize = optimize, + .strip = if (is_tvos) true else null, + .unwind_tables = if (is_tvos) .none else null, }); const syscerts = b.addLibrary(.{ .name = "system_certs_zig", From 6a6a21c41a0ec572c12bd13ea0eb374496f55cd7 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 02:02:07 +0530 Subject: [PATCH 05/17] add swift include files --- platforms/swift/include/module.modulemap | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 platforms/swift/include/module.modulemap diff --git a/platforms/swift/include/module.modulemap b/platforms/swift/include/module.modulemap new file mode 100644 index 0000000..4ee0a18 --- /dev/null +++ b/platforms/swift/include/module.modulemap @@ -0,0 +1,4 @@ +module CColyseus { + umbrella header "colyseus_swift.h" + export * +} From 4619e47fb068e6c0aa4f9856850c8d92df5abb94 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 02:04:57 +0530 Subject: [PATCH 06/17] fixing typo in example package swift --- platforms/swift/example/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/swift/example/Package.swift b/platforms/swift/example/Package.swift index 9cc35d8..5841869 100644 --- a/platforms/swift/example/Package.swift +++ b/platforms/swift/example/Package.swift @@ -16,7 +16,7 @@ let package = Package( .executableTarget( name: "ColyseusExample", dependencies: [ - .product(name: "Colyseus", package: "Colyseus"), + .product(name: "Colyseus", package: "colyseus"), ], path: "ColyseusExample" ), From 486fc3ad832bed65fd739a1f3aa987a61658a131 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 02:06:58 +0530 Subject: [PATCH 07/17] resolving package name issue in example --- platforms/swift/example/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/swift/example/Package.swift b/platforms/swift/example/Package.swift index 5841869..6288724 100644 --- a/platforms/swift/example/Package.swift +++ b/platforms/swift/example/Package.swift @@ -16,7 +16,7 @@ let package = Package( .executableTarget( name: "ColyseusExample", dependencies: [ - .product(name: "Colyseus", package: "colyseus"), + .product(name: "Colyseus", package: "swift"), ], path: "ColyseusExample" ), From 852a2745cc09b2b64cb502cc6bbbac0561ac944e Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 02:21:54 +0530 Subject: [PATCH 08/17] fixing the syntax errors in swift files --- platforms/swift/Sources/Colyseus/ColyseusMessage.swift | 4 ++-- platforms/swift/Sources/Colyseus/ColyseusSchema.swift | 2 +- platforms/swift/build.sh | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/platforms/swift/Sources/Colyseus/ColyseusMessage.swift b/platforms/swift/Sources/Colyseus/ColyseusMessage.swift index e3bcb82..57a6e8a 100644 --- a/platforms/swift/Sources/Colyseus/ColyseusMessage.swift +++ b/platforms/swift/Sources/Colyseus/ColyseusMessage.swift @@ -71,7 +71,7 @@ public final class MessageReader { public var stringValue: String? { var len = 0 guard let ptr = colyseus_message_reader_get_str(raw, &len) else { return nil } - return String(bytes: UnsafeBufferPointer(start: ptr, count: len), encoding: .utf8) + return String(bytes: UnsafeRawBufferPointer(start: ptr, count: len), encoding: .utf8) } public var binaryValue: Data? { @@ -96,7 +96,7 @@ public final class MessageReader { var ptr: UnsafePointer? = nil var len = 0 guard colyseus_message_reader_map_get_str(raw, key, &ptr, &len), let p = ptr else { return nil } - return String(bytes: UnsafeBufferPointer(start: p, count: len), encoding: .utf8) + return String(bytes: UnsafeRawBufferPointer(start: p, count: len), encoding: .utf8) } public func mapInt(forKey key: String) -> Int64? { var v: Int64 = 0; return colyseus_message_reader_map_get_int(raw, key, &v) ? v : nil } diff --git a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift index 084bcd0..e217dc6 100644 --- a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift +++ b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift @@ -113,7 +113,7 @@ enum SchemaWalker { private static func primitiveFromPtr(_ ptr: UnsafeMutableRawPointer, _ typeStr: UnsafePointer?) -> Any { let t = typeStr.map { String(cString: $0) } ?? "number" switch t { - case "string": return ptr.assumingMemoryBound(to: CChar.self).map { String(cString: $0) } as Any? ?? "" + case "string": return String(cString: ptr.assumingMemoryBound(to: CChar.self)) case "boolean": return ptr.load(as: Bool.self) case "int8": return Int(ptr.load(as: Int8.self)) case "uint8": return Int(ptr.load(as: UInt8.self)) diff --git a/platforms/swift/build.sh b/platforms/swift/build.sh index ca1efd4..c5f0a2d 100755 --- a/platforms/swift/build.sh +++ b/platforms/swift/build.sh @@ -120,10 +120,9 @@ build_variant() { mkdir -p "$LIB_DIR" local OUT_LIB="$LIB_DIR/libcolyseus.a" lipo_or_copy "$OUT_LIB" "$@" - # xcframework needs headers alongside each library. - local HDR_DIR="$LIB_DIR/Headers" - cp -R "$HEADERS_DST" "$HDR_DIR" - XCFRAMEWORK_ARGS+=("-library" "$OUT_LIB" "-headers" "$HDR_DIR") + # xcodebuild -create-xcframework expects the PARENT directory containing + # the headers — it will create the Headers/ subdirectory itself. + XCFRAMEWORK_ARGS+=("-library" "$OUT_LIB" "-headers" "$HEADERS_DST") } build_variant "macos" \ From d8acca733f6167283f78c6b7cb9764353905f753 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 02:27:22 +0530 Subject: [PATCH 09/17] fix the schema swift errors --- .../Sources/Colyseus/ColyseusSchema.swift | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift index e217dc6..f843c70 100644 --- a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift +++ b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift @@ -72,41 +72,47 @@ enum SchemaWalker { private static func decodeArray(_ arr: UnsafeMutablePointer?) -> [Any] { guard let arr = arr else { return [] } - var result = [Any]() - colyseus_array_schema_foreach( - arr, - { _, value, userdata in - guard let value = value, let userdata = userdata else { return } - let list = userdata.assumingMemoryBound(to: [Any].self) - if arr.pointee.has_schema_child { - list.pointee.append(SchemaWalker.walk(value)) - } else { - list.pointee.append(SchemaWalker.primitiveFromPtr(value, arr.pointee.child_primitive_type)) - } - }, - &result - ) - return result + typealias Ctx = (list: [Any], hasSchema: Bool, primitiveType: UnsafePointer?) + var ctx: Ctx = (list: [], hasSchema: arr.pointee.has_schema_child, primitiveType: arr.pointee.child_primitive_type) + withUnsafeMutablePointer(to: &ctx) { ctxPtr in + colyseus_array_schema_foreach( + arr, + { _, value, userdata in + guard let value = value, let userdata = userdata else { return } + let ctx = userdata.assumingMemoryBound(to: Ctx.self) + if ctx.pointee.hasSchema { + ctx.pointee.list.append(SchemaWalker.walk(value)) + } else { + ctx.pointee.list.append(SchemaWalker.primitiveFromPtr(value, ctx.pointee.primitiveType)) + } + }, + ctxPtr + ) + } + return ctx.list } private static func decodeMap(_ map: UnsafeMutablePointer?) -> SchemaState { guard let map = map else { return [:] } - var result = SchemaState() - colyseus_map_schema_foreach( - map, - { key, value, userdata in - guard let key = key, let value = value, let userdata = userdata else { return } - let dict = userdata.assumingMemoryBound(to: SchemaState.self) - let k = String(cString: key) - if map.pointee.has_schema_child { - dict.pointee[k] = SchemaWalker.walk(value) - } else { - dict.pointee[k] = SchemaWalker.primitiveFromPtr(value, map.pointee.child_primitive_type) - } - }, - &result - ) - return result + typealias Ctx = (dict: SchemaState, hasSchema: Bool, primitiveType: UnsafePointer?) + var ctx: Ctx = (dict: [:], hasSchema: map.pointee.has_schema_child, primitiveType: map.pointee.child_primitive_type) + withUnsafeMutablePointer(to: &ctx) { ctxPtr in + colyseus_map_schema_foreach( + map, + { key, value, userdata in + guard let key = key, let value = value, let userdata = userdata else { return } + let ctx = userdata.assumingMemoryBound(to: Ctx.self) + let k = String(cString: key) + if ctx.pointee.hasSchema { + ctx.pointee.dict[k] = SchemaWalker.walk(value) + } else { + ctx.pointee.dict[k] = SchemaWalker.primitiveFromPtr(value, ctx.pointee.primitiveType) + } + }, + ctxPtr + ) + } + return ctx.dict } // Decode a raw void* primitive value using the type string stored in the collection. From 5dc9863076afe5e4dc654da5147bb5cd3d38d697 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Sat, 28 Feb 2026 02:52:06 +0530 Subject: [PATCH 10/17] fix xcframework errros and clear zig cache in ci --- .github/workflows/swift.yml | 7 ++-- platforms/swift/Package.swift | 2 +- platforms/swift/build.sh | 45 +++++++++++++++++++----- platforms/swift/build.zig | 9 +++++ platforms/swift/example/Package.swift | 2 +- platforms/swift/include/module.modulemap | 1 + 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 444a5e1..139906f 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -16,6 +16,10 @@ jobs: with: submodules: recursive + - name: Cleanup Zig cache + run: | + rm -rf ~/.cache/zig + - name: Install Zig uses: mlugg/setup-zig@v2 with: @@ -41,8 +45,7 @@ jobs: - name: Build example (macOS) working-directory: platforms/swift/example - run: | - swift build -c release 2>&1 | tail -5 + run: swift build -c release - name: Upload xcframework artifact uses: actions/upload-artifact@v4 diff --git a/platforms/swift/Package.swift b/platforms/swift/Package.swift index 4c108fd..ceae522 100644 --- a/platforms/swift/Package.swift +++ b/platforms/swift/Package.swift @@ -22,7 +22,7 @@ import PackageDescription let package = Package( name: "Colyseus", platforms: [ - .macOS(.v12), + .macOS(.v13), .iOS(.v15), .tvOS(.v15), ], diff --git a/platforms/swift/build.sh b/platforms/swift/build.sh index c5f0a2d..c5faa28 100755 --- a/platforms/swift/build.sh +++ b/platforms/swift/build.sh @@ -93,6 +93,14 @@ echo "=== Assembling xcframework ===" # tvOS device: single arm64 # tvOS simulator: single arm64-sim +# Merge all static libs in a slice's lib/ dir into one archive, then lipo. +merge_slice_libs() { + local SLICE_DIR="$1" + local OUT_LIB="$2" + local ALL_LIBS=("$SLICE_DIR"/lib/*.a) + libtool -static -o "$OUT_LIB" "${ALL_LIBS[@]}" +} + lipo_or_copy() { local OUT_LIB="$1"; shift local INPUTS=("$@") @@ -116,34 +124,55 @@ cp "$SCRIPT_DIR/include/module.modulemap" "$HEADERS_DST/" build_variant() { local VARIANT_NAME="$1"; shift + local SLICE_DIRS=("$@") local LIB_DIR="$BUILD_DIR/libs/$VARIANT_NAME" mkdir -p "$LIB_DIR" local OUT_LIB="$LIB_DIR/libcolyseus.a" - lipo_or_copy "$OUT_LIB" "$@" + + # Merge all sub-libs per slice into one archive, then lipo across arches. + local MERGED_LIBS=() + local idx=0 + for SLICE_DIR in "${SLICE_DIRS[@]}"; do + local MERGED="$LIB_DIR/merged_${idx}.a" + merge_slice_libs "$SLICE_DIR" "$MERGED" + MERGED_LIBS+=("$MERGED") + idx=$((idx + 1)) + done + + lipo_or_copy "$OUT_LIB" "${MERGED_LIBS[@]}" + rm -f "$LIB_DIR"/merged_*.a + # xcodebuild -create-xcframework expects the PARENT directory containing # the headers — it will create the Headers/ subdirectory itself. XCFRAMEWORK_ARGS+=("-library" "$OUT_LIB" "-headers" "$HEADERS_DST") } build_variant "macos" \ - "$ZIG_OUT/macos-arm64/lib/libcolyseus.a" \ - "$ZIG_OUT/macos-x86_64/lib/libcolyseus.a" + "$ZIG_OUT/macos-arm64" \ + "$ZIG_OUT/macos-x86_64" build_variant "ios" \ - "$ZIG_OUT/ios-arm64/lib/libcolyseus.a" + "$ZIG_OUT/ios-arm64" build_variant "ios-simulator" \ - "$ZIG_OUT/ios-arm64-simulator/lib/libcolyseus.a" \ - "$ZIG_OUT/ios-x86_64-simulator/lib/libcolyseus.a" + "$ZIG_OUT/ios-arm64-simulator" \ + "$ZIG_OUT/ios-x86_64-simulator" build_variant "tvos" \ - "$ZIG_OUT/tvos-arm64/lib/libcolyseus.a" + "$ZIG_OUT/tvos-arm64" build_variant "tvos-simulator" \ - "$ZIG_OUT/tvos-arm64-simulator/lib/libcolyseus.a" + "$ZIG_OUT/tvos-arm64-simulator" xcodebuild -create-xcframework "${XCFRAMEWORK_ARGS[@]}" -output "$XCF_DIR" +# xcodebuild copies the entire -headers directory as-is, which means any +# sub-directory named "Headers" inside HEADERS_DST gets nested as +# xcframework/.../Headers/Headers/. Remove those duplicates. +for slice in "$XCF_DIR"/*/; do + rm -rf "${slice}Headers/Headers" +done + echo "" echo "=== xcframework built: $XCF_DIR ===" diff --git a/platforms/swift/build.zig b/platforms/swift/build.zig index 359e48b..c9f0c94 100644 --- a/platforms/swift/build.zig +++ b/platforms/swift/build.zig @@ -252,6 +252,15 @@ pub fn build(b: *std.Build) void { } b.installArtifact(colyseus); + b.installArtifact(mbedtls); + b.installArtifact(mbedx509); + b.installArtifact(mbedcrypto); + b.installArtifact(wslay); + b.installArtifact(http_zig); + b.installArtifact(syscerts); + b.installArtifact(strutil); + b.installArtifact(msgpack_builder); + b.installArtifact(msgpack_reader); // Install colyseus public headers so build.sh can copy them into the xcframework. const header_pairs = [_][2][]const u8{ diff --git a/platforms/swift/example/Package.swift b/platforms/swift/example/Package.swift index 6288724..6a7a085 100644 --- a/platforms/swift/example/Package.swift +++ b/platforms/swift/example/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "ColyseusExample", platforms: [ - .macOS(.v12), + .macOS(.v13), .iOS(.v15), ], dependencies: [ diff --git a/platforms/swift/include/module.modulemap b/platforms/swift/include/module.modulemap index 4ee0a18..7a3b8d7 100644 --- a/platforms/swift/include/module.modulemap +++ b/platforms/swift/include/module.modulemap @@ -1,4 +1,5 @@ module CColyseus { umbrella header "colyseus_swift.h" export * + module * { export * } } From cde333120cd1d5897cff2c25c1fa7617c4547dd3 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Tue, 10 Mar 2026 15:09:18 +0530 Subject: [PATCH 11/17] testing instructions added --- platforms/swift/example/Example.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 platforms/swift/example/Example.md diff --git a/platforms/swift/example/Example.md b/platforms/swift/example/Example.md new file mode 100644 index 0000000..e215c36 --- /dev/null +++ b/platforms/swift/example/Example.md @@ -0,0 +1,9 @@ +# Testing Instructions +(W.I.P) +- Make sure example server is running + - Move to {project-root}/example-server + - npm install + - npm start +- Move to {project-root}/platforms/swift/example +- swift build -c release +- swift run \ No newline at end of file From d8f224be39d3fe2efc0f11a4a4ba5c76a9cde19d Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Tue, 10 Mar 2026 15:10:06 +0530 Subject: [PATCH 12/17] artifcat retention removed --- .github/workflows/swift.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 139906f..a5a381e 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -52,4 +52,3 @@ jobs: with: name: Colyseus.xcframework path: platforms/swift/build/Colyseus.xcframework.zip - retention-days: 30 From 429463a47a86c384cc1c265691e272867e687191 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Tue, 10 Mar 2026 15:11:19 +0530 Subject: [PATCH 13/17] dispatch workflow only when changes are in swift src or its manual --- .github/workflows/godot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/godot.yml b/.github/workflows/godot.yml index 76e42ad..18ab54d 100644 --- a/.github/workflows/godot.yml +++ b/.github/workflows/godot.yml @@ -3,14 +3,14 @@ name: Godot Extension Build on: push: paths: - - 'platforms/godot/**' + - 'platforms/swift/**' branches: - main pull_request: paths: - - 'platforms/godot/**' + - 'platforms/swift/**' workflow_dispatch: - + workflow_call: jobs: # Run test suite first (includes integration tests with example-server) test: From f8c5eebec1a8f1ddf60c8b424f30e05c6bd24996 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Wed, 1 Apr 2026 21:56:25 +0530 Subject: [PATCH 14/17] workflow fix for godot and swift --- .github/workflows/godot.yml | 10 ++++++++-- .github/workflows/swift.yml | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/godot.yml b/.github/workflows/godot.yml index 18ab54d..11deef7 100644 --- a/.github/workflows/godot.yml +++ b/.github/workflows/godot.yml @@ -3,12 +3,18 @@ name: Godot Extension Build on: push: paths: - - 'platforms/swift/**' + - 'platforms/godot/**' + - 'src/**' + - 'include/**' + - 'third_party/**' branches: - main pull_request: paths: - - 'platforms/swift/**' + - 'platforms/godot/**' + - 'src/**' + - 'include/**' + - 'third_party/**' workflow_dispatch: workflow_call: jobs: diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index a5a381e..a5590df 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -2,7 +2,17 @@ name: Swift Platform on: push: + paths: + - 'platforms/swift/**' + - 'src/**' + - 'include/**' + - 'third_party/**' pull_request: + paths: + - 'platforms/swift/**' + - 'src/**' + - 'include/**' + - 'third_party/**' workflow_call: jobs: From 2e60658b1ecb0f9da90fc133ac6360be2c716aa2 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Wed, 1 Apr 2026 22:21:08 +0530 Subject: [PATCH 15/17] fix Swift CColyseus message builder imports --- platforms/swift/Sources/Colyseus/ColyseusMessage.swift | 4 ++-- platforms/swift/include/colyseus_swift.h | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/platforms/swift/Sources/Colyseus/ColyseusMessage.swift b/platforms/swift/Sources/Colyseus/ColyseusMessage.swift index 57a6e8a..93cfe1e 100644 --- a/platforms/swift/Sources/Colyseus/ColyseusMessage.swift +++ b/platforms/swift/Sources/Colyseus/ColyseusMessage.swift @@ -152,7 +152,7 @@ public final class MessageBuilder { @discardableResult public func set(_ k: String, _ v: Double) -> MessageBuilder { colyseus_message_map_put_float(raw, k, v); return self } @discardableResult public func set(_ k: String, _ v: Bool) -> MessageBuilder { colyseus_message_map_put_bool(raw, k, v); return self } @discardableResult public func setNil(_ k: String) -> MessageBuilder { colyseus_message_map_put_nil(raw, k); return self } - @discardableResult public func set(_ k: String, _ v: MessageBuilder) -> MessageBuilder { colyseus_message_map_put(raw, k, v.raw); return self } + @discardableResult public func set(_ k: String, _ v: MessageBuilder) -> MessageBuilder { colyseus_message_map_put_msg(raw, k, v.raw); return self } @discardableResult public func push(_ v: String) -> MessageBuilder { colyseus_message_array_push_str(raw, v); return self } @discardableResult public func push(_ v: Int64) -> MessageBuilder { colyseus_message_array_push_int(raw, v); return self } @@ -160,7 +160,7 @@ public final class MessageBuilder { @discardableResult public func push(_ v: Double) -> MessageBuilder { colyseus_message_array_push_float(raw, v); return self } @discardableResult public func push(_ v: Bool) -> MessageBuilder { colyseus_message_array_push_bool(raw, v); return self } @discardableResult public func pushNil() -> MessageBuilder { colyseus_message_array_push_nil(raw); return self } - @discardableResult public func push(_ v: MessageBuilder) -> MessageBuilder { colyseus_message_array_push(raw, v.raw); return self } + @discardableResult public func push(_ v: MessageBuilder) -> MessageBuilder { colyseus_message_array_push_msg(raw, v.raw); return self } public func encode() -> Data { var length = 0 diff --git a/platforms/swift/include/colyseus_swift.h b/platforms/swift/include/colyseus_swift.h index 1a791ab..51c49f1 100644 --- a/platforms/swift/include/colyseus_swift.h +++ b/platforms/swift/include/colyseus_swift.h @@ -17,5 +17,6 @@ #include #include #include +#include #endif /* COLYSEUS_SWIFT_H */ From f58cea7f550edef55782eb47622189bc95f01ba3 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Wed, 1 Apr 2026 23:54:53 +0530 Subject: [PATCH 16/17] added Schema tests for swift --- .github/workflows/swift.yml | 4 + platforms/swift/Package.swift | 6 + .../Sources/Colyseus/ColyseusSchema.swift | 1 - .../ColyseusTests/SchemaWalkerTests.swift | 171 ++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 platforms/swift/Tests/ColyseusTests/SchemaWalkerTests.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index a5590df..fc92848 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -57,6 +57,10 @@ jobs: working-directory: platforms/swift/example run: swift build -c release + - name: Run Swift package tests + working-directory: platforms/swift + run: swift test + - name: Upload xcframework artifact uses: actions/upload-artifact@v4 with: diff --git a/platforms/swift/Package.swift b/platforms/swift/Package.swift index ceae522..51be6aa 100644 --- a/platforms/swift/Package.swift +++ b/platforms/swift/Package.swift @@ -48,5 +48,11 @@ let package = Package( name: "CColyseus", path: "build/Colyseus.xcframework" ), + + .testTarget( + name: "ColyseusTests", + dependencies: ["Colyseus"], + path: "Tests/ColyseusTests" + ), ] ) diff --git a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift index f843c70..017276b 100644 --- a/platforms/swift/Sources/Colyseus/ColyseusSchema.swift +++ b/platforms/swift/Sources/Colyseus/ColyseusSchema.swift @@ -15,7 +15,6 @@ enum SchemaWalker { /// Walk a colyseus_dynamic_schema_t* and return a SchemaState. static func walk(_ ptr: UnsafeMutableRawPointer) -> SchemaState { let schema = ptr.assumingMemoryBound(to: colyseus_dynamic_schema_t.self) - var result = SchemaState() // Iterate over all fields stored in the hash table. typealias Ctx = (result: SchemaState, schema: UnsafeMutablePointer) diff --git a/platforms/swift/Tests/ColyseusTests/SchemaWalkerTests.swift b/platforms/swift/Tests/ColyseusTests/SchemaWalkerTests.swift new file mode 100644 index 0000000..c639c53 --- /dev/null +++ b/platforms/swift/Tests/ColyseusTests/SchemaWalkerTests.swift @@ -0,0 +1,171 @@ +import XCTest +@testable import Colyseus +import CColyseus + +/// Exercises `SchemaWalker` against the native dynamic-schema C API. +/// This validates Swift↔C struct layout and conversion — not full parity with +/// the JavaScript `@colyseus/schema` encoder/decoder (that would need fixture bytes + room decode tests). +final class SchemaWalkerTests: XCTestCase { + + // MARK: - Helpers + + private func addDynamicField( + _ vtable: UnsafeMutablePointer, + index: Int32, + name: String, + type: colyseus_field_type_t, + typeStr: String + ) { + name.withCString { nameC in + typeStr.withCString { tsC in + guard let field = colyseus_dynamic_field_create(index, nameC, type, tsC) else { + XCTFail("colyseus_dynamic_field_create failed") + return + } + colyseus_dynamic_vtable_add_field(vtable, field) + } + } + } + + private func setValue( + _ schema: UnsafeMutablePointer, + index: Int32, + name: String, + kind: colyseus_field_type_t, + _ configure: (UnsafeMutablePointer) -> Void + ) { + guard let value = colyseus_dynamic_value_create(kind) else { + XCTFail("colyseus_dynamic_value_create failed") + return + } + configure(value) + name.withCString { nameC in + colyseus_dynamic_schema_set(schema, index, nameC, value) + } + } + + // MARK: - Tests + + func testWalkFlatPrimitives() { + guard let vtable = colyseus_dynamic_vtable_create("TestFlat") else { + return XCTFail("vtable create") + } + defer { colyseus_dynamic_vtable_free(vtable) } + + addDynamicField(vtable, index: 0, name: "title", type: COLYSEUS_FIELD_STRING, typeStr: "string") + addDynamicField(vtable, index: 1, name: "score", type: COLYSEUS_FIELD_NUMBER, typeStr: "number") + addDynamicField(vtable, index: 2, name: "alive", type: COLYSEUS_FIELD_BOOLEAN, typeStr: "boolean") + addDynamicField(vtable, index: 3, name: "ticks", type: COLYSEUS_FIELD_INT64, typeStr: "int64") + addDynamicField(vtable, index: 4, name: "token", type: COLYSEUS_FIELD_UINT64, typeStr: "uint64") + + guard let schema = colyseus_dynamic_schema_create(vtable) else { + return XCTFail("schema create") + } + defer { colyseus_dynamic_schema_free(schema) } + + setValue(schema, index: 0, name: "title", kind: COLYSEUS_FIELD_STRING) { v in + "hello".withCString { colyseus_dynamic_value_set_string(v, $0) } + } + setValue(schema, index: 1, name: "score", kind: COLYSEUS_FIELD_NUMBER) { v in + colyseus_dynamic_value_set_number(v, 42.5) + } + setValue(schema, index: 2, name: "alive", kind: COLYSEUS_FIELD_BOOLEAN) { v in + colyseus_dynamic_value_set_boolean(v, true) + } + setValue(schema, index: 3, name: "ticks", kind: COLYSEUS_FIELD_INT64) { v in + colyseus_dynamic_value_set_int64(v, -100) + } + setValue(schema, index: 4, name: "token", kind: COLYSEUS_FIELD_UINT64) { v in + colyseus_dynamic_value_set_uint64(v, 9_000_000_000_000_000_001) + } + + let state = SchemaWalker.walk(UnsafeMutableRawPointer(schema)) + + XCTAssertEqual(state.count, 5) + XCTAssertEqual(state["title"] as? String, "hello") + guard let score = state["score"] as? Double else { return XCTFail("score type") } + XCTAssertEqual(score, 42.5, accuracy: 1e-9) + XCTAssertEqual(state["alive"] as? Bool, true) + XCTAssertEqual(state["ticks"] as? Int64, -100) + XCTAssertEqual(state["token"] as? UInt64, 9_000_000_000_000_000_001) + } + + func testWalkFloat32AndFloat64Kinds() { + guard let vtable = colyseus_dynamic_vtable_create("TestFloats") else { + return XCTFail("vtable create") + } + defer { colyseus_dynamic_vtable_free(vtable) } + + addDynamicField(vtable, index: 0, name: "f32", type: COLYSEUS_FIELD_FLOAT32, typeStr: "float32") + addDynamicField(vtable, index: 1, name: "f64", type: COLYSEUS_FIELD_FLOAT64, typeStr: "float64") + + guard let schema = colyseus_dynamic_schema_create(vtable) else { + return XCTFail("schema create") + } + defer { colyseus_dynamic_schema_free(schema) } + + setValue(schema, index: 0, name: "f32", kind: COLYSEUS_FIELD_FLOAT32) { v in + colyseus_dynamic_value_set_float32(v, 1.25) + } + setValue(schema, index: 1, name: "f64", kind: COLYSEUS_FIELD_NUMBER) { v in + colyseus_dynamic_value_set_number(v, 3.5) + v.pointee.type = COLYSEUS_FIELD_FLOAT64 + } + + let state = SchemaWalker.walk(UnsafeMutableRawPointer(schema)) + + guard let f32 = state["f32"] as? Double else { return XCTFail("f32 type") } + guard let f64 = state["f64"] as? Double else { return XCTFail("f64 type") } + XCTAssertEqual(f32, 1.25, accuracy: 1e-6) + XCTAssertEqual(f64, 3.5, accuracy: 1e-9) + } + + func testWalkNestedRef() { + guard let childVt = colyseus_dynamic_vtable_create("Child") else { + return XCTFail("child vtable") + } + defer { colyseus_dynamic_vtable_free(childVt) } + addDynamicField(childVt, index: 0, name: "n", type: COLYSEUS_FIELD_STRING, typeStr: "string") + + guard let parentVt = colyseus_dynamic_vtable_create("Parent") else { + return XCTFail("parent vtable") + } + defer { colyseus_dynamic_vtable_free(parentVt) } + addDynamicField(parentVt, index: 0, name: "label", type: COLYSEUS_FIELD_STRING, typeStr: "string") + addDynamicField(parentVt, index: 1, name: "child", type: COLYSEUS_FIELD_REF, typeStr: "ref") + colyseus_dynamic_vtable_set_child(parentVt, 1, childVt) + + guard let childSchema = colyseus_dynamic_schema_create(childVt) else { + return XCTFail("child schema") + } + setValue(childSchema, index: 0, name: "n", kind: COLYSEUS_FIELD_STRING) { v in + "inner".withCString { colyseus_dynamic_value_set_string(v, $0) } + } + + guard let parentSchema = colyseus_dynamic_schema_create(parentVt) else { + colyseus_dynamic_schema_free(childSchema) + return XCTFail("parent schema") + } + + setValue(parentSchema, index: 0, name: "label", kind: COLYSEUS_FIELD_STRING) { v in + "root".withCString { colyseus_dynamic_value_set_string(v, $0) } + } + guard let refVal = colyseus_dynamic_value_create(COLYSEUS_FIELD_REF) else { + colyseus_dynamic_schema_free(parentSchema) + colyseus_dynamic_schema_free(childSchema) + return XCTFail("ref value") + } + colyseus_dynamic_value_set_ref(refVal, childSchema) + "child".withCString { colyseus_dynamic_schema_set(parentSchema, 1, $0, refVal) } + + let state = SchemaWalker.walk(UnsafeMutableRawPointer(parentSchema)) + + colyseus_dynamic_schema_free(parentSchema) + colyseus_dynamic_schema_free(childSchema) + + XCTAssertEqual(state["label"] as? String, "root") + let nested = state["child"] as? SchemaState + XCTAssertNotNil(nested) + XCTAssertEqual(nested?["n"] as? String, "inner") + } +} From fe974a8e19d6f5c027e6280dc2c3c05a375f6ea2 Mon Sep 17 00:00:00 2001 From: Bharat Sharma Date: Thu, 2 Apr 2026 01:18:20 +0530 Subject: [PATCH 17/17] fix(msgpack): correct encode ownership and room send buffer lifetime --- src/msgpack/msgpack_builder.zig | 65 ++++++++++++++++++++++++--------- src/room.c | 2 + tests/README.md | 47 ++++++++++-------------- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/msgpack/msgpack_builder.zig b/src/msgpack/msgpack_builder.zig index c9454a9..7872b33 100644 --- a/src/msgpack/msgpack_builder.zig +++ b/src/msgpack/msgpack_builder.zig @@ -22,7 +22,6 @@ const PayloadWrapper = struct { payload_type: PayloadType, payload: ?Payload = null, array_elements: ?std.ArrayList(Payload) = null, - encoded_data: ?[]u8 = null, fn deinit(self: *PayloadWrapper) void { if (self.array_elements) |*list| { @@ -34,9 +33,6 @@ const PayloadWrapper = struct { if (self.payload) |*p| { p.free(allocator); } - if (self.encoded_data) |data| { - allocator.free(data); - } } }; @@ -46,7 +42,6 @@ fn createMapWrapper() ?*PayloadWrapper { .payload_type = .map, .payload = Payload.mapPayload(allocator), .array_elements = null, - .encoded_data = null, }; return wrapper; } @@ -57,7 +52,6 @@ fn createArrayWrapper() ?*PayloadWrapper { .payload_type = .array, .payload = null, .array_elements = .{}, - .encoded_data = null, }; return wrapper; } @@ -68,7 +62,6 @@ fn createPrimitiveWrapper(payload: Payload) ?*PayloadWrapper { .payload_type = .primitive, .payload = payload, .array_elements = null, - .encoded_data = null, }; return wrapper; } @@ -221,18 +214,58 @@ export fn colyseus_message_array_push_msg(arr: ?*PayloadWrapper, value: ?*Payloa } // ============================================================================ -// Helper to get Payload for encoding +// Encode snapshot: deep clone so zig-msgpack encode cannot alias builder heap. // ============================================================================ +fn clonePayloadForEncode(p: Payload) (Payload.Error || error{ OutOfMemory, UnsupportedNonStringMapKey })!Payload { + return switch (p) { + .nil, .bool, .int, .uint, .float, .timestamp => p, + .str => |s| try Payload.strToPayload(s.str, allocator), + .bin => |b| try Payload.binToPayload(b.bin, allocator), + .ext => |e| try Payload.extToPayload(e.type, e.data, allocator), + .arr => |items| { + var arr_payload = try Payload.arrPayload(items.len, allocator); + errdefer arr_payload.free(allocator); + for (items, 0..) |item, i| { + const cloned = try clonePayloadForEncode(item); + try arr_payload.setArrElement(i, cloned); + } + return arr_payload; + }, + .map => |m| { + var new_payload = Payload.mapPayload(allocator); + errdefer new_payload.free(allocator); + var it = m.iterator(); + while (it.next()) |entry| { + const key = entry.key_ptr.*; + switch (key) { + .str => |ks| { + const val = try clonePayloadForEncode(entry.value_ptr.*); + try new_payload.mapPut(ks.str, val); + }, + else => return error.UnsupportedNonStringMapKey, + } + } + return new_payload; + }, + }; +} + fn getPayloadForEncoding(wrapper: *PayloadWrapper) ?Payload { switch (wrapper.payload_type) { - .map, .primitive => return wrapper.payload, + .map, .primitive => { + const p = wrapper.payload orelse return null; + wrapper.payload = null; + return p; + }, .array => { if (wrapper.array_elements) |list| { var arr_payload = Payload.arrPayload(list.items.len, allocator) catch return null; + errdefer arr_payload.free(allocator); for (list.items, 0..) |item, i| { - arr_payload.setArrElement(i, item) catch { - arr_payload.free(allocator); + const cloned = clonePayloadForEncode(item) catch return null; + arr_payload.setArrElement(i, cloned) catch { + cloned.free(allocator); return null; }; } @@ -253,17 +286,13 @@ export fn colyseus_message_encode(wrapper: ?*PayloadWrapper, out_len: *usize) ?[ return null; } - // Free previous encoded data if exists - if (wrapper.?.encoded_data) |data| { - allocator.free(data); - wrapper.?.encoded_data = null; - } - // Get the payload to encode const payload_to_encode = getPayloadForEncoding(wrapper.?) orelse { out_len.* = 0; return null; }; + // zig-msgpack: Pack.write does not consume the payload; caller must free after write. + defer payload_to_encode.free(allocator); // Create buffer for encoding var buffer: [16384]u8 = undefined; @@ -294,7 +323,7 @@ export fn colyseus_message_encode(wrapper: ?*PayloadWrapper, out_len: *usize) ?[ }; @memcpy(result, buffer[0..encoded_len]); - wrapper.?.encoded_data = result; + // Caller owns the returned buffer; free with colyseus_message_encoded_free. out_len.* = encoded_len; return result.ptr; } diff --git a/src/room.c b/src/room.c index e46c598..eca59ee 100644 --- a/src/room.c +++ b/src/room.c @@ -398,6 +398,7 @@ void colyseus_room_send(colyseus_room_t* room, const char* type, colyseus_messag if (encoded_data && encoded_len > 0) { colyseus_room_send_encoded(room, type, encoded_data, encoded_len); + colyseus_message_encoded_free(encoded_data, encoded_len); } } @@ -409,6 +410,7 @@ void colyseus_room_send_int(colyseus_room_t* room, int type, colyseus_message_t* if (encoded_data && encoded_len > 0) { colyseus_room_send_int_encoded(room, type, encoded_data, encoded_len); + colyseus_message_encoded_free(encoded_data, encoded_len); } } diff --git a/tests/README.md b/tests/README.md index 5439f3e..8726eef 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,23 +6,27 @@ All tests are written in Zig, taking advantage of Zig's built-in testing framewo ## Running Tests -### Run All Tests +### Full suite (`zig build test`) + +Integration tests need **example-server** on `http://localhost:2567` (same as CI). Terminal A: ```bash -zig build test +cd example-server +npm install +npx tsx src/index.ts +# or: npm start (tsx watch) ``` -This runs: -- **22 Zig tests** across 6 test suites -- Most tests run without requiring a server -- Integration test requires a running Colyseus server +Terminal B, from the **repository root**: -### Run Tests Without Integration Test +```bash +zig build test +``` -If you don't have a Colyseus server running: +### Without a server (unit tests only) ```bash -zig build test -Dskip-integration +zig build test -Dskip-integration=true ``` ### Run Individual Tests @@ -91,20 +95,12 @@ All tests are written in **idiomatic Zig** using the built-in test framework: - No running server needed - Run in milliseconds -### Integration Test -To run the full integration test (`test_integration.zig`), you need: +### Integration tests -1. A running Colyseus server on `localhost:2567` -2. A room named `my_room` available +`zig build test` runs `test_integration`, `test_schema_callbacks`, and `test_messages` against **example-server** (`localhost:2567`, room `my_room`). Start the server as in **Full suite** above. -Start the example server: -```bash -cd example-server -npm install -npm start -``` +Single integration binary: -Then run: ```bash zig build test_integration ``` @@ -137,10 +133,10 @@ test "my new test" { ## Test Output -When running `zig build test -Dskip-integration`: +Example success output: ``` -Build Summary: 17/17 steps succeeded; 22/22 tests passed +Build Summary: … steps succeeded; … tests passed test success ``` @@ -158,10 +154,5 @@ All HTTP tests passed! ## Continuous Integration -For CI/CD pipelines, use: -```bash -zig build test -Dskip-integration -``` - -This ensures tests run without requiring a live server. +CI starts **example-server**, waits until `localhost:2567` responds, then runs `zig build test`.