KMP-NativeCoroutines
KMP-NativeCoroutines copied to clipboard
Migrate compiler plugin to KSP
When the current compiler plugin was created KSP didn't support Kotlin/Native and a custom compiler plugin would allow us to modify classes instead of creating extension functions (which seemed a great idea at the time). However the current compiler plugin has some issues:
- It can cause recursion errors since it uses non-public APIs to get the required information
- #4
- #5
- #23
- #27
- #54
- #55
- It's currently incompatible with KSP
- #50
- Modifying the classes can cause some issues with inheritance and interface usages from Swift
- #51
- The compiler plugin is tied to a specific Kotlin version, so it requires a lot of maintenance
- #30
Using KSP would solve all these issues. The only downside being that we can no longer modify the classes and would need to generate extension properties/functions, which might not be exposed to ObjC/Swift in the same way.
Instead of generating extension properties/functions (which IIRC aren't handled well in JS), it might also be possible to generate an entirely new class that wraps that just wraps the JVM class:
// original class
class MyClass {
suspend fun myFun(): String { … }
}
// NativeCoroutines-generated
class MyClassNative(private val delegate: MyClass) {
suspend fun myFun = delegate.myFun()
fun myFunNative() =
nativeSuspend { delegate.myFun() }
}
Consumers would have to manually convert MyClass
to MyClassNative
, but they'd be able to avoid relying on extension functions.
Hmm alright didn't know about any limitations with extensions in JS.
I am actually not a big fan of such wrapper classes. Mainly because:
- all properties and functions have to be wrapped (not just the coroutines once)
- using this from Swift/ObjC/JS wouldn't be "straightforward"
I guess it would probably need some more code to get a single wrapper object as well, else you'll be creating a lot of wrapper objects in the client code.
How exactly are extension functions handled in JS? In Swift they are actually exposed as extension functions on classes, the "problem" there is with interfaces (and inline classes).
In Kotlin/JS, you need to annotate everything with @JsExport
in order for it to be visible from regular JS code.
@JsExport
's documentation says that it doesn't support extension properties 😔
On further inspection -- it looks like you can @JsExport
an extension function, just not extension properties
The way this gets exposed though is by converting the receiver to a parameter. This can cause naming collisions:
class MyClass {
suspend fun myFun(): String { … }
}
@JsExport
fun MyClass.myFunNative() = nativeSuspend { this.myFun() }
// will generate a method that can be called in JS using `myFunNative(myClassInstance)`
class MyOtherClass {
suspend fun myFun(): String { … }
}
@JsExport
fun MyOtherClass.myFunNative() = nativeSuspend { this.myFun() }
// will generate conflicting method that can be called in JS using `myFunNative(myOtherClassInstance)`
This likely means that we need to use @JsName
to avoid naming collisions, and still doesn't have the same convenient syntax of extension functions in Swift, but is still better than wrapping I suppose
This would be fantastic. I've been really enjoying this plugin but chasing down all the recursive crashes when I switch from Android to iOS builds are really frustrating. Let me know if there is anything I can do to help push this along.
If the compiler plugin is migrated to KSP we won't be able to modify any of the source code, making #63 impossible. Perhaps a better solution would be to generate the native extensions in the customer compiler (just like KSP would).