Reputation: 705
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
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