kotlinkotlin-multiplatformkotlin-native

Actual function implementation returns KotlinCValue<AnyObject> instead of specific objective-c type


I wan't to abstract two classes from their native implementation in my Kotlin Multiplatform project.
The two classes represent a Position and a Region but I also want to be able to transform them into their native objective-c implementation, so into CLLocationCoordinate2D and MKCoordinateRegion.

To achieve this I used the expect actual pattern but added the two functions toCoreLocation and toCoreRegion which should return me their native counterparts. These functions work fine in the Kotlin code but the generated objective-c code does not return the type of CLLocationCoordinate2D or MKCoordinateRegion but just KotlinCValue<AnyObject>.

This is my commonMain code:

expect class Position(
    latitude: Double,
    longitude: Double
)

expect class Region(
    center: Position,
    latitudinalMeters: Double,
    longitudinalMeters: Double
)

This is my actual implementation inside iosMain:

import platform.MapKit.MKCoordinateRegionMakeWithDistance
import platform.CoreLocation.CLLocationCoordinate2DMake

actual class Position actual constructor(
    val latitude: Double,
    val longitude: Double
) {
    fun toCoreLocation() = CLLocationCoordinate2DMake(
        latitude, longitude
    )
}

actual class Region actual constructor(
    val center: Position,
    val latitudinalMeters: Double,
    val longitudinalMeters: Double
) {
    fun toCoreRegion() = MKCoordinateRegionMakeWithDistance(
        center.toCoreLocation(), latitudinalMeters, longitudinalMeters
    )
}

And this is the compiled objective-c code:

public class Position : KotlinBase {

    public init(latitude: Double, longitude: Double)

    open func toCoreLocation() -> KotlinCValue<AnyObject>

    open var latitude: Double { get }

    open var longitude: Double { get }
}

public class Region : KotlinBase {

    public init(center: Position, latitudinalMeters: Double, longitudinalMeters: Double)

    open func toCoreRegion() -> KotlinCValue<AnyObject>

    open var center: Position { get }

    open var latitudinalMeters: Double { get }

    open var longitudinalMeters: Double { get }
}

Does anybody know how it's possible to return the native type from the compiled objective-c functions?
Do I maybe need to add .def file into the nativeInterop/cinterop directory?


Solution

  • You need to ensure that the value of your object is in a fixed memory position. If you want to to access it in Swift, you need to unpack it first.

    To do so, you have two options:

    a) You can create a struct in Swift and copy the cValue to the pointer of the struct via place in kotlin.

    // in Kotlin, Test.kt
    fun place(
        loc: CValue<CLLocationCoordinate2D>,
        ptr: CPointer<CLLocationCoordinate2D>,
    ) {
        // copy the cValue into the pointer address
        loc.place(ptr)
    }
    
    // in Swift
    func unrwap(_ cValue:KotlinCValue<AnyObject>) -> CLLocationCoordinate2D  {
        var ptr = CLLocationCoordinate2D()
        TestKt.place(cValue:cValue, ptr: &ptr)
        return ptr // the coordinates are now populated 
    }
    

    b) You can load the MutableUnsafeRawPointer while the cValue is in a memScoped block. For that you need to write a helper in Kotlin, which takes a callback when the pointer is accessible in the memory. This raw pointer can be instantiated using load.

    // in Kotlin
    fun interface PtrHandler<T : CStructVar> {
        fun handle(ptr: CPointer<T>): Unit
    }
    
    object Bridge {
        fun <T : CStructVar> load(cValue: CValue<T>, handler: PtrHandler<T>) {
            memScoped { 
                //2: the ptr of cValue is fixed here
                handler.handle(cValue.ptr)
            }
        }
    }
    
    // in Swift
    private class SwiftPtrHandler<T>:PtrHandler {
        var loaded:T {
            return _loaded!
        }
        
        private var _loaded:T? = nil
    
        //3: receives the raw pointer from the Bridge object
        func handle(ptr:UnsafeMutableRawPointer) {
            let t = ptr.load(as: T.self)
            // 4: populate for later usage
            _loaded = t
        }
    }
    
    
    func unwrap<T>(_ cValue:KotlinCValue<AnyObject>) -> T {
        let handler = SwiftPtrHandler<T>()
        //1: call load 
        Bridge().load(cValue: cValue, handler: handler)
        return handler.loaded
    }
    
    // usage
    let loc:CLLocationCoordinate2D = unwrap(cValue)