solution with MOKO KSwift plugin

Hello! Aleksey Mikhailov, technical director of IceRock Development, is in touch. In the last article, I talked about what problems there are in working with Kotlin from the side of Swift, and considered ways to solve them. In this article I will dwell on the third solution that we use in practice. These are Gradle plugins, namely a plugin that we developed ourselves.

In this article, I will cover:

  1. Gradle plugin we made

  2. Klibs and what they eat with

  3. What is an intermediate representation

  4. What to do with the intermediate representation

  5. How to solve intermediate representation problems

  6. Summary: what to do to make it more convenient and easier for you to work with what Kotlin generated from the Swift side

Gradle plugin we made

it MOKO KSwift. Its main feature is that it generates Swift code based on the Kotlin intermediate representation. And he takes an intermediate representation already from klibs, which fall into the linking stage during the Kotlin compilation process just at the moment when the binary for iOS is created. The plugin itself has potential for expansion. It has two generators made by us at once:

  • support for sealed interfaces and sealed classes with the creation of swift enums from them;

  • support for Kotlin extensions, with the creation of normal extensions from them by Swift.

An example of how generators work

In the image above, you can see an example: we had a sealed interface, we got an enum, and everything that was required was automatically generated. You can handle this in switch, then everything will be without the default branch.

I’ll show you on the diagram. The beginning of the chain is exactly the same: Xcode → Gradle → compiler. But further, it is at the linking stage, when the Kotlin code is already compiled, that we simply have a set of klibs. All these klibs are already transferred to separate tasks, again by Kotlin itself for linking.

At the moment the linking is completed, a trigger is added to call our Gradle plugin. First of all, it reads all the klibs that were involved in the linking. And when reading all klibs, it learns about all those types, classes, methods in general … – in general, about everything that you have got into the binary. And when he went through all this, the plugin finds exactly what it needs (sealeds or extensions), and based on this information it already generates code using the SwiftPoet library.

When the generation ends and Gradle stops working, not only the Kotlin framework itself, but also the generated Swift files should already be connected to Xcode. They will also need to be added to Xcode, and all this will participate in the final compilation.

Klibs and what they eat with

I mentioned klibs many times, it’s time to tell what they are.

Klib is short for Kotlin library, that is, it is a Kotlin library. The Kotlin/Native compiler only works with klibs. All that the compiler needs to build the final binary, no matter if it is a framework or an application, is just klibs. They are also divided into targets, so each target has its own klib. And every klib has an intermediate representation.

The intermediate view contains everything you have written in the code, except for the comments. Therefore, from klibs we can learn everything important for the program. And for reading klibs, we use the official library from JetBrains. They support it themselves and use it in the compiler infrastructure, and you can connect it to your plugins and use it, which we did. Therefore, there will be no such situation when, after the release of a new version of Kotlin, we have to redo the reading of klibs: this is done by JetBrains themselves.

What is an intermediate representation

I will talk about this concept briefly, because if you go into details, then there will be more questions than answers. You can see article on the NSU website about how all this was invented at the time of the beginning of Kotlin / Native and why.

For example, we have Kotlin code (see the image above), and during the compilation process, we get just an intermediate representation, or intermediate representation (IR) (see the example below). This is a complete intermediate representation of the code from the previous image. We are interested in the plugin exactly what we see about the function itself, that is, its signature. Here we know its name, its scope, arguments, return value, types, annotations and generics, if any. That is, there is generally everything that affects the code.

FUN name:f visibility:public modality:FINAL <>
(x:kotlin.Boolean) returnType:kotlin.Int
	VALUE_PARAMETER name:x index:0 type:kotlin.Boolean
	     VAR name:a type:<root>.A [val]
	          WHEN type=<root>.A origin=IF
	                    if: GET_VAR 'x: kotlin.Boolean'
	                    then: BLOCK type=<root>.A
	                         CONSTRUCTOR _CALL 'public constructor <init>
(arg: kotlin.Int) [primary]' type=<root>.A
	                              arg: CONST Int type=kotlin.Int value=5
	                    if: CONST Boolean type=kotlin.Boolean value=true
	                    then: BLOCK type=<root>.A origin=null
	                         CONSTRUCTOR_CALL ‘public constructor <init>
(arg: kotlin.Int) [primary]' type=<root>.A
	                              arg: CONST Int type=kotlin.Int value=6
	     CALL ‘public final fun print (): kotlin.Unit’
	          $this: GET_VAR ‘val a: <root>.A [val]' type=<root>.A
	     RETURN type=kotlin.Nothing
	          CONST Int type=kotlin.Int value=0

What is IR

Personally, I dealt with IR by what I managed to google, but I managed to google not very much. There is article, which tells where everything came from and what it looks like, but no more. The debugger helped the most.

In general, klibs can contain everything you need. There is library from JetBrains, which we use, and you can use it too. I picked it up and transferred klib there, which I compiled myself, and looked through the debugger to see what it gives me. There was a lot of content, and through the debugger I learned most of it.

How exactly to generate new code, of course, you won’t learn much. I tried to make a compiler plugin that will do the same thing as KMP-NativeCoroutines, but it’s still more complicated. You need to know what to generate and where to add something in the intermediate representation itself. This helped me a lot this performance and this webinar (in English) about how to make your own compiler plugin. They explain it with several examples: somewhere it’s just code generation, somewhere it’s a change in the written code.

These are the main available materials that I have seen. As I was told, in JetBrains, when developing features related to compiler plugins, teams consult among themselves on how best to implement this.

A stable API for compiler plugins was planned for version 1.7, but has now been delayed to version 1.8-1.9. In general, we still have to wait another two years for a stable API so that Compose does not break between new versions of Kotlin and JetBrains and can write documentation. At the moment, they are not even trying to write documentation, but simply pass on solutions to each other, as they say, by word of mouth.

Also you can look recording of speech by Ilmir Usmanov from JetBrains, which directly handles all this in the compiler. He talks in more detail about the intermediate representation, as well as why it came to this and how it will develop further.

Already, the intermediate representation is used not only in Kotlin/Native, but also in Kotlin/JS. And even in Kotlin / JVM it comes – through the IR compiler, which has been talked about since version 1.5 and on which Compose relies. All this thanks to the intermediate representation, which is common to all Kotlin targets.

What to do with the intermediate representation

An intermediate representation is needed in order to, knowing in general all the information about the Kotlin code that we wrote, to generate the most suitable Swift code that simplifies the work of Swift for iOS developers.

There is one problem with this approach that has not yet been solved, but will be solved in the future. After compiling the Kotlin code into klibs, everything goes to the linking stage. At the linking stage, there is an additional procedure that affects how we will see the Kotlin code from the Swift side. It’s called mangling.

What is the point: since we have packages in Kotlin, we can write some user class there in the Feature1 package and in the Feature2 package. And depending on what import we wrote in our Kotlin file, either one or the other class will be shown. But when we compile all this using Kotlin / Native and look from the side of Swift, then there will be no packages there. There will simply be our entire framework called Shared, for example, and in it everything that was in Kotlin, already without packages. And so this user here, which was with the same name in different packages, will become user and user_ here, with an underscore at the end.

The same will happen with functions within interfaces that overlap in signature. Moreover, the Kotlin signatures do not take into account the names of the arguments, the type has an influence there, and the opposite is true in the Objective-C and Swift signatures. This is the language difference. Due to the fact that these underscores are added precisely at the linking stage, it may turn out that we will generate a new Swift code that will rely on the fact that some kind of extension has been added to the user class. And after linking, it turns out that Kotlin / Native no longer called this class user, but user_, and the code will be invalid.

How to solve intermediate representation problems

So far there are two options.

First option I saw in the Jetpack Compose compiler plugin commits. This is, in fact, using the internals of the Kotlin / Native compiler itself, exactly the logic that is responsible for adding these underscores. Call her, ask her what the name will be there, and get an answer. But so far there has been no way to test how viable this idea is.

And the second option – do as with Sourcery. When we already know that the generated header contains such and such names, we can try to find the classes we need already in the header itself, and understand whether we need to add an underscore there and how many of them there will be or not.

Summary: what to do to make it more convenient and easier for you to work with what Kotlin generated from the Swift side

  1. If you use Flow and want to use it from the Swift side, hook the KMP-NativeCoroutines plugin. It will definitely make things easier for you.

  2. If you want to use sealed classes and sealed interfaces, then hook MOKO KSwift.

  3. If you want to use extensions for platform classes like UILabel, UITextView and others or for interfaces, then hook MOKO KSwift or Sourcery. Either one solves the problem.

  4. If you want a completely custom template, then you can use Sourcery if you don’t need information about Kotlin classes and signatures at all. If information about the original Kotlin code is needed, then you will have to make your own version of the feature for MOKO KSwift.

  5. Well, you can try not to use some of the features in the public API, for example, the same abstract classes. This will make it easier and more reliable.

Do you have any questions? Write in the comments.

We write other articles about KMM, subscribe to our telegram channelto be the first to know about them.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *