Monday, June 12, 2017

Example JVM agent in Kotlin

Oleg Shelajev wrote an excellent tutorial post on writing JVM agents. These are bits of code which run before your main() method. Why do this? It permits some interesting tricks, chiefly modifying classes as they are loaded, but also estimating the actual memory used by Java objects.

I gave this a try myself, but rather than writing my agent in Java, I wrote it in Kotlin. It was straight-forward, with only one gotcha.

AgentX.kt

@file:JvmName("AgentX")

package hm.binkley.labs.skratch.jvmagent

import java.lang.instrument.Instrumentation

fun premain(arguments: String?, instrumentation: Instrumentation) {
    println("Hello from AgentX 'premain'!")
}

fun main(args: Array<String>) {
    println("Hello from AgentX 'main'!")
}

OK, the non-gotcha. You can declare functions at the package level. This acts just like static methods in Java, with simpler syntax (no potentially artificial wrapper class to hold the static method). The two obvious examples in the above code are main() and premain().

But when calling Kotlin from Java, you use a wrapping class name in the the fully-qualified method name. My Kotlin file is named "AgentX.kt", so the default class name for Java is "AgentXKt". I'm lazy, wanted to save some typing, so I used a Kotlin package-level annotation to name the wrapping class just "AgentX".

Output

The JVM requires an absolute path to any agent jar, and I'm running Cygwin, so a little help to get a full path. Similarly, I used the Maven shade plugin to build a single uber-jar holding my own classes, and those of my dependencies (the Kotlin standard library).

$ java -javaagent:$(cygpath -m $PWD/target/skratch-0-SNAPSHOT.jar) -jar target/skratch-0-SNAPSHOT.jar
Hello from AgentX 'premain'!
Hello from AgentX 'main'!

Project is here: https://github.com/binkley/skratch.

Gotcha

Enough preamble, now the gotcha. Unlike Java, Kotlin helps you protect yourself from nulls without boilerplate code. So in premain() for the "arguments" parameter, you need to use String? rather than String as the parameter type as the JVM may pass you a null. The first time I tried the code, I didn't realize this and it blew up:

$ java -javaagent:$(cygpath -m $PWD/target/skratch-0-SNAPSHOT.jar) -cp target/skratch-0-SNAPSHOT.jar hm.binkley.labs.skratch.jvmagent.AgentX
java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386)
        at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:401)
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method hm.binkley.labs.skratch.jvmagent.AgentX.premain, parameter arguments
        at hm.binkley.labs.skratch.jvmagent.AgentX.premain(AgentX.kt)
        ... 6 more
FATAL ERROR in native method: processing of -javaagent failed

Interesting! Kotlin found the issue at runtime. It can't find it at compile time as the JVM API for "premain" is pure convention without an interface or class to inspect.

Let's try running the agent a different way. The command-line lets us pass options, and these become the "arguments" parameter:

$ java -javaagent:$(cygpath -m $PWD/target/skratch-0-SNAPSHOT.jar)= -cp target/skratch-0-SNAPSHOT.jar hm.binkley.labs.skratch.jvmagent.AgentX
Hello from AgentX 'premain'!
Hello from AgentX 'main'!

Sneaky. The mere presence of the "=" on the command line turns the "arguments" parameter from null to an empty string.

No comments: