From b3a49bab8d25b97eb28d13ee0a60056d33e0879e Mon Sep 17 00:00:00 2001 From: tomsci Date: Fri, 8 Nov 2024 21:13:39 +0000 Subject: [PATCH] Add linux support (#6) With lots of workarounds for the fact that AnyHashable is broken in the linux Swift port. --- .github/workflows/swift.yml | 30 +++++++ Package.swift | 2 + Sources/Lua/LuaFoundation.swift | 1 + Sources/Lua/LuaState.swift | 46 +++++++++++ Sources/Lua/LuaTableRef.swift | 142 +++++++++++++++++++++++++++++--- Tests/lua-test/LuaTests.swift | 55 ++++++++++++- 6 files changed, 262 insertions(+), 14 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 119548b..527152b 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -32,6 +32,36 @@ jobs: - name: Upload Pages artifact uses: actions/upload-pages-artifact@v2 + build-linux: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + - uses: swift-actions/setup-swift@v2 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v + + build-oss: + + runs-on: macos-13 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + - uses: swift-actions/setup-swift@v2 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v + deploy: needs: build if: ${{ github.ref == 'refs/heads/main' }} diff --git a/Package.swift b/Package.swift index ed10417..b022042 100644 --- a/Package.swift +++ b/Package.swift @@ -34,6 +34,7 @@ let package = Package( resources: [], swiftSettings: [ // .define("LUASWIFT_NO_FOUNDATION") + .define("LUASWIFT_ANYHASHABLE_BROKEN", .when(platforms: [.linux])) ] ), .target( @@ -82,6 +83,7 @@ let package = Package( ], swiftSettings: [ // .define("LUASWIFT_NO_FOUNDATION") + .define("LUASWIFT_ANYHASHABLE_BROKEN", .when(platforms: [.linux])) ], plugins: [ .plugin(name: "EmbedLuaPlugin") diff --git a/Sources/Lua/LuaFoundation.swift b/Sources/Lua/LuaFoundation.swift index 389ba49..e1b85d2 100644 --- a/Sources/Lua/LuaFoundation.swift +++ b/Sources/Lua/LuaFoundation.swift @@ -4,6 +4,7 @@ #if !LUASWIFT_NO_FOUNDATION // canImport(Foundation) import Foundation +import CoreFoundation import CLua /// Represents all the String encodings that this framework can convert strings to and from. diff --git a/Sources/Lua/LuaState.swift b/Sources/Lua/LuaState.swift index f59b1a7..89d23c4 100644 --- a/Sources/Lua/LuaState.swift +++ b/Sources/Lua/LuaState.swift @@ -935,9 +935,13 @@ extension UnsafeMutablePointer where Pointee == lua_State { return ptr case .number: if let intVal = tointeger(index) { +#if LUASWIFT_ANYHASHABLE_BROKEN + return intVal +#else // Integers are returned type-erased (thanks to AnyHashable) meaning fewer cast restrictions in // eg tovalue() return AnyHashable(intVal) +#endif } else { return tonumber(index) } @@ -1117,6 +1121,48 @@ extension UnsafeMutablePointer where Pointee == lua_State { } } +#if LUASWIFT_ANYHASHABLE_BROKEN + // Then the directCast clause above won't have worked, and we need to try every integer type + if t == .number, let intVal = value as? lua_Integer { + if let intSubType = Int(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = Int8(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = Int16(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = Int32(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = Int64(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = UInt(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = UInt8(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = UInt16(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = UInt32(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let intSubType = UInt64(exactly: intVal), let ret = intSubType as? T { + return ret + } + if let dbl = Double(exactly: intVal), let ret = dbl as? T { + return ret + } + if let flt = Float(exactly: intVal), let ret = flt as? T { + return ret + } + } +#endif + return nil } diff --git a/Sources/Lua/LuaTableRef.swift b/Sources/Lua/LuaTableRef.swift index 0c6e5f1..67553a1 100644 --- a/Sources/Lua/LuaTableRef.swift +++ b/Sources/Lua/LuaTableRef.swift @@ -88,6 +88,7 @@ public struct LuaTableRef { } } + // ElementType will only ever be Any or AnyHashable (needed when resolving Dictionary keys) private func doResolveArray(test: (Array) -> Bool) -> Array? { var testArray = Array() func good(_ val: Any) -> Bool { @@ -119,7 +120,11 @@ public struct LuaTableRef { elementType = .rawpointer acceptsAny = false } else { +#if LUASWIFT_ANYHASHABLE_BROKEN + elementType = TypeConstraint(intTest: good) +#else elementType = nil +#endif acceptsAny = false } @@ -162,6 +167,10 @@ public struct LuaTableRef { result.append(ref.guessType()) // as per tovalue() docs case .dict, .array, .hashableDict, .hashableArray, .direct, .rawpointer: // None of these are applicable for TypeConstraint(stringTest:) fatalError() +#if LUASWIFT_ANYHASHABLE_BROKEN + case .int, .int8, .int16, .int32, .int64, .uint, .uint8, .uint16, .uint32, .uint64: // Ditto + fatalError() +#endif case .none: // TypeConstraint(stringTest:) failed to find any compatible type return nil } @@ -193,10 +202,14 @@ public struct LuaTableRef { resolvedVal = nil } case .string, .bytes, .direct, .rawpointer: // None of these are applicable for TypeConstraint(tableTest:) - return nil + fatalError() +#if LUASWIFT_ANYHASHABLE_BROKEN + case .int, .int8, .int16, .int32, .int64, .uint, .uint8, .uint16, .uint32, .uint64: // Ditto + fatalError() +#endif #if !LUASWIFT_NO_FOUNDATION case .data: // ditto - return nil + fatalError() #endif case .none: // TypeConstraint(tableTest:) failed to find any compatible type return nil @@ -211,6 +224,19 @@ public struct LuaTableRef { } else if elementType == .rawpointer, let mut = value as? UnsafeMutableRawPointer { result.append(UnsafeRawPointer(mut)) } else { +#if LUASWIFT_ANYHASHABLE_BROKEN + if let elementType { + switch elementType { + case .int, .int8, .int16, .int32, .int64, .uint, .uint8, .uint16, .uint32, .uint64: + // Reuse PossibleValue's actualValue cos I'm lazy (L isn't actually used but must be specified...) + result.append(PossibleValue(type: elementType, testValue: value).actualValue(L, 0, value)!) + continue + default: + break + } + + } +#endif // Nothing from toany has made T happy, give up return nil } @@ -330,6 +356,10 @@ public struct LuaTableRef { case .data: self.testValue = emptyData #endif case .direct: self.testValue = testValue! +#if LUASWIFT_ANYHASHABLE_BROKEN + case .int, .int8, .int16, .int32, .int64, .uint, .uint8, .uint16, .uint32, .uint64: + self.testValue = testValue! +#endif case .luavalue: self.testValue = LuaValue.nilValue case .anyhashable: self.testValue = opaqueHashable case .rawpointer: self.testValue = dummyRawPtr @@ -346,6 +376,18 @@ public struct LuaTableRef { case .array, .hashableArray: fatalError("Can't call actualValue on an array") case .dict, .hashableDict: fatalError("Can't call actualValue on a dict") case .direct: return anyVal +#if LUASWIFT_ANYHASHABLE_BROKEN + case .int: return Int(exactly: anyVal as! lua_Integer) + case .int8: return Int8(exactly: anyVal as! lua_Integer) + case .int16: return Int16(exactly: anyVal as! lua_Integer) + case .int32: return Int32(exactly: anyVal as! lua_Integer) + case .int64: return Int64(exactly: anyVal as! lua_Integer) + case .uint: return UInt(exactly: anyVal as! lua_Integer) + case .uint8: return UInt8(exactly: anyVal as! lua_Integer) + case .uint16: return UInt16(exactly: anyVal as! lua_Integer) + case .uint32: return UInt32(exactly: anyVal as! lua_Integer) + case .uint64: return UInt64(exactly: anyVal as! lua_Integer) +#endif case .luavalue: return L.ref(index: index) case .anyhashable: if let stringRef { @@ -416,6 +458,46 @@ public struct LuaTableRef { if type == nil || type == .direct { result.append(PossibleValue(type: .direct, testValue: value)) } +#if LUASWIFT_ANYHASHABLE_BROKEN + if value is lua_Integer { + if type == nil || type == .int { + result.append(PossibleValue(type: .int, testValue: 0 as Int)) + } + if type == nil || type == .int8 { + result.append(PossibleValue(type: .int8, testValue: 0 as Int8)) + } + if type == nil || type == .int16 { + result.append(PossibleValue(type: .int16, testValue: 0 as Int16)) + } + if type == nil || type == .int16 { + result.append(PossibleValue(type: .int16, testValue: 0 as Int16)) + } + if type == nil || type == .int32 { + result.append(PossibleValue(type: .int32, testValue: 0 as Int32)) + } + if type == nil || type == .int64 { + result.append(PossibleValue(type: .int64, testValue: 0 as Int64)) + } + if type == nil || type == .uint { + result.append(PossibleValue(type: .uint, testValue: 0 as UInt)) + } + if type == nil || type == .uint8 { + result.append(PossibleValue(type: .uint8, testValue: 0 as UInt8)) + } + if type == nil || type == .uint16 { + result.append(PossibleValue(type: .uint16, testValue: 0 as UInt16)) + } + if type == nil || type == .uint16 { + result.append(PossibleValue(type: .uint16, testValue: 0 as UInt16)) + } + if type == nil || type == .uint32 { + result.append(PossibleValue(type: .uint32, testValue: 0 as UInt32)) + } + if type == nil || type == .uint64 { + result.append(PossibleValue(type: .uint64, testValue: 0 as UInt64)) + } + } +#endif if type == nil || type == .rawpointer { result.append(PossibleValue(type: .rawpointer)) } @@ -462,6 +544,18 @@ enum TypeConstraint { case anyhashable // AnyHashable (or Any, in some contexts) case luavalue case rawpointer // UnsafeRawPointer (relevant when we have UnsafeMutableRawPointer from a [light]userdata) +#if LUASWIFT_ANYHASHABLE_BROKEN + case int + case int8 + case int16 + case int32 + case int64 + case uint + case uint8 + case uint16 + case uint32 + case uint64 +#endif } extension TypeConstraint { @@ -495,6 +589,34 @@ extension TypeConstraint { return nil } } + +#if LUASWIFT_ANYHASHABLE_BROKEN + init?(intTest test: (Any) -> Bool) { + if test(0 as Int) { + self = .int + } else if test(0 as Int8) { + self = .int8 + } else if test(0 as Int16) { + self = .int16 + } else if test(0 as Int32) { + self = .int32 + } else if test(0 as Int64) { + self = .int64 + } else if test(0 as UInt) { + self = .uint + } else if test(0 as UInt8) { + self = .uint8 + } else if test(0 as UInt16) { + self = .uint16 + } else if test(0 as UInt32) { + self = .uint32 + } else if test(0 as UInt64) { + self = .uint64 + } else { + return nil + } + } +#endif } fileprivate func isArrayType(_: T?) -> Bool { @@ -517,9 +639,9 @@ extension Dictionary where Key == AnyHashable, Value == Any { /// /// This function is also defined on `Dictionary`, see ``luaTableToArray()-3ngmn``. public func luaTableToArray() -> [Any]? { - var intKeys: [Int] = [] + var intKeys: [lua_Integer] = [] for (k, _) in self { - if let intKey = k as? Int, intKey > 0 { + if let intKey = k as? lua_Integer, intKey > 0 { intKeys.append(intKey) } else { // Non integer key found, doom @@ -530,9 +652,9 @@ extension Dictionary where Key == AnyHashable, Value == Any { // Now check all those integer keys are a sequence and build the result intKeys.sort() var result: [Any] = [] - var i = 1 + var i: lua_Integer = 1 while i <= intKeys.count { - if intKeys[i-1] == i { + if intKeys[Int(i-1)] == i { result.append(self[i]!) i = i + 1 } else { @@ -557,9 +679,9 @@ extension Dictionary where Key == AnyHashable, Value == AnyHashable { /// /// This function is also defined on `Dictionary`, see ``luaTableToArray()-7jqqs``. public func luaTableToArray() -> [AnyHashable]? { - var intKeys: [Int] = [] + var intKeys: [lua_Integer] = [] for (k, _) in self { - if let intKey = k as? Int, intKey > 0 { + if let intKey = k as? lua_Integer, intKey > 0 { intKeys.append(intKey) } else { // Non integer key found, doom @@ -570,9 +692,9 @@ extension Dictionary where Key == AnyHashable, Value == AnyHashable { // Now check all those integer keys are a sequence and build the result intKeys.sort() var result: [AnyHashable] = [] - var i = 1 + var i: lua_Integer = 1 while i <= intKeys.count { - if intKeys[i-1] == i { + if intKeys[Int(i-1)] == i { result.append(self[i]!) i = i + 1 } else { diff --git a/Tests/lua-test/LuaTests.swift b/Tests/lua-test/LuaTests.swift index 4e3a08d..ab9f853 100644 --- a/Tests/lua-test/LuaTests.swift +++ b/Tests/lua-test/LuaTests.swift @@ -4,6 +4,10 @@ import XCTest import Lua import CLua +#if !LUASWIFT_NO_FOUNDATION +import Foundation +import CoreFoundation +#endif fileprivate func dummyFn(_ L: LuaState!) -> CInt { return 0 @@ -2421,8 +2425,11 @@ final class LuaTests: XCTestCase { let asciiByteArray: [UInt8] = [0x64, 0x65, 0x66] let nonUtf8ByteArray: [UInt8] = [0xFF, 0xFF, 0xFF] let intArray = [11, 22, 33] + let luaIntegerArray: [lua_Integer] = [11, 22, 33] let intArrayAsDict = [1: 11, 2: 22, 3: 33] + let intArrayAsLuaIntegerDict: [lua_Integer: lua_Integer] = [1: 11, 2: 22, 3: 33] let stringIntDict = ["aa": 11, "bb": 22, "cc": 33] + let stringLuaIntegerDict: [String: lua_Integer] = ["aa": 11, "bb": 22, "cc": 33] let stringArrayIntDict: Dictionary<[String], Int> = [ ["abc"]: 123 ] let whatEvenIsThis: Dictionary>, Int> = [ ["abc": [123: 456]]: 789 ] @@ -2442,12 +2449,20 @@ final class LuaTests: XCTestCase { XCTAssertEqual(L.tovalue(3, type: Any.self) as? [UInt8], nonUtf8ByteArray) XCTAssertEqual(L.tovalue(3, type: AnyHashable.self) as? [UInt8], nonUtf8ByteArray) + XCTAssertEqual(L.tovalue(4, type: Any.self) as? Dictionary, intArrayAsLuaIntegerDict) + XCTAssertEqual((L.tovalue(4, type: Any.self) as? Dictionary)?.luaTableToArray() as? Array, luaIntegerArray) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertEqual(L.tovalue(4, type: Dictionary.self) as? Dictionary, intArrayAsDict) XCTAssertEqual(L.tovalue(4, type: Any.self) as? Dictionary, intArrayAsDict) XCTAssertEqual((L.tovalue(4, type: Any.self) as? Dictionary)?.luaTableToArray() as? Array, intArray) +#endif + + XCTAssertEqual(L.tovalue(5, type: Dictionary.self) as? Dictionary, stringLuaIntegerDict) + XCTAssertEqual(L.tovalue(5, type: Any.self) as? Dictionary, stringLuaIntegerDict) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertEqual(L.tovalue(5, type: Dictionary.self) as? Dictionary, stringIntDict) XCTAssertEqual(L.tovalue(5, type: Any.self) as? Dictionary, stringIntDict) - +#endif let tableKeyDict = L.tovalue(6, type: Dictionary<[String], Int>.self) XCTAssertEqual(tableKeyDict, stringArrayIntDict) @@ -2458,8 +2473,12 @@ final class LuaTests: XCTestCase { // tables _can_ now be returned as AnyHashable - they will always convert to Dictionary. let anyHashableDict: AnyHashable = try XCTUnwrap(L.tovalue(5)) + XCTAssertEqual(anyHashableDict as? [String: lua_Integer], stringLuaIntegerDict) + XCTAssertEqual((L.tovalue(4, type: AnyHashable.self) as? Dictionary)?.luaTableToArray() as? Array, luaIntegerArray) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertEqual(anyHashableDict as? [String: Int], stringIntDict) XCTAssertEqual((L.tovalue(4, type: AnyHashable.self) as? Dictionary)?.luaTableToArray() as? Array, intArray) +#endif } // There are 2 basic Any pathways to worry about, which are tovalue and tovalue. @@ -2471,9 +2490,15 @@ final class LuaTests: XCTestCase { func test_tovalue_any_int() { L.push(123) let anyVal: Any? = L.tovalue(1) + XCTAssertNotNil(anyVal as? lua_Integer) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertNotNil(anyVal as? Int) +#endif let anyHashable: AnyHashable? = L.tovalue(1) + XCTAssertNotNil(anyHashable as? lua_Integer) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertNotNil(anyHashable as? Int) +#endif } func test_tovalue_any_string() { @@ -2520,12 +2545,18 @@ final class LuaTests: XCTestCase { L.push(["abc": 123]) let anyDict: Dictionary = try XCTUnwrap(L.tovalue(1)) + XCTAssertEqual(anyDict as? [String: lua_Integer], ["abc": 123]) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertEqual(anyDict as? [String: Int], ["abc": 123]) +#endif // Check T=Any does in fact behave the same as T=Dictionary let anyVal: Any = try XCTUnwrap(L.tovalue(1)) XCTAssertTrue(type(of: anyVal) == Dictionary.self) + XCTAssertEqual(anyVal as? [String: lua_Integer], ["abc": 123]) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertEqual(anyVal as? [String: Int], ["abc": 123]) +#endif } func test_tovalue_stringanydict() throws { @@ -2534,7 +2565,10 @@ final class LuaTests: XCTestCase { L.rawset(-1, key: "123", value: 456) let anyDict: Dictionary = try XCTUnwrap(L.tovalue(1)) XCTAssertEqual(anyDict["abc"] as? String, "def") + XCTAssertEqual(anyDict["123"] as? lua_Integer, 456) +#if !LUASWIFT_ANYHASHABLE_BROKEN XCTAssertEqual(anyDict["123"] as? Int, 456) +#endif } func test_tovalue_luavalue() throws { @@ -2682,9 +2716,12 @@ final class LuaTests: XCTestCase { XCTAssertThrowsError(try L.load(string: "woop woop"), "", { err in let expected = #"[string "woop woop"]:1: syntax error near 'woop'"# - XCTAssertEqual((err as? LuaLoadError), .parseError(expected)) - XCTAssertEqual((err as CustomStringConvertible).description, "LuaLoadError.parseError(\(expected))") - XCTAssertEqual(err.localizedDescription, "LuaLoadError.parseError(\(expected))") + // Note, casting err directly to CustomStringConvertible does _not_ pick up the LuaLoadError impl when + // testing on Linux... + let loadErr = err as? LuaLoadError + XCTAssertEqual(loadErr, .parseError(expected)) + XCTAssertEqual((loadErr as CustomStringConvertible?)?.description, "LuaLoadError.parseError(\(expected))") + XCTAssertEqual(loadErr?.localizedDescription, "LuaLoadError.parseError(\(expected))") }) XCTAssertThrowsError(try L.load(string: "woop woop", name: "@nope.lua"), "", { err in @@ -3020,7 +3057,11 @@ final class LuaTests: XCTestCase { // CF bridging is _weird_: I cannot write L.push(cfn) ie CFNumber does not directly conform to Pushable, but // the conversion to Pushable will always succeed, presumably because NSNumber is Pushable? let cfn_pushable = try XCTUnwrap(cfn as? Pushable) +#if !os(Linux) // CF bridging seems to be entirely lacking on Linux... L.push(cfn as NSNumber) // 3 - CFNumber as NSNumber (Pushable) +#else + L.pushnil() +#endif L.push(cfn_pushable) // 4 - CFNumber as Pushable L.push(any: cfn) // 5 - CFNumber as Any L.push(nd) // 6 - integer-representable NSNumber from a double @@ -3032,7 +3073,9 @@ final class LuaTests: XCTestCase { XCTAssertEqual(L.toint(1), 1234) XCTAssertEqual(L.toint(2), 1234) +#if !os(Linux) XCTAssertEqual(L.tonumber(3), 1234.5678) +#endif XCTAssertEqual(L.tonumber(4), 1234.5678) XCTAssertEqual(L.tonumber(5), 1234.5678) } @@ -3424,9 +3467,11 @@ final class LuaTests: XCTestCase { let emptyDict: [AnyHashable: Any] = [:] XCTAssertEqual(emptyDict.luaTableToArray() as? [Bool], []) +#if !LUASWIFT_ANYHASHABLE_BROKEN let dict: [AnyHashable: Any] = [1: 111, 2: 222, 3: 333] XCTAssertEqual(dict.luaTableToArray() as? [Int], [111, 222, 333]) XCTAssertEqual((dict as! [AnyHashable: AnyHashable]).luaTableToArray() as? [Int], [111, 222, 333]) +#endif // A Lua array table shouldn't have an index 0 let zerodict: [AnyHashable: Any] = [0: 0, 1: 111, 2: 222, 3: 333] @@ -3441,10 +3486,12 @@ final class LuaTests: XCTestCase { XCTAssertNil(gap.luaTableToArray()) XCTAssertNil((gap as! [AnyHashable: AnyHashable]).luaTableToArray()) +#if !LUASWIFT_ANYHASHABLE_BROKEN // This should succeed because AnyHashable type-erases numbers so 2.0 should be treated just like 2 let sneakyDouble: [AnyHashable: Any] = [1: 111, 2.0: 222, 3: 333] XCTAssertEqual(sneakyDouble.luaTableToArray() as? [Int], [111, 222, 333]) XCTAssertEqual((sneakyDouble as! [AnyHashable: AnyHashable]).luaTableToArray() as? [Int], [111, 222, 333]) +#endif let sneakyFrac: [AnyHashable: Any] = [1: 111, 2: 222, 2.5: "wat", 3: 333] XCTAssertNil((sneakyFrac as! [AnyHashable: AnyHashable]).luaTableToArray())