10

So far I have

import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler

  MyProjectCompiler.initialize("SampleKtFileOutput")
    .packageName("com.test.sample")
    .compile(File(someFile.path))
    .result { ktSource: String -> K2JVMCompiler()
       .exec(System.out, /** arguments here?*/) }

This manually starts the compiler, but I would like to compile the resulting String from the first compiler (MyProjectCompiler which generates kotlin source) in-memory and check the result without writing to a file.

I would like to include everything on the current classpath if possible.

Preston Garno
  • 1,175
  • 10
  • 33

2 Answers2

5

I found the easiest way to do it is to use something like the code in the original question and use java.io.tmpdir. Here's a re-usable solution:

Add the kotlin compiler as a test dependency:

testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler', version: "$kotlin_version"

Wrapper for the compiler:

object JvmCompile {

  fun exe(input: File, output: File): Boolean = K2JVMCompiler().run {
    val args = K2JVMCompilerArguments().apply {
      freeArgs = listOf(input.absolutePath)
      loadBuiltInsFromDependencies = true
      destination = output.absolutePath
      classpath = System.getProperty("java.class.path")
          .split(System.getProperty("path.separator"))
          .filter {
            it.asFile().exists() && it.asFile().canRead()
          }.joinToString(":")
      noStdlib = true
      noReflect = true
      skipRuntimeVersionCheck = true
      reportPerf = true
    }
    output.deleteOnExit()
    execImpl(
        PrintingMessageCollector(
            System.out,
            MessageRenderer.WITHOUT_PATHS, true),
        Services.EMPTY,
        args)
  }.code == 0

}

Classloader for creating objects from the compiled classes:

class Initializer(private val root: File) {

  val loader = URLClassLoader(
      listOf(root.toURI().toURL()).toTypedArray(),
      this::class.java.classLoader)

  @Suppress("UNCHECKED_CAST") 
  inline fun <reified T> loadCompiledObject(clazzName: String): T? 
      = loader.loadClass(clazzName).kotlin.objectInstance as T

  @Suppress("UNCHECKED_CAST") 
  inline fun <reified T> createInstance(clazzName: String): T? 
      = loader.loadClass(clazzName).kotlin.createInstance() as T

}

Example test case:

First make a kotlin source file

MockClasswriter("""
    |
    |package com.test
    |
    |class Example : Consumer<String> {
    |  override fun accept(value: String) {
    |    println("found: '$\value'")
    |  }
    |}
    """.trimMargin("|"))
    .writeToFile(codegenOutputFile)

Make sure it compiles:

assertTrue(JvmCompile.exe(codegenOutputFile, compileOutputDir))

Load the class as interface instance

Initializer(compileOutputDir)
      .createInstance<Consumer<String>>("com.test.Example")
      ?.accept("Hello, world!")

The output will be as expected: found: 'Hello, world!'

Preston Garno
  • 1,175
  • 10
  • 33
  • I tried the code but 3 things I am missing: `MockClasswriter`,`codegenOutputFile` and `compileOutputDir` – oshai Oct 21 '18 at 23:29
0

Reading the source of the K2JVMCompiler class, it seems that the compiler only supports compilation for files. Digging deeper, it seems overcomplicated to fake the entries of org.jetbrains.kotlin.codegen.KotlinCodegenFacade static method compileCorrectFiles.

Your best guess it to use a file system to do this. A temporary RAM disk may suit your needs. (This is built-in macOS for example)

Xvolks
  • 2,065
  • 1
  • 21
  • 32
  • Is there any way to use the JSR 223 API to do this? I'm not too familiar with it – Preston Garno Sep 04 '17 at 11:46
  • JSR 223 will just let you add scripting ability to your application, it will be easier to tune your application, but it won't help you with RAM drives or compiling in RAM. – Xvolks Sep 04 '17 at 11:50
  • Thats's what the REPL runs with, no? Possible to use that to define each type that I compile one by one and then just test that way? I've been through the kotlin source but I can't really figure out how to run the REPL programatically – Preston Garno Sep 04 '17 at 11:57
  • IMHO: The REPL is the command line utility. One the other hands the JSR 233 allows you to run pre-written scripts inside your application without the REPL. I've already used it to run Beanshell and Jython scripts inside applications, this helps creating user defined behaviors for example. – Xvolks Sep 04 '17 at 12:02
  • regardless of 223 I thing that figuring out the REPL could be much easier than manual compile & classloading. I'll be testing with strings at the very least but that's not hard since I still have the classes in IR. Couldn't find anything about it anywhere though so it might take some work – Preston Garno Sep 04 '17 at 12:11