⬆️ ⬇️

Auto compiler objects

The article is based on the Kotlin version "1.1-04".



This article is devoted to the description of the automatic class generation mechanism by the Kotlin compiler for anonymous block of user code. It is described in which cases automatic classes are created, where they are located, how they are implemented and used.



In Java there is only one way to create a code element - it is a lambda expression that can only implement a functional interface (for more details, see the Java documentation). In Kotlin there are several different ways to describe an object containing custom code.



It is a well-known feature of Kotlin that it is very easy to implement code that allows writing such constructs:



 fun Action( cb:()->Unit ) { cb() } fun Test() { Action{ //        } } 


How is this implemented and what is the risk of using such a code in a program?



PS: The original version of the article contained a logical error and incorrect conclusions. I apologize to everyone for this fact. This article has been revised and supplemented with additional data.





Auto compiler objects



The description of a separate code in the JVM not possible, you can operate only with classes. In some situations, the Kotlin compiler automatically creates classes in which the specified code will be executed, and in some cases calls it directly. In different cases, classes of different types will be created.



Kotlin allows you to implement constructs with anonymous code using different syntaxes. The examples below are divided by their method of use in the test programs, and not by the way they are implemented by the compiler.

In fact, almost all of the listed syntax constructs are implemented in the same way.



Anonymous function



A block of code with arbitrary parameters and a return value, which can be called directly or passed as a parameter to a function.



An example of an anonymous function
 fun Action(cb : () -> Int) //  = println(cb()) class BB { fun F() = 1133 fun M() { Action(this::F) //     } } fun statMethod() = 1111 fun Test() { val cb = { 1122 } //     cb() //      Action(cb) //     Action { 1133 } //        Action(::statMethod) //      } 


The constructions " ::statMethod " and " this::F " are used to facilitate the syntax and are completely equivalent to ::statMethod function in anonymous code:



  Action{ statMethod() } //  Action(::statMethod) Action{ F() } //  Action(this::F) 


Anonymous object function



A block of code that runs in the context of an object instance, i.e. will have a field " this ", referring to the object for which this code is called.



In the implementation of this approach in Kotlin this object is emulated to simplify the syntax, but in fact an implicitly passed object instance is used. It should be noted that despite the presence of the this and accessing the class elements without specifying an explicit object, the executed code is not part of the class and, accordingly, does not have access to its protected elements.



An example of an anonymous function object
 import java.awt.event.ActionEvent import java.awt.event.ActionListener fun <T> cAction(v : T, cb : T.() -> Int) //       = println(v.cb()) class BB { fun F() = 1133 //   fun M() { cAction(this, BB::F) //       } } fun Test() { cAction("text", String::length) //       } 


Lambda function



A block implemented in an automatically created class, a successor of a functional interface, for a single method of this interface.



Example lambda function
 fun add(i : ActionListener) {} //   fun Test() { add(ActionListener { }) //  ,   } 


Anonymous class



Automatically created anonymous class, with the possibility of inheritance from the specified class.



Anonymous class example
 fun add(i : ActionListener) {} //   fun Test() { add(object : ActionListener {//  ,   override fun actionPerformed(e : ActionEvent?) {} }) } 




External automatic classes



Let's put together all the above described methods of declaring automatic objects into one source file and compile it.



Full text of the test code
 import java.awt.event.ActionEvent import java.awt.event.ActionListener fun add(i : ActionListener) {} fun <T> cAction(v : T, cb : T.() -> Int) = println(v.cb()) fun Action(cb : () -> Int) = println(cb()) fun statMethod() = 1111 class BB { fun F() = 1133 fun M() { //jm/test/ktest/BB$M$1.INSTANCE cAction(this, BB::F) //jm/test/ktest/BB$M$2 Action(this::F) //jm/test/ktest/BB$M$3 Action{F()} } } fun Test() { //jm/test/ktest/KMainKt$Test$1 add(object : ActionListener { override fun actionPerformed(e : ActionEvent?) {} }) //jm/test/ktest/KMainKt$Test$2.INSTANCE add(ActionListener { e-> }) //jm/test/ktest/KMainKt$Test$cb$1.INSTANCE val cb = { 1122 } cb() Action(cb) //jm/test/ktest/KMainKt$Test$3.INSTANCE Action { 1133 } //jm/test/ktest/KMainKt$Test$4.INSTANCE Action(::statMethod) //jm/test/ktest/KMainKt$Test$5.INSTANCE cAction("text", String::length) } 


If you look in the directory where the compiler saved the created classes, you can see the following files:



File nameSize, byte
BB.class1150
KMainKt.class3169
BB $ M $ 1.class1710
BB $ M $ 2.class1379
BB $ M $ 3.class1025
KMainKt $ Test $ 1.class973
KMainKt $ Test $ 2.class910
KMainKt $ Test $ 3.class1029
KMainKt $ Test $ 4.class1450
KMainKt $ Test $ 5.class1222
KMainKt $ Test $ cb $ 1.class1035


It is clear where the first two files came from - these are the classes that are described in our program:



  • The file " BB.class " is a class declared in the text.
  • The file " KMainKt.class " is the main class of the Kotlin program, which includes all data and methods that are not included in the classes. The name of this file consists of the file name of the source program (in our case it is β€œ kMain.kt ”) and the suffix β€œ Kt ”. In our example, all the methods used are placed in this class, such as β€œ Action ”, β€œ cAction ”, etc.


Where did the rest of the files come from?



These files contain the code of classes that the compiler generated automatically. In the program text above, in the comments, the names of the generated classes are indicated, in accordance with the place of their declaration in the program.



ATTENTION: The compiler creates a new CLASS each time, rather than a new instance of some class. Those. each time the code is used, a new class is created in the program, which is used in the only place of the program!



If you look at the source text, you can pay attention to the fact that the text, it would seem, not related to the creation of temporary objects, also leads to the generation of classes.



  Action(::statMethod) // jm/test/ktest/KMainKt$Test$4.INSTANCE cAction("text", String::length) // jm/test/ktest/KMainKt$Test$5.INSTANCE 


This is explained by the fact that in order to ensure the syntax used in the program text, the compiler simply creates wrapper classes that call the specified methods.



The table above shows that the generated class files are quite large, large, to the size of the code that is executed in blocks of code for which they were created. Despite the fact that in the example the volume of the actual useful code is almost zero, the volume of the created classes is almost 4 times the size of the main file of the program " KMainKt.class ". The reason for this is that a lot of additional information is stored in the class files and, if the amount of class code is small, the amount of useful information in the class file will be a small part of it.



If the code that executes the automatic classes is large, then the volume of the resulting files can be ignored. If, in most cases, the code for the automatic class is very small, then it is rather difficult to call the creation of 1-1.5Kb files for each anonymous block described in the program effective or even acceptable.



Automatically generated classes directly affect the size of the program in assembled form. With the active use of anonymous code in the program, its size can grow rapidly. In many cases, program size can play a significant role. This may concern both mobile or WEB applications, as well as applications intended for execution on personal computers.



Duplication. Optimization?



We will conduct another experiment: check the ability of Kotlin to optimize automatic classes.



If we consider all the places where the compiler automatically creates classes in our example, then we can assume that there is an optimization related to the fact that classes will be created only for blocks with unique code, and in all other cases objects of already existing classes will be used.



To test the optimization possibilities, simply duplicate two lines of code. One, using custom code, and the second, using fully automatic code.



 Action{} Action{} Action( ::statMethod ) Action( ::statMethod ) 


Ideally, Kotlin optimizes the use of the automatic class for both the first group and the second. In a good version - only for the second. Checking the uniqueness of the user code can be very difficult to implement, so we will only rely on the elementary optimization of classes created by the compiler completely in automatic mode.



I will leave the modification of the example, its compilation and viewing of the class files as an independent exercise and proceed directly to the result.



A new unique class will be created for each code description location!



Unfortunately, in the current version of Kotlin (version 1.1-M04 is used), no optimization related to the generation of automatic classes does not exist and any use of the code will create a new class.





How to deal with the "extra" size?



In Kotlin programs, the use of anonymous code is very widespread and, without doubt, the ease of its description and use is one of the principal advantages of this language. Due to the abundance of anonymous code applications, the problem of increasing the size of a program due to the generation of classes for each code fragment can become a very big problem.



There are two ways to deal with the storage of redundant information in a file with the collected application.



The first is to use various tools that are designed to remove service information from the compiled files for the JVM .



One of the very interesting tools of this group is ProGuard . This is a free tool that allows you to repack a JAR archive with the application and remove all service information from it. In addition to the simple removal of proprietary information, it does a great job of optimizing the code for the classes. As a bonus, this tool can be used as an " Obfuscator ", i.e. a tool that makes it difficult to recover application logic from its code.

This is a powerful tool, but its review goes beyond the goals of this article.



The second way (which can be combined with the first) is to use the features of the Kotlin compiler. This feature is discussed in the next section.



Internal Auto Classes



Let's make experiment with our example, having changed the description of two methods.



 inline fun <T> cAction(v : T, cb : T.() -> Int) = println(v.cb()) inline fun Action(cb : () -> Int) = println(cb()) 


In this case, we added the inline to the write function.



After compiling the program, the list of files created by the compiler for automatic classes was reduced to three: β€œ KMainKt$Test$1.class ”, β€œ KMainKt$Test$2.class ” and β€œ KMainKt$Test$cb$1.class ”. Those. there are only those classes that are not passed as parameters to the Action and cAction functions. The size of the file " KMainKt.class " increased by about 200 bytes.



Where are the other classes?



The code of all functions declared with the inline modifier was inserted directly into the place where the functions were called. In addition, the compiler has created separate functions with the same name. The syntax of the language will not allow using the declared functions. when you try to call it, its code will be placed in the place of use, but its static copy is created to provide access to this function through reflection .



How it works?



The block of anonymous code in Kotlin, depending on where it is used, can be implemented either with the help of automatic objects, or placed directly at the call site. In the first case, an object of a certain class will be created, the description of which will be in this section, and in the second case such a block as an object does not exist at all.



When creating automatic classes, Kotlin can create two types of objects:



  1. An object of some class, a successor of a user class.
  2. An object representing a special class to call a block of code.


Custom Type Objects



The first method is used when creating anonymous classes, lambda functions and implementing interfaces using delegates. In this case, a class will be created, the successor of the specified (explicitly or implicitly) user class and the code block will be placed inside one of the methods of this class.



Code
 fun add(i : ActionListener) {} add(object : ActionListener { override fun actionPerformed(e : ActionEvent?) {} }) add(ActionListener { e -> }) 


If an automatic class is created for an anonymous class, then an object for it will be built in the place of the declaration and passed as a parameter to the destination. If a class is created to implement a functional interface, then if it does not capture local variables, the previously created singleton of this class will be used. If local variables are captured, a new object will be built, to which references to all captured variables will be passed to the constructor as parameters.



The implementation and method of calling such classes looks like this.
 //   : add(ActionListener { e -> }) class AutoClassForCase1 : ActionListener { override fun actionPerformed(e : ActionEvent?) { //        - } companion object { @JvmField val INSTANCE = AutoClassForCase1() } } add( AutoClassForCase1.INSTANCE ) //     


Capture Local Variables



Kotlin , in contrast to Java , allows anonymous code to capture local variables of any type (in Java they must be " effectively final "), so the transfer of captured variables to an automatic class occurs through the special class " Ref<T> ". As a result of this approach, if a local variable was captured by at least one block of code, then it automatically turns into a link at the place of its use, and all references to it are made using this link. This method introduces some additional overhead in comparison with the method used in Java , but allows you to change the values ​​of variables.



Local variable capture code

When blocking a local variable, the code is:



  fun Test() { val local = 10 Action{ local+1 } local + 2 } 


turns into this:



  fun Test() { val local = Ref<Int>(10) //      Action{ local.value + 1 } //        local.value + 2 //     local.value = null //       } 


At the same time, you need to pay attention to the following points:



  1. The value of the captured variable is accessed from the code block through the object of the automatic class, which contains the same object as the link in the function body. This object is created and passed to the constructor object of the automatic class in the place where the code block is declared.
  2. In case the code captures any variables from the current context, not a singleton is used as an object of the automatic class, but a new object is created. This happens in order to be able to call the constructor and pass references to the captured variables.
  3. At the end of the function, Kotlin automatically generates the object cleanup code for all captured variables, so after the function completes, the number of references to local objects is always equal to the number of automatic objects created.


Objects for code block



The example described just above for calling an anonymous block of code is implemented by the compiler in approximately the following way:



Implementing an anonymous block of code
  //       class AutoClassForMethod : Function0<Int> { //       private val local : Ref<Int> //       constructor( l:Ref<Int> ) { local.value = l.value } //     fun invoke() : Int { //      } } //          Action( AutoClassForMethod(local) ) 


In this example, for simplicity, I lowered the layer of intermediate parameter conversions from Any (actually Object because this part of the Kotlin code is written in Java ) to specific types since it does not affect understanding.



All Kotlin code blocks always have the type FunctionX<Ret[,Param1[,...]]> , where FunctionX is a set of template classes that implement a method call with an arbitrary number of parameters. The last template type is the return type, the first is the type of the first parameter, and so on. The symbol in the class name I used to denote the number of method parameters. Those. the method described in Kotlin as ()->Unit will have the actual type Function0<Unit> , and the method described as (Int,String)->Double type Function2<Int,String,Double> , etc.



FunctionX templates are described in the Kotlin library " kotlin-runtime.jar " and can be used in your program. The syntax for describing blocks of code using the "standard" syntax in the form ()->Unit and using the template Function0<Unit> absolutely equivalent. You can verify this by replacing the function declaration in the original example.



 fun <T> cAction(v : T, cb : Function1<T,Int>) = println(cb.invoke(v)) fun Action(cb : Function0<Int>) = println(cb.invoke()) 


The program will be collected and executed as before.

Please note that the syntax " T.() -> Int " used earlier was completely transparently replaced by the use of a template with passing an extra type corresponding to the type of object for which the method will be called. Those. the difference between calling the static function and the function of the object, only in the extra parameter " this ".



What is this useful?



From the point of view of a simple user, the available syntax of the language is a little useful here.



But, from the point of view of a developer of a library, or a person implementing a complex control algorithm using dynamic lists of methods, knowing how automated objects are organized and how to handle blocks of code can be extremely useful.



β€” .





inline-



inline . .



inline , . Those. " ++ ", . inline- Kotlin - inline- " ++ ". , .



inline , , . , . Those. " inline " .



inline- inline-



, " inline " , . , " inline ", ..



: inline-
 class Holder { var cb : (()->Unit)? = null inline fun setup( cb:()->Unit ) { this.cb = cb // :    inline- } } fun Test() { Holder().setup{ } } 


inline - inline . , , - . , , inline-, , " noinline ".



noinline
 class Holder { var cb : (()->Unit)? = null inline fun setup( cb:()->Unit, noinline delayed:(()->Unit)?=null ) { this.cb = delayed cb() CallCodeWithDelay() } fun DelayPassed() = cb?.invoke() } 




++ inline . Kotlin . , , . , . " inline " .



, inline , . , , Kotlin , Lint , , inline . , , .



 @Suppress("NOTHING_TO_INLINE") inline fun SetupControl( ctl: Label ) { ctl.text = "Label" ctl.setSize( 100,100 ) } fun Test() { SetupControl( Label() ) } 


SetupControl "" , .





" ++ ", , , inline .



, Kotlin return . , inline , .



 fun Test1() : Int { Action { return 3 } } 


inline , , return , , . Those. , " Test ". return β€” .





, inline , , .



JVM , , . , , ..



:
 fun <T,P> templateTest(value : T) : P { if ( value is P ) // :    return value } templateTest<String,String>("text") 


inline- " reified ".



 inline fun <T,reified P> templateTest(value : T) : P { if ( value is P ) //         return value } 


. , templateTest , . , Kotlin reified ,

, , , refrection , , reified , .



 This function has a reified type parameter and thus can only be inlined at compilation time, not called directly 


inline-



, inline , .

Kotlin . . .





: ,



inline-
  • .. , .



  • , , . , , , Β«KotlinΒ», Β«RefΒ». , . , . inline- , ..



  • , , .



  • inline- .


inline-
  • , , "".



  • : , , ..



  • , . , .

    , .



  • JVM Java 7 Java 8 65534 (.. 65536 , ). inline- , . , .


, inline- . Kotlin , β€” .



, .

inline-, :



  • , , , , . -, .



  • , (.. -inline- ). inline- , , , , , . Those. , inline , β€” ..



  • . , .



  • , . β€” . .


')

Source: https://habr.com/ru/post/319692/



All Articles