5

In dynamic languages like JavaScript/Python, it's possible to overwrite or "modify" functions during run-time. For example, in order to modify the alert function in JS, one could do:

const _prev_alert = window.alert;
window.alert = function() {
  _prev_alert.apply(this, arguments);
  console.log("Alert function was called!");
}

This would output "Alert function was called!" to the console every time the alert function is called.

Now, obviously something like this would be impossible during runtime in Kotlin-JVM or Kotlin-Native due to their static nature. However, in regards to those same languages, is it possible to perhaps modify a non-compiled function during compile time? I don't mean pre-compiled functions from libraries, but instead functions I have written in the same project I'm developing on.

For example, let's say I have a function I wrote called get_number. Could I modify get_number to return a different number without changing how its called in main and without modifying its code directly? (Or is there a way I COULD write the original get_number so modification IS possible down the line?)

fun main(args: Array<String>) {
    println(get_number())
}

fun get_number(): Int {
    return 3
}

// Without modifying the code above, can I get main to print something besides 3?

I've been reading into Kotlin's metaprogramming with Annotations and Reflections, so perhaps those could control the compiler's behavior and overwrite get_number's code? Or is this complete lunacy and the only way something of this nature would be possible is through developing my own, separate, metaprogramming wrapper over Kotlin?

Also, just to double-clarify, this question is not about Kotlin-JS and the answer (if it exists) should be applicable to Kotlin-JVM or Native.

Salem
  • 13,516
  • 4
  • 51
  • 70
Griffort
  • 1,174
  • 1
  • 10
  • 26
  • 1
    It's a little bit unclear what the end goal is here. From an application design perspective, it's almost always more desirable to use an appropriate design pattern than to start relying on things like [dynamic proxies](https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html), [reflection](https://kotlinlang.org/docs/reference/reflection.html) or [AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming). So, can it be done? Yes. Should it be done? Probably not. – Robby Cornelissen Aug 30 '18 at 01:21
  • 1
    @RobbyCornelissen It's intent is for a port of a casual, extendable, educational game development library that uses a similar metaprogramming style with JavaScript. The modifications are mostly abstracted, just to be clear, they're simply used for quick and compatible extension developments that don't tamper with the source. This isn't some bizarre question regarding how to avoid class implementation/extension or general structure, more just a curious inquiry regarding the potential of Kotlin metaprogramming. Since you claim it could be done, perhaps you could expand upon that in an answer? – Griffort Aug 30 '18 at 02:06

1 Answers1

7

As stated in my comment: in almost all cases, it's more desirable to use an appropriate design pattern than to start relying on things like dynamic proxies, reflection, or AOP to address this kind of problem.

That being said, the question asks whether it's possible to modify Kotlin functions at compile time through meta-programming, and the answer is "Yes". To demonstrate, below is a complete example that uses AspectJ.


Project structure

I set up a small Maven-based project with the following structure:

.
├── pom.xml
└── src
    └── main
        └── kotlin
            ├── Aop.kt
            └── Main.kt

I'll reproduce the contents of all files in the sections below.


Application code

The actual application code is in the file named Main.kt, and—except for the fact that I renamed your function to be in line with Kotlin naming rules—it's identical to the code provided in your question. The getNumber() method is designed to return 3.

fun main(args: Array<String>) {
    println(getNumber())
}

fun getNumber(): Int {
    return 3
}

AOP code

The AOP-related code is in Aop.kt, and is very simple. It has an @Around advice with a point cut that matches the execution of the getNumber() function. The advice will intercept the call to the getNumber() method and return 42 (instead of 3).

import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect

@Aspect
class Aop {
    @Around("execution(* MainKt.getNumber(..))")
    fun getRealNumber(joinPoint: ProceedingJoinPoint): Any {
        return 42
    }
}

(Note how the name of the generated class for the Main.kt file is MainKt.)


POM file

The POM file puts everything together. I'm using 4 plugins:

This is the complete POM file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>x.y.z</groupId>
    <artifactId>kotlin-aop</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <kotlin.version>1.2.61</kotlin.version>
        <aspectj.version>1.9.1</aspectj.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
    </dependencies>
    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <plugins>
            <plugin>
                <artifactId>kotlin-maven-plugin</artifactId>
                <groupId>org.jetbrains.kotlin</groupId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>kapt</id>
                        <goals>
                            <goal>kapt</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>compile</id>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>com.jcabi</groupId>
                <artifactId>jcabi-maven-plugin</artifactId>
                <version>0.14.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>ajc</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>MainKt</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Building and executing

To build, as with any Maven project, you just need to run:

mvn clean package

This will build a fat JAR at the target/kotlin-aop-1.0-SNAPSHOT.jar location. This JAR can then be executed using the java command:

java -jar target/kotlin-aop-1.0-SNAPSHOT.jar

Execution then gives us the following result, demonstrating that everything worked as expected:

42

(Application was built and executed using the most recent Oracle Java 8 JDK at the time of writing—1.8.0_181)


Conclusion

As the example above demonstrates, it's certainly possible to redefine Kotlin functions, but—to reiterate my original point—in almost all cases, there are more elegant solutions available to achieve what you need.

Robby Cornelissen
  • 91,784
  • 22
  • 134
  • 156
  • 3
    I wholeheartedly disagree with your assertion that "it's more desirable to use an appropriate design pattern than to start relying on things like dynamic proxies, reflection, or AOP". In fact, I maintain that it's precisely those things that comprise an elegant solution. Please enlighten me as to other, "more elegant solutions" to the problems that AOP solves. For example, how would you implement something like domain-object security without an AOP-based solution? An example of your solution would go a long way to change my opinion. – Matthew Adams May 09 '19 at 14:14
  • 2
    Also, there is one clarification that I'd make regarding your example project given above. You have demonstrated that it's possible to _weave_ binary .class files _after_ they've been compiled by the Kotlin compiler & kapt. That is distinct from, say, using ajc to compile the whole lot in one go, like you can in Java. My interpretation of the OP's question refers to the latter, not the former. Perhaps the OP's question should have been something like "How do I compile my Kotlin files with an AOP compiler like ajc so that compilation & weaving is completed in a single step?" or similar. – Matthew Adams May 09 '19 at 14:21