Hylke
Hylke

Reputation: 705

How to provide an unfrozen or isolated swift implementation of a kotlin interface to Kotlin Native code

I am trying to create a multiplatform library, which requires some platform / app specific dependencies to function properly. As a solution I thought to define interfaces in kotlin code, and have the host app implement these interfaces and providing them to the library when using the library.

This library will do certain things on other threads using coroutines multithreaded, and it will need to use these dependencies from those threads.

This is where the memory model of kotlin native comes in and bites me, so I got to the solution where I use Stately's IsolatedState for my dependencies, but that does not work as expected. When running the code from an iOS unit test, written purely in kotlin code, it's all okay, but when I try to call it from actual iOS Swift code, it gets weird.

Consider the following example code in multiplatform, where I defined interfaces for a dependency creator and a dependency, which both need to be implemented in swift. Using this I can put the created dependency in an IsolateState, so i can access it from multiple threads.

interface DependencyCreator {
    fun create(): Dependency
}

interface Dependency {
    fun foo()
}

class SomeClient(
    someDependencyCreator: DependencyCreator
) {
    init {
        println("is dependency creator frozen: ${someDependencyCreator.isFrozen}")
    }
    private val dependency: IsolateState<Dependency> = IsolateState {
        val theDependency = someDependencyCreator.create()
        println("is the dependency frozen? ${theDependency.isFrozen}")
        return@IsolateState theDependency
    }

    suspend fun doFoo() = withContext(Dispatchers.Default) {
        delay(100)
        dependency.access { it.foo() }
    }
}

I've written the following unit test code, which i run with the ios target (./gradlew iosTest -i):

@Test
fun testFrozenDependency() {
    val dependencyCreator = object : DependencyCreator {
        override fun create(): Dependency {
            return object : Dependency {
                override fun foo() {
                    println("I foo'd")
                }
            }
        }
    }

    val client = SomeClient(dependencyCreator)
    runBlocking {
        client.doFoo()
    }
    assertTrue(true)
}

The result of this unit test is:

is dependency creator frozen: false
is the dependency frozen? false
I foo'd

However, when I try to do exactly this on iOS, it doesnt work. This is my iOS code:

class iOSDependency : Dependency {
    func foo() {
        print("foo'd from ios")
    }
}

class iOSDependencyCreator : DependencyCreator {
    func create() -> Dependency {
        return iOSDependency()
    }
}

class ViewModel : ObservableObject {
    var client: SomeClient = SomeClient(someDependencyCreator: iOSDependencyCreator())
}

When I run this code (the ViewModel is instantiated), i get the following result:

is dependency creator frozen: true
is the dependency frozen? true
Function doesn't have or inherit @Throws annotation and thus exception isn't propagated from Kotlin to Objective-C/Swift as NSError.
It is considered unexpected and unhandled instead. Program will be terminated.
Uncaught Kotlin exception: kotlin.IllegalStateException: Mutable state shouldn't be frozen
    at 0   library                             0x0000000109adcf8f kfun:kotlin.Throwable#<init>(kotlin.String?){} + 95 (/Users/teamcity2/buildAgent/work/11ac87a349af04d5/runtime/src/main/kotlin/kotlin/Throwable.kt:23:37)
    at 1   library                             0x0000000109ad68cd kfun:kotlin.Exception#<init>(kotlin.String?){} + 93 (/Users/teamcity2/buildAgent/work/11ac87a349af04d5/runtime/src/main/kotlin/kotlin/Exceptions.kt:23:44)
    at 2   library                             0x0000000109ad6b3d kfun:kotlin.RuntimeException#<init>(kotlin.String?){} + 93 (/Users/teamcity2/buildAgent/work/11ac87a349af04d5/runtime/src/main/kotlin/kotlin/Exceptions.kt:34:44)
    at 3   library                             0x0000000109ad70ad kfun:kotlin.IllegalStateException#<init>(kotlin.String?){} + 93 (/Users/teamcity2/buildAgent/work/11ac87a349af04d5/runtime/src/main/kotlin/kotlin/Exceptions.kt:70:44)
    at 4   library                             0x0000000109b9276e kfun:co.touchlab.stately.isolate.StateHolder#<init>(1:0;co.touchlab.stately.isolate.StateRunner){} + 702 (/Users/runner/work/Stately/Stately/stately-isolate/src/nativeCommonMain/kotlin/co/touchlab/stately/isolate/Platform.kt:17:19)
    at 5   library                             0x0000000109b91694 kfun:co.touchlab.stately.isolate.createState$lambda-0#internal + 324 (/Users/runner/work/Stately/Stately/stately-isolate/src/commonMain/kotlin/co/touchlab/stately/isolate/IsoState.kt:49:30)
    at 6   library                             0x0000000109b918c3 kfun:co.touchlab.stately.isolate.$createState$lambda-0$FUNCTION_REFERENCE$2.invoke#internal + 163 (/Users/runner/work/Stately/Stately/stately-isolate/src/commonMain/kotlin/co/touchlab/stately/isolate/IsoState.kt:49:28)
    at 7   library                             0x0000000109b92162 kfun:co.touchlab.stately.isolate.BackgroundStateRunner.stateRun$lambda-1#internal + 354 (/Users/runner/work/Stately/Stately/stately-isolate/src/nativeCommonMain/kotlin/co/touchlab/stately/isolate/BackgroundStateRunner.kt:15:24)
    at 8   library                             0x0000000109b7f988 _ZN6Worker19processQueueElementEb + 3624
    at 9   library                             0x0000000109b7eb46 _ZN12_GLOBAL__N_113workerRoutineEPv + 54
    at 10  libsystem_pthread.dylib             0x00007fff61167109 _pthread_start + 148
    at 11  libsystem_pthread.dylib             0x00007fff61162b8b thread_start + 15

As you can see, unlike with the unit test, the provided swift implementations are frozen when they "go into" the multiplatform code.

Currently I am at a loss. How can you let your multiplatform library depend on dependencies provided by the host app, and use these dependencies from multiple threads (using IsolateState, so all frozen memory considerations should be tackled)? Is there some trick to not freeze the class instances generated by swift?

I used the following versions:

Upvotes: 1

Views: 341

Answers (1)

Kevin Galligan
Kevin Galligan

Reputation: 17312

I haven't tested anything, but looking at your code, it seems that Swift classes report themselves as frozen to the Kotlin/Native runtime. If that is the case, then as a workaround you'll need to wrap the Swift class in something from Kotlin:

data class DependencyWrapper(val dependency:Dependency)

class SomeClient(
    someDependencyCreator: DependencyCreator
) {
    private val dependency: IsolateState<Dependency> = IsolateState {
        DependencyWrapper(someDependencyCreator.create())
    }

    //etc
}

If that works, file an issue with Stately. The purpose of checking frozen state on the mutable value is to keep people from doing obviously wrong things. It checks the value returned from someDependencyCreator.create() to make sure it's mutable. If it's frozen, IsolateState is assumed to be pointless. However, if Swift classes report themselves as frozen, which makes practical, if not logical, sense, then I think IsolateState needs a way to override the default behavior.

(I wrote IsolateState, for context)

Upvotes: 1

Related Questions