We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

A mirror without reflection for Kotlin/Multiplatform

00:00

Formal Metadata

Title
A mirror without reflection for Kotlin/Multiplatform
Title of Series
Number of Parts
542
Author
License
CC Attribution 2.0 Belgium:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Identifiers
Publisher
Release Date
Language

Content Metadata

Subject Area
Genre
Abstract
Reflection is a very powerful JVM feature that allows to create implementations of interfaces on the go, as well as exploring the type hierarchy, methods and properties of a given class. These capabilities do not exist in Kotlin/Multiplatform, so we will explore an alternative method to runtime reflection: a compile-time symbol processor to create compile time mirrors. Using Mocking as an excuse (as mocking typically needs reflection), we will explore how we can use KSP (Kotlin Symbol Processor) to circumvent the absence of reflection and generate efficient test mocks at compile time. We will also see the limitations that multiplatform brings to both the KSP generator and its associated runtime, and we will explain the tradeoff that needs to be made when using KSP, and why they are needed.
Field (computer science)CodeComputing platformMKS system of unitsMoment of inertiaRun time (program lifecycle phase)Cross-platformJava appletReflection (mathematics)Presentation of a groupComputer fileTemplate (C++)Functional (mathematics)Proxy serverDifferent (Kate Ryan album)Social classComputer programmingCompilation albumType theoryStatement (computer science)Electric generatorComputer virusObject (grammar)Source codeLambda calculusSystem callImplementationInterface (computing)outputVideoconferencingCategory of beingAndroid (robot)Goodness of fitSpacetimeField (computer science)Video gameCodeGame theoryLibrary (computing)Computer animation
CompilerCodeThermodynamischer ProzessSymbol tableData modelMaxima and minimaMomentumProcess (computing)Source codeFrustrationDeclarative programmingGenetic programmingMetadataInteractive televisionCategory of beingRegular graphFunctional (mathematics)CodeCompilerProcess (computing)Social classFormal languageSource codeRevision controlVideoconferencingConfiguration spaceCompilation albumMultiplication signPlug-in (computing)Thermodynamischer ProzessInformationArithmetic meanReflection (mathematics)Interface (computing)Cross-platformGenetic programmingType theorySystem callRun time (program lifecycle phase)NumberoutputConstructor (object-oriented programming)Computer animation
Software testingMIDICompilerPlug-in (computing)Connectivity (graph theory)Open sourceReflection (mathematics)Social classSource codeCompilerCross-platformEndliche ModelltheorieWave packetProjective planeProxy serverError messageSoftware testingGenetic programmingCodeINTEGRALThermodynamischer ProzessLibrary (computing)View (database)Functional (mathematics)HoaxInterface (computing)Category of beingLimit (category theory)IntegerArithmetic meanProduct (business)Game controllerStability theoryMultiplication signCycle (graph theory)Computer animation
Cross-platformOpen sourceLibrary (computing)Computer animation
Run time (program lifecycle phase)Cross-platformFormal verificationObject (grammar)Library (computing)Electric generatorMultiplication signComputer animation
Program flowchart
Transcript: English(auto-generated)
Hey everyone, thank you for joining me. I'm Salomon Briss, and this is a Mirror Without Reflection for Kotlin Multiplatform. So, without further ado, let's go into the subject and define what is reflection.
Reflection is a feature that allows an executing Java program to examine or introspect upon itself and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the name of all of its members. This definition is extracted from the Java documentation, and it explains that reflection basically
allows a program to introspect upon itself and look at its own method and properties. For example, in this code, I simply print every field and method that a class is declaring as accessible.
And so, this is possible thanks to these class objects that the Java runtime gives me,
and it allows me to access all fields, methods, properties, and everything that defines this class. Now, that's not the only thing Java reflection can do. Java reflection can also provide proxies.
So, for example, here I create a simple println proxy. So, here I create a proxy by saying, okay, here's a class, here's its class loader, here's a class. It's not really a class, it's an interface. So, here's an interface, and what I'm asking the runtime
to do is to give me an object that implements this interface and delegates every call to this lambda. So, basically, I'm creating an implementation of an interface at runtime.
And this is how you can use it. As you can see, it's pretty simple. All you have to do is call this createProxy method, and then you'll have an implementation of the interface created at runtime.
So, this talk is going to be about a lot of definitions, because we are going to have multiple pieces of the puzzle. So, the first piece of the puzzle was, of course, reflection. Let's go into Kotlin Multiplatform. Maybe let's not go into Kotlin Multiplatform, because you've just seen an entire presentation about what is Kotlin Multiplatform and how it works.
So, I'm not going to go into details about what is Kotlin Multiplatform and how it works, but I'm simply going to say that Kotlin Multiplatform is a way to compile Kotlin code for different targets,
namely JVM and Android on one side, JavaScript on another side, and finally Kotlin Native on the last side. Kotlin Native encompasses iOS and also other less interesting targets. Let's face it, Kotlin Native exists for the sole purpose of iOS.
So, while Kotlin JVM supports reflection, Kotlin JS and Kotlin Native do not. What is important to understand in this sentence is that reflection is not a feature of Kotlin JVM, it's a feature of the JVM that Kotlin uses and builds upon with its own reflection library.
But basically, it's a feature of the JVM, it's not a feature of the Kotlin language, and as such, it is not provided in Kotlin JS and in Kotlin Native. So, Kotlin Multiplatform being the center of Kotlin JVM, Kotlin JS, and Kotlin Native, hence do not support reflection.
So, we need to get together and find another way of doing what we usually do with reflection.
Maybe if we go back to the definition of what reflection is, we can single out this word. Reflection is a feature that allows an executing Java program. So, what this means is that reflection is a runtime feature, and we all know that what we cannot do at runtime, let's do at compile time.
What can go wrong, right? So, to do that at compile time, what reflection does at runtime, we need to add several other pieces of our puzzle.
Kotlin Poets is a Kotlin and Java API for generating Kotlin source files. So, what you could do is generate Kotlin source files by hand with templates and fill yourself like the vibe of the PHP 2000 era,
where everything was done with templating, or you could use a type API that will build the Kotlin file for you. So, I strongly encourage you to not generate your Kotlin source files by hand and use an API such as Kotlin Poets.
And here, for example, it's very simple. I create a new function called hello. I declare that it takes a name argument, and I add the statement println hello name. So, it generates basically this function.
So, that's a very important piece of our puzzle, but that's by far not the most complicated one. So, the next piece of our puzzle is KSP, and KSP stands for Kerbal Space Program.
It's a very good video game, and the goal of this video game is to build a rocket and explore space. It's an exploration game, so it's purposefully undocumented. So, there's no manual for discovery, and that's the entire game.
You need to build your rocket, send your Kerbals to space, and see what happens. So, the game is heavily based on trial and error, right? Not all Kerbals will survive the journey. You will send them to space, and not all Kerbals will come back. But when you do build a rocket and a space station in orbit, you feel a great sense of accomplishment.
And as it happens, KSP also stands for Kotlin Symbol Processing API. The goal is to build a compiler processor, a compiler code processor, and it is very, very lightly documented.
Let's be honest, there is no manual for its discovery. You will use trial and error, and you will scream at your screen, yelling at your frustration. Using KSP is a very good exercise in managing your frustration because of its light documentation.
But when you finally achieve a functional Kotlin Symbol Processor, you will, just like in the KSP video game, feel a great sense of accomplishment.
So, let's see all our pieces of the puzzle. We use KSP to instrument code at compile time. We use Kotlin Poets to generate code at compile time. And we use Kotlin Multiplatform to compile everything for all targets that Kotlin Multiplatform supports.
So, the idea here is not to allow a code to introspect upon itself at runtime, but to generate the information your code needs at compile time.
It is a lot more optimized, of course, because you don't have to introspect. All the code and all the information you need are generated for you at compile time. But it is, of course, a lot more complicated. So, how do you create a mirror generator?
So, a mirror is a class that contains reflection information of another class. So, how would you create a mirror generator? Well, creating a Symbol Processor in KSP is not that complicated. What you need to do is create a Symbol Processor class that takes a code generator and a logger as constructor inputs.
And you will use those to generate code and log when things go right or wrong.
And then you can find all symbols that are annotated by a specific annotation and then simply see what type of symbol that is and then you can continue to instrument the code starting with this.
So, as you can see here, for example, look at if the symbol annotated is a property or maybe it's just a property setter because you can inculcate, annotate, get and set properties, methods or maybe it's a function declaration
or maybe it's a class declaration and there are a lot of other things available. What's interesting in KSP and what I'm not showing here in code is that you could ask KSP to give you all symbols that are of a declaring interface, for example,
that are implementing an interface. You don't have, just like APT, you don't have to use annotations. Annotations are a very valid means of conveying the information that the code will be instrumented.
But you could, with KSP, say, OK, give me all symbols, all classes that implement this interface, for example, or give me all calls to these methods or these kind of things.
And then what you need to do after you have instrumented the code is to generate your file, the Kotlin source file that you will generate. And the good news is that, sorry, the good news is that Kotlin Poet does support KSP.
So you don't have to write a facade between the KSP code generator and the KSP code generator.
Kotlin Poet does support KSP, so it's, as you can see, pretty easy to write your very own code generator with KSP. And then what you need to do is to add your symbol processor to your Kotlin compilation toolchain with Gradle.
And as you can see, it's pretty simple, just apply the plugin. Now, the KSP plugins is versioned using its own version number and the Kotlin version number.
For example, here it's version 109 of the Kotlin symbol processor of KSP and it's version 1810 of the Kotlin language. And at the moment, because the Kotlin compiler plugin API keeps changing and is not stable and is not documented,
KSP depends on a very specific version of the Kotlin language. So you need to upgrade KSP with the same Kotlin version that you need to upgrade Kotlin.
And that's kind of a bummer because you need to wait when a new Kotlin language version comes up, you need to wait for KSP to be compatible with this new version, even for minor version.
If you use the wrong minor version, KSP will warn you that it is not compatible with this minor version. And once again, that's because the Kotlin compiler plugin API isn't stable and that KSP is using internal functions and features of the Kotlin compiler plugin.
Then, of course, you probably need to add your own runtime because when you generate code, you will probably need to provide with the generated code a runtime of your own. And then you need to declare that your KSP code processor will run on this code.
Now, as you can see, it is declared differently than with regular Kotlin dependencies because at the moment, KSP doesn't interact with the Kotlin Gradle compiler, with the Kotlin Gradle DSL,
so you have to use this weird KSP comma main metadata configuration in Gradle dependencies. So, what can you do with this technology?
Well, for the last two years, I've been developing an example because it was needed for the company I worked at, and that was mocking. So, what we have here is a class that works with Kotlin multiplatform tests
and that works with all targets of Kotlin and that generates mocks at compile time. Because mocking, in, for example, MockK or with Mockito, mocking uses the proxy reflection feature of the JVM,
which does not exist in Kotlin multiplatform. So, for example, here we say we want a view that will be mocked, so view is an interface and it will be generated by the MockKMP compiler plugin.
We want a fake, and a fake is a data class, and we want a data class that's filled with fake values, empty string, zeroed integers and all those kind of stuff. We want a controller that uses both a fake and a mock. We want to define the behavior of our mock.
For example, here I say that in the interface view in my mock, in my view mock, if I call view.render with any argument, it will return true, and I want to be able to verify that a mocked has been called with a specific data in this instance model.
So, all that and all that DSL is possible thanks to KSP and Kotlin Poet and the ability to generate code at compile time.
So, what was previously unavailable to Kotlin multiplatform because reflection wasn't available, is now available thanks to code generation. And by the way, if you're interested in this in mocking for Kotlin multiplatform,
you can use MockKMP which is a library that we built with Deezer and this library, this testing library is used in production, meaning in test production at Deezer, almost all the multiplatform tests at Deezer
uses this MockKMP library that we developed together. So, there's a problem with KSP. If we go back to the example I just gave you, this method uses this injectMocks function.
This class uses injectMocks and the fact is that injectMocks is precisely the function that is generated for this class. Because this class, we can see here, because this class has atMock annotated properties and atFake annotated properties,
then an injectMocks function will be generated by the MockKMP compiler plugin slash symbol processor. And when you load the project, the Deezer project or any project that uses this system,
well, injectMocks is an error because it hasn't been generated yet. So, IDEA will show you an error saying, okay, this function just doesn't exist. I don't know what you are talking about. So, you need to either build the project or you need to say to Gradle to generate and run KSP.
And at the moment, there is no way around that and that's because KSP has a very important limitation. It treats the source code it is instrumenting as read-only. There is no way with KSP to add properties or to modify a symbol that you are instrumenting.
So, this MyTest class, there is no way with KSP that I can add a property or that I can add an annotation and all that. And since there is no reflection in Kotlin Multiplatform, there is no way to find a class that exists,
but there is no class dot with name. So, that means that you need in your code to use the code that is generated and that code doesn't exist unless you generate it.
And that's a small price to pay to use KSP. So, why would you use KSP as opposed to writing a full-fledged Kotlin compiler plugin? First and foremost, because KSP provides a kind of stable API.
The API changes, but it follows a depreciation cycle and the API of KSP is supposed to be public, so they treat it with the respect of a public API.
And also because when you use KSP, you don't have to write a compiler plugin. Writing a compiler plugin with Kotlin just not only means that you will have to understand the inner component of the Kotlin compiler, which are absolutely not documented.
KSP is a little bit documented. The Kotlin compiler internals are just not documented. But it also means that you will have to handle compiler integration and gradle integration. So, you will have to add your own cradle plugin, you will have to add your own compiler plugin, and it becomes a very complicated endeavor.
And finally, because for code-generating use cases, KSP remains a lot simpler than writing a compiler plugin, which once again is done completely in the dark. You won't have any support if you try to write your own compiler plugin.
So, using KSP is still a very, very important tool in the grand open source library of Kotlin multiplatform project. A lot of Kotlin multiplatform libraries use KSP now,
and I encourage you to contribute to that grand library. And that's it for me. Just want to say that I represent here coding coders. We are certified for our Kotlin training. So, if you want Kotlin multiplatform training, be sure to contact us.
We have a lot of libraries that are open source. Romain with the next talk is going to present you another one of them. And we like to do our open source work with Kotlin multiplatform for every target it can compile to.
So, whether you want to contribute to Kotlin multiplatform libraries or learn how to use Kotlin multiplatform, be sure to contact us. Thank you very much. Thank you again.
We have time for one question. If someone has a question, raise your hand. Just shout it and you have to repeat the question. So, you've decided to write your own mock library. The way of making a people works with your stuff,
or is that impossible? So, the question is rather than creating a whole new library for mocking the Kotlin multiplatform, is there a way to port Mockito to Kotlin multiplatform? And the answer is definitely no. Mockito uses a lot of reflection, not just for proxy,
but for object generation and for verification. And it instruments the runtime heavily. And since there is no runtime, there is no JVM runtime in Kotlin multiplatform, there is no way to port Mockito to Kotlin multiplatform.
Now, what we've tried to do with MockAmp is to emulate the same API that Mockito provides, so that when you use MockAmp, you're at home, you're using an API that is really close, but there's no way to port Mockito itself.
Thank you very much, and have a nice FOSDEM.