diff --git a/Sources/SkipBridge/BridgeSupport.swift b/Sources/SkipBridge/BridgeSupport.swift index 2c1e0f5..d923ecd 100644 --- a/Sources/SkipBridge/BridgeSupport.swift +++ b/Sources/SkipBridge/BridgeSupport.swift @@ -62,17 +62,15 @@ extension SwiftObjectPointer { return try! SwiftObjectPointer.call(Java_PeerBridged_peer_methodID, on: bridged, options: options, args: []) } - /// Return a pointer to the Swift instance for a given Java object. - public static func projection(of object: JavaObjectPointer, options: JConvertibleOptions, peerOnly: Bool = false) -> SwiftObjectPointer? { + /// Return the `Swift_peer` of the given Kotlin object if it is `SwiftPeerBridged`. + public static func tryPeer(of object: JavaObjectPointer, options: JConvertibleOptions) -> SwiftObjectPointer? { let object_java = object.toJavaParameter(options: options) - let options_java = options.rawValue.toJavaParameter(options: options) - let peerOnly_java = peerOnly.toJavaParameter(options: options) - let ptr: SwiftObjectPointer = try! Java_fileClass.callStatic(method: Java_projection_methodID, options: options, args: [object_java, options_java, peerOnly_java]) + let ptr: SwiftObjectPointer = try! Java_fileClass.callStatic(method: Java_tryPeer_methodID, options: options, args: [object_java]) return ptr == SwiftObjectNil ? nil : ptr } } private let Java_fileClass = try! JClass(name: "skip/bridge/kt/BridgeSupportKt") -private let Java_projection_methodID = Java_fileClass.getStaticMethodID(name: "Swift_projection", sig: "(Ljava/lang/Object;IZ)J")! +private let Java_tryPeer_methodID = Java_fileClass.getStaticMethodID(name: "Swift_peer", sig: "(Ljava/lang/Object;)J")! private let Java_PeerBridged_class = try! JClass(name: "skip/bridge/kt/SwiftPeerBridged") private let Java_PeerBridged_peer_methodID = Java_PeerBridged_class.getMethodID(name: "Swift_peer", sig: "()J")! diff --git a/Sources/SkipBridge/BridgedTypes.swift b/Sources/SkipBridge/BridgedTypes.swift index dc8a479..1185209 100644 --- a/Sources/SkipBridge/BridgedTypes.swift +++ b/Sources/SkipBridge/BridgedTypes.swift @@ -6,6 +6,131 @@ import Foundation +// +// NOTE: +// Keep this in sync with `SkipBridgeKt.BridgedTypes` +// + +/// Supported bridged type constants. +public enum BridgedTypes: String { + case boolean_ + case byte_ + case char_ + case double_ + case float_ + case int_ + case long_ + case short_ + case string_ + + case byteArray + case date + case list + case map + case set + case uuid + case uri + + case swiftArray + case swiftData + case swiftDate + case swiftDictionary + case swiftSet + case swiftUUID + case swiftURL + + case other +} + +/// Utilities to convert unknown bridged objects. +public struct AnyBridging { + /// Convert an unknown Kotlin/Java instance to its Swift projection. + public static func fromJavaObject(_ ptr: JavaObjectPointer?, options: JConvertibleOptions, fallback: (() -> Any)? = nil) -> Any? { + guard let ptr else { + return nil + } + if let projection = tryProjection(of: ptr, options: options) { + return projection + } + + let bridgedTypeString = bridgedTypeString(of: ptr, options: options) + let bridgedType = BridgedTypes(rawValue: bridgedTypeString) ?? .other + switch bridgedType { + case .boolean_: + return Bool.fromJavaObject(ptr, options: options) + case .byte_: + return Int8.fromJavaObject(ptr, options: options) + case .char_: + // TODO + // return Character.fromJavaObject(ptr, options: options) + fatalError("Character is not yet bridgable") + case .double_: + return Double.fromJavaObject(ptr, options: options) + case .float_: + return Float.fromJavaObject(ptr, options: options) + case .int_: + return Int.fromJavaObject(ptr, options: options) + case .long_: + return Int64.fromJavaObject(ptr, options: options) + case .short_: + return Int16.fromJavaObject(ptr, options: options) + case .string_: + return String.fromJavaObject(ptr, options: options) + case .byteArray: + return Data.fromJavaObject(ptr, options: options) + case .date: + return Date.fromJavaObject(ptr, options: options) + case .list: + return Array.fromJavaObject(ptr, options: options) + case .map: + return Dictionary.fromJavaObject(ptr, options: options) + case .set: + return Array.fromJavaObject(ptr, options: options) + case .uuid: + return UUID.fromJavaObject(ptr, options: options) + case .uri: + return URL.fromJavaObject(ptr, options: options) + case .swiftArray: + return Array.fromJavaObject(ptr, options: options) + case .swiftData: + return Data.fromJavaObject(ptr, options: options) + case .swiftDate: + return Date.fromJavaObject(ptr, options: options) + case .swiftDictionary: + return Dictionary.fromJavaObject(ptr, options: options) + case .swiftSet: + return Array.fromJavaObject(ptr, options: options) + case .swiftUUID: + return UUID.fromJavaObject(ptr, options: options) + case .swiftURL: + return URL.fromJavaObject(ptr, options: options) + case .other: + if let fallback { + return fallback() + } else { + fatalError("Unable to bridge Kotlin/Java instance \(ptr)") + } + } + } + + private static func tryProjection(of ptr: JavaObjectPointer, options: JConvertibleOptions) -> Any? { + let ptr_java = ptr.toJavaParameter(options: options) + let options_java = options.rawValue.toJavaParameter(options: options) + let closure_java: JavaObjectPointer? = try! Java_fileClass.callStatic(method: Java_tryProjection_methodID, options: options, args: [ptr_java, options_java]) + let closure: (() -> Any)? = SwiftClosure0.closure(forJavaObject: closure_java, options: options) + return closure?() + } + + private static func bridgedTypeString(of ptr: JavaObjectPointer, options: JConvertibleOptions) -> String { + let ptr_java = ptr.toJavaParameter(options: options) + return try! Java_fileClass.callStatic(method: Java_bridgedTypeString_methodID, options: options, args: [ptr_java]) + } +} + +private let Java_fileClass = try! JClass(name: "skip/bridge/kt/BridgeSupportKt") +private let Java_tryProjection_methodID = Java_fileClass.getStaticMethodID(name: "Swift_projection", sig: "(Ljava/lang/Object;I)Lkotlin/jvm/functions/Function0;")! +private let Java_bridgedTypeString_methodID = Java_fileClass.getStaticMethodID(name: "bridgedTypeStringOf", sig: "(Ljava/lang/Object;)Ljava/lang/String;")! + // // NOTE: // The Kotlin version of custom converting types should conform to `SwiftCustomBridged`. @@ -29,11 +154,17 @@ extension Array: JObjectProtocol, JConvertible { for i in 0.. SwiftObjectPointer { - if peerOnly { - let peer = (object as? SwiftPeerBridged)?.Swift_peer() - return peer ?? SwiftObjectNil - } else { - let projection = (object as? SwiftProjectable)?.Swift_projection(options: options) - return projection ?? SwiftObjectNil - } -} - -/// Protocol added to a Kotlin type that can project to Swift. -public protocol SwiftProjectable { - func Swift_projection(options: Int) -> SwiftObjectPointer +public func Swift_peer(of object: Any) -> SwiftObjectPointer { + let peer = (object as? SwiftPeerBridged)?.Swift_peer() + return peer ?? SwiftObjectNil } /// Protocol added to a Kotlin projection type backed by a Swift peer object. @@ -31,14 +21,23 @@ public protocol SwiftPeerBridged { func Swift_peer() -> SwiftObjectPointer } -extension SwiftPeerBridged: SwiftProjectable { - public func Swift_projection(options: Int) -> SwiftObjectPointer { - return Swift_peer() - } -} - /// Marker type used to guarantee uniqueness of our `Swift_peer` constructor. public final class SwiftPeerMarker { } +/// Return a closure that creates a Swift projection of this Kotlin instance, else nil. +public func Swift_projection(of object: Any, options: Int) -> (() -> Any)? { + return (object as? SwiftProjectable)?.Swift_projection(options: options) +} + +/// Protocol added to a Kotlin type that can project to Swift. +public protocol SwiftProjectable { + func Swift_projection(options: Int) -> () -> Any +} + +/// Return the `BridgedTypes` enum case name for the given Kotlin/Java object's type. +public func bridgedTypeStringOf(_ object: Any) -> String { + return bridgedTypeOf(object).name +} + #endif diff --git a/Sources/SkipBridgeKt/BridgedTypes.swift b/Sources/SkipBridgeKt/BridgedTypes.swift new file mode 100644 index 0000000..b05513f --- /dev/null +++ b/Sources/SkipBridgeKt/BridgedTypes.swift @@ -0,0 +1,105 @@ +// Copyright 2024 Skip +// +// This is free software: you can redistribute and/or modify it +// under the terms of the GNU Lesser General Public License 3.0 +// as published by the Free Software Foundation https://fsf.org + +#if SKIP + +// +// NOTE: +// Keep this in sync with `SkipBridge.BridgedTypes` +// + +/// Supported bridged type constants. +public enum BridgedTypes { + case boolean_ + case byte_ + case char_ + case double_ + case float_ + case int_ + case long_ + case short_ + case string_ + + case byteArray + case date + case list + case map + case set + case uuid + case uri + + case swiftArray + case swiftData + case swiftDate + case swiftDictionary + case swiftSet + case swiftUUID + case swiftURL + + case other +} + +public func bridgedTypeOf(_ object: Any) -> BridgedTypes { + // Likely most common case is a custom-bridged object + guard !(object is SwiftProjectable) else { + return .other + } + + if object is Bool { + return .boolean_ + } else if object is Int8 { + return .byte_ + } else if object is Character { + return .char_ + } else if object is Double { + return .double_ + } else if object is Float { + return .float_ + } else if object is Int { + return .int_ + } else if object is Int64 { + return .long_ + } else if object is Int16 { + return .short_ + } else if object is String { + return .string_ + } + + if object is kotlin.ByteArray { + return .byteArray + } else if object is java.util.Date { + return .date + } else if object is kotlin.collections.List { + return .list + } else if object is kotlin.collections.Map { + return .map + } else if object is kotlin.collections.Set { + return .set + } else if object is java.util.UUID { + return .uuid + } else if object is java.net.URI { + return .uri + } + + // Use class name so that we don't have a dependency on Skip libraries for these types + // SKIP INSERT: val className = object_::class.java.name + switch className { + case "skip.lib.Array": + return .swiftArray + case "skip.foundation.Data": + return .swiftData + case "skip.foundation.Date": + return .swiftDate + case "skip.lib.Dictionary": + return .swiftDictionary + case "skip.lib.Set": + return .swiftSet + default: + return .other + } +} + +#endif diff --git a/Sources/SkipBridgeToKotlinSamples/Samples.swift b/Sources/SkipBridgeToKotlinSamples/Samples.swift index e6c4df8..d6292d3 100644 --- a/Sources/SkipBridgeToKotlinSamples/Samples.swift +++ b/Sources/SkipBridgeToKotlinSamples/Samples.swift @@ -55,6 +55,7 @@ public var swiftStringVar = "s" public var swiftClassVar = SwiftHelperClass() public var swiftInnerClassVar = SwiftHelperClass.Inner() public var swiftKotlinClassVar = KotlinHelperClass() +public var swiftAnyVar: Any = "a" // MARK: Global optional vars diff --git a/Sources/SkipBridgeToSwiftSamples/Samples.swift b/Sources/SkipBridgeToSwiftSamples/Samples.swift index b26b4f0..1475220 100644 --- a/Sources/SkipBridgeToSwiftSamples/Samples.swift +++ b/Sources/SkipBridgeToSwiftSamples/Samples.swift @@ -57,6 +57,7 @@ public var kotlinStringVar = "s" public var kotlinClassVar = KotlinHelperClass() public var kotlinInnerClassVar = KotlinHelperClass.Inner() public var kotlinSwiftClassVar = SwiftHelperClass() +public var kotlinAnyVar: Any = "a" // MARK: Global optional vars diff --git a/Sources/SkipBridgeToSwiftSamplesTestsSupport/TestsSupport.swift b/Sources/SkipBridgeToSwiftSamplesTestsSupport/TestsSupport.swift index feb0924..a3fda14 100644 --- a/Sources/SkipBridgeToSwiftSamplesTestsSupport/TestsSupport.swift +++ b/Sources/SkipBridgeToSwiftSamplesTestsSupport/TestsSupport.swift @@ -157,6 +157,18 @@ public func testSupport_kotlinSwiftClassVar_stringVar(value: String) -> String { return kotlinSwiftClassVar.stringVar } +public func testSupport_kotlinAnyVar(value: Any) -> Any { + kotlinAnyVar = value + return kotlinAnyVar +} + +public func testSupport_kotlinAnyVar_kotlinClass(value: String) -> String? { + let helper = KotlinHelperClass() + helper.stringVar = value + kotlinAnyVar = helper + return (kotlinAnyVar as? KotlinHelperClass)?.stringVar +} + public func testSupport_kotlinOptionalBoolVar(value: Bool?) -> Bool? { kotlinOptionalBoolVar = value return kotlinOptionalBoolVar @@ -323,11 +335,11 @@ public func testSupport_kotlinProtocolMember() -> String? { guard obj.optionalKotlinProtocolVar?.stringValue() == "foo" else { return "obj.optionalKotlinProtocolVar?.stringValue() == \"foo\"" } - - let obj2 = KotlinClass() - obj2.optionalKotlinProtocolVar = helper - guard obj.optionalKotlinProtocolVar?.hashValue == obj2.optionalKotlinProtocolVar?.hashValue else { - return "obj.optionalKotlinProtocolVar?.hashValue == obj2.optionalKotlinProtocolVar?.hashValue" + guard obj.optionalKotlinProtocolVar is KotlinHelperClass else { + return "obj.optionalKotlinProtocolVar is KotlinHelperClass" + } + guard obj.optionalKotlinProtocolVar?.hashValue == helper.hashValue else { + return "obj.optionalKotlinProtocolVar?.hashValue == helper.hashValue" } return nil } @@ -345,13 +357,12 @@ public func testSupport_swiftProtocolMember() -> String? { guard obj.optionalSwiftProtocolVar?.stringValue() == "foo" else { return "obj.optionalSwiftProtocolVar?.stringValue() == \"foo\"" } - - let obj2 = KotlinClass() - obj2.optionalSwiftProtocolVar = helper - guard obj.optionalSwiftProtocolVar?.hashValue == obj2.optionalSwiftProtocolVar?.hashValue else { - return "obj.optionalSwiftProtocolVar?.hashValue == obj2.optionalSwiftProtocolVar?.hashValue" + guard obj.optionalSwiftProtocolVar is SwiftHelperClass else { + return "obj.optionalKotlinProtocolVar is SwiftHelperClass" + } + guard obj.optionalSwiftProtocolVar?.hashValue == helper.hashValue else { + return "obj.optionalSwiftProtocolVar?.hashValue == helper.hashValue" } - return nil } diff --git a/Tests/SkipBridgeToKotlinSamplesTests/BridgeToKotlinSamplesTests.swift b/Tests/SkipBridgeToKotlinSamplesTests/BridgeToKotlinSamplesTests.swift index 3a23615..a301f38 100644 --- a/Tests/SkipBridgeToKotlinSamplesTests/BridgeToKotlinSamplesTests.swift +++ b/Tests/SkipBridgeToKotlinSamplesTests/BridgeToKotlinSamplesTests.swift @@ -88,6 +88,36 @@ final class BridgeToKotlinTests: XCTestCase { XCTAssertEqual(testSupport_swiftKotlinClassVar_stringVar(value: "ss"), "ss") } + func testAnyVar() { + swiftAnyVar = 1 + XCTAssertEqual(swiftAnyVar as? Int, 1) + + swiftAnyVar = "a" + XCTAssertEqual(swiftAnyVar as? String, "a") + + let helper = SwiftHelperClass() + swiftAnyVar = helper + XCTAssertTrue(swiftAnyVar is SwiftHelperClass) + XCTAssertEqual(swiftAnyVar as? SwiftHelperClass, helper) + } + + func testAnyVarContainerValues() { + swiftAnyVar = ["a", 2, 3.0] + guard let anyArray = swiftAnyVar as? [Any] else { + return XCTFail() + } + XCTAssertEqual(anyArray[0] as? String, "a") + XCTAssertEqual(anyArray[1] as? Int, 2) + XCTAssertEqual(anyArray[2] as? Double, 3.0) + + swiftAnyVar = ["a": 1, "b": 2] + guard let anyDictionary = swiftAnyVar as? [AnyHashable: Any] else { + return XCTFail() + } + XCTAssertEqual(anyDictionary["a"] as? Int, 1) + XCTAssertEqual(anyDictionary["b"] as? Int, 2) + } + func testOptionalSimpleVars() { swiftOptionalBoolVar = false XCTAssertEqual(swiftOptionalBoolVar, false) @@ -249,10 +279,8 @@ final class BridgeToKotlinTests: XCTestCase { helper.stringVar = "foo" obj.optionalSwiftProtocolVar = helper XCTAssertEqual(obj.optionalSwiftProtocolVar?.stringValue(), "foo") - - let obj2 = SwiftClass() - obj2.optionalSwiftProtocolVar = helper - XCTAssertEqual(obj.optionalSwiftProtocolVar?.hashValue, obj2.optionalSwiftProtocolVar?.hashValue) + XCTAssertTrue(obj.optionalSwiftProtocolVar is SwiftHelperClass) + XCTAssertEqual(obj.optionalSwiftProtocolVar?.hashValue, helper.hashValue) } public func testKotlinProtocolMember() { @@ -264,10 +292,8 @@ final class BridgeToKotlinTests: XCTestCase { helper.stringVar = "foo" obj.optionalKotlinProtocolVar = helper XCTAssertEqual(obj.optionalKotlinProtocolVar?.stringValue(), "foo") - - let obj2 = SwiftClass() - obj2.optionalKotlinProtocolVar = helper - XCTAssertEqual(obj.optionalSwiftProtocolVar?.hashValue, obj2.optionalSwiftProtocolVar?.hashValue) + XCTAssertTrue(obj.optionalKotlinProtocolVar is KotlinHelperClass) + XCTAssertEqual(obj.optionalKotlinProtocolVar?.hashValue, helper.hashValue) } public func testStruct() { diff --git a/Tests/SkipBridgeToSwiftSamplesTestsSupportTests/BridgeToSwiftSamplesTests.swift b/Tests/SkipBridgeToSwiftSamplesTestsSupportTests/BridgeToSwiftSamplesTests.swift index d0b7b4f..40197c4 100644 --- a/Tests/SkipBridgeToSwiftSamplesTestsSupportTests/BridgeToSwiftSamplesTests.swift +++ b/Tests/SkipBridgeToSwiftSamplesTestsSupportTests/BridgeToSwiftSamplesTests.swift @@ -72,6 +72,29 @@ final class BridgeToSwiftTests: XCTestCase { XCTAssertEqual(testSupport_kotlinSwiftClassVar_stringVar(value: "ss"), "ss") } + func testAnyVar() { + XCTAssertEqual(testSupport_kotlinAnyVar(value: 1) as? Int, 1) + XCTAssertEqual(testSupport_kotlinAnyVar(value: "a") as? String, "a") + XCTAssertEqual(testSupport_kotlinAnyVar_kotlinClass(value: "ss"), "ss") + } + + func testAnyVarContainerValues() { + let anyArray = testSupport_kotlinAnyVar(value: ["a", 2, 3.0]) as? [Any] + guard let anyArray else { + return XCTFail() + } + XCTAssertEqual(anyArray[0] as? String, "a") + XCTAssertEqual(anyArray[1] as? Int, 2) + XCTAssertEqual(anyArray[2] as? Double, 3.0) + + let anyDictionary = testSupport_kotlinAnyVar(value: ["a": 1, "b": 2]) as? [AnyHashable: Any] + guard let anyDictionary else { + return XCTFail() + } + XCTAssertEqual(anyDictionary["a"] as? Int, 1) + XCTAssertEqual(anyDictionary["b"] as? Int, 2) + } + func testOptionalSimpleVars() { XCTAssertEqual(testSupport_kotlinOptionalBoolVar(value: false), false) XCTAssertNil(testSupport_kotlinOptionalBoolVar(value: nil))