Hey, everyone. Thank you for joining me. I'm Solomon Briss and this is A Mirror Without
Reflection for Cartoon Multi-Platform. 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
printer and 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 create proxy method and then
you'll have an interface, 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. 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 error
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. Okay. 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 Kerbal to space and
see what happened. So, the game is heavily based on trial and error. Not all Kerbal will
survive the journey. You will send them to space and not all Kerbal 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
codes at compile time. We use Kotlin Poets to generate codes 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 a constructor input. And you will use those
to, well, generate code and log when things go right or wrong. And then you can get, 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, in Kotlin, 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 simple,
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, okay, give me
all symbols, all classes that implement this interface, for example, or give me all codes
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 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 Kotlin, to
your Kotlin compilation tool chain 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 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 function 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 word, KSP common main metadata configuration
in Gradle dependencies. So what can you do with this technology? Well, for the last two
years, I have 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 multi-platform
test and that works with all targets of Kotlin and that generates mocks at compile time because
mocking in, for example, mock K or with mockito, mocking uses the proxy reflection feature of
the JVM which does not exist in Kotlin multi-platform. 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 mock AMP compiler
plugin. We want a fake and a fake is a data class and we want a data class that 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 Poets and the ability to generate code at compile
time. So what was previously unavailable to Kotlin multi-platform 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 multi-platform you can use mock AMP 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 multi-platform tests at Deezer uses this mock
AMP 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 inject mocks function, this class
uses inject mocks. And the fact is that inject mocks 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 at fake annotated properties, then an inject mocks function will be generated
by the mock AMP compiler plug-in slash symbol processor. And when you load the project,
the Deezer project or any project that uses this system, well, inject mock 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, 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 my test class, there's no way with KSP that I can add a property or 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's 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 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 components 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 gradle 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, 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 lots 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 too. 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.
Shout it, and you have to repeat the question. Yes, so you've decided to write your own library.
There was any way of making Mokey to work with yours, but if you were, is that possible?
So the question is, rather than using, rather than creating a whole new library for marking
a Kotlin multiplatform, is there a way to put Mokey to Kotlin multiplatform? And the
answer is definitely no. Mokey to uses a lot of reflection, just 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 Mokey to Kotlin multiplatform. Now, what we've tried to do with Mokey and
P is to emulate the same API that Mokey provides so that when you use Mokey and P, you're at
home, you're using an API that is really close, but there's no way to port Mokey to itself.
Thank you very much, and have a nice first time.