📜 ⬆️ ⬇️

Simple compilation of Scala code at runtime.

So let's get down to it. I love Scala not only for the fact that it allows you to write two times less code than in Java. Clear and expressive code. In addition, on Scala, you can do things generally inaccessible to Java developers: generics of a higher order, learn types of generics at runtime using manifests.

One of the things that will be discussed is the compilation of Scala code during program execution. This may be necessary when you want to execute the code coming from a remote source or write a self-modifying program, an analogue of the eval function in JS.

The most important difference between Scala and Java is that its compiler is not a native program, like javac, but comes in the form of a jar file , that is, it is a simple library that no one bothers to call anytime to get JVM bytecode from source codes.

The only drawback of this method is that the Scala Compiler API does not have a very convenient interface for simple tasks: when you just need to take the source code and get a pack of compiled classes from it, using which it will be possible to create instances through the Java Reflection API. That is why I decided once to create a simple wrapper for compilation, about which there will be a story. I collected the code for it bit by bit from Stack Overflow and thematic sites.
')
The interface for compiling Scala code is provided by the scala.tools.nsc.Global class. To invoke the compilation, you must create an instance of this class, then an instance of the nested class Run and run the compileSources method:

val compiler = new Global ( settings, reporter ) <br/>
new compiler. Run ( ) compileSources ( sources )

Everything is simple enough, but for compilation you need settings (settings), a reporter that will collect compilation errors and the sources themselves.

The settings are of the type scala.tools.nsc.Settings , in the settings for my needs two parameters were enough: usejavacp, which determines that the compiler should use the current Java classpath (this is useful because the source code you want to compile may contain links to external classes from environment), and the second parameter outputDirs. The fact is that the compiler, which is in the Global class, can add compiled classes only in the form of files on the disk. I did not want to be attached to the disk, so I found a workaround, quite popular, by the way - to use a virtual directory in memory.

As a result, my code for settings looks like this:

val settings = new Settings<br/>
<br/>
settings. usejavacp . value = true <br/>
<br/>
val directory = new VirtualDirectory ( "(memory)" , None ) <br/>
settings. outputDirs . setSingleOutput ( directory )

Next you need to configure the reporter. I used scala.tools.nsc.reporters.StoreReporter :

val reporter = new StoreReporter ( )

It saves all the problems with the compilation within itself, and errors can be analyzed as soon as the compilation is over.

And the last thing to do is to prepare the source. Everything is not so simple and the compiler accepts a list of instances of scala.tools.nsc.util.SourceFile , while we, as you remember, have only a line with the source code. Thanks to Scala's short syntax, the conversion is simple:

val sources = List ( new BatchSourceFile ( "<source>" , source ) )

We create one file where we put the source code.

Now everything is ready, we can call the compiler and the first thing to do is to check: how did our sources compile:

if ( reporter. hasErrors ) { <br/>
throw new CompilationFailedException ( source,<br/>
reporter. infos . map ( info = > ( info. pos . line , info. msg ) ) ) <br/>
}

For simplicity, in case of errors (and not warnings), I throw an exception that the compilation is unsuccessful, this exception:

class CompilationFailedException ( val programme: String , val messages: Iterable [ ( Int, String ) ] ) <br/>
extends Exception ( messages. map ( message = > message._1 + ". " + message._2 ) . mkString ( "n" ) )

If everything went well, it means that we already have the collected classes, it remains to somehow download them from the same virtual folder. Use the built-in class scala.tools.nsc.interpreter.AbstractFileClassLoader :

val classLoader = new AbstractFileClassLoader ( directory, this . getClass . getClassLoader ( ) )

AbstractFileClassLoader will create a new one with each new compilation, so that if we want to recompile any class, it will be successfully loaded a second time, without conflict with its previous incarnation.

After creating a class loader, you need to go through the files of the virtual folder and load the classes from the files inside it. In Scala, a single source file can contain several classes that are not necessarily nested inside each other; when compiling into JVM bytecode, such several classes will be decomposed into several files, with the nested classes lying in separate classes named ClassName $ InnerClassName.class. I used this code to compile implementations of a single interface, so that I always knew what to expect from the resulting classes. By the way, therefore, the nested classes, which strived to stand on a par with the main ones, strongly interfered with me, when loading, I skip them if there is a $ sign in the name:

for ( classFile < - directory ; if ( ! classFile. name . contains ( '$' ) ) ) yield { <br/>
<br/>
val path = classFile. path <br/>
val fullQualifiedName = path. substring ( path. indexOf ( '/' ) + 1 , path. lastIndexOf ( '.' ) ) . replace ( "/" , "." ) <br/>
<br/>
classLoader. loadClass ( fullQualifiedName ) <br/>
}

To load classes, you need to get their full names (fully named), which include the package structure. To recreate this name, I used the path manipulation in the folder.

Well, now our classes are loaded and the construction above returns a list of these classes.

That's all. So simple, it seems, although a non-trivial task, could only be accomplished by writing a page of code.

The full text of the resulting class:

/*!# Compiler<br/>
<br/>
This class is a wrapper over Scala Compiler API<br/>
which has simple interface just accepting the source code string.<br/>
<br/>
Compiles the source code assuming that it is a .scala source file content.<br/>
It used a classpath of the environment that called the `Compiler` class.<br/>
*/
<br/>
<br/>
import tools. nsc . { Global, Settings } <br/>
import tools.nsc.io._ <br/>
import tools.nsc.reporters.StoreReporter <br/>
import tools.nsc.interpreter.AbstractFileClassLoader <br/>
import tools.nsc.util._ <br/>
<br/>
class Compiler { <br/>
<br/>
def compile ( source: String ) : Iterable [ Class [ _ ] ] = { <br/>
<br/>
// prepare the code you want to compile <br/>
val sources = List ( new BatchSourceFile ( "<source>" , source ) ) <br/>
<br/>
// Setting the compiler settings <br/>
val settings = new Settings<br/>
<br/>
/*! Take classpath from currently running scala environment. */ <br/>
settings. usejavacp . value = true <br/>
<br/>
/*! Save class files for compiled classes into a virtual directory in memory. */ <br/>
val directory = new VirtualDirectory ( "(memory)" , None ) <br/>
settings. outputDirs . setSingleOutput ( directory ) <br/>
<br/>
val reporter = new StoreReporter ( ) <br/>
val compiler = new Global ( settings, reporter ) <br/>
new compiler. Run ( ) compileSources ( sources ) <br/>
<br/>
/*! After the compilation if errors occured, `CompilationFailedException`<br/>
is being thrown with a detailed message. */
<br/>
if ( reporter. hasErrors ) { <br/>
throw new CompilationFailedException ( source,<br/>
reporter. infos . map ( info = > ( info. pos . line , info. msg ) ) ) <br/>
} <br/>
<br/>
/*! Each time new `AbstractFileClassLoader` is created for loading classes<br/>
it gives an opportunity to treat same name classes loading well.<br/>
*/
<br/>
// Loading new compiled classes <br/>
val classLoader = new AbstractFileClassLoader ( directory, this . getClass . getClassLoader ( ) ) <br/>
<br/>
/*! When classes are loading inner classes are being skipped. */ <br/>
for ( classFile < - directory ; if ( ! classFile. name . contains ( '$' ) ) ) yield { <br/>
<br/>
/*! Each file name is being constructed from a path in the virtual directory. */ <br/>
val path = classFile. path <br/>
val fullQualifiedName = path. substring ( path. indexOf ( '/' ) + 1 ,path. lastIndexOf ( '.' ) ) . replace ( "/" , "." ) <br/>
<br/>
Console. println ( fullQualifiedName ) <br/>
<br/>
/*! Loaded classes are collecting into a returning collection with `yield`. */ <br/>
classLoader. loadClass ( fullQualifiedName ) <br/>
} <br/>
} <br/>
} <br/>
<br/>
/*!### Compilation exception<br/>
<br/>
Compilation exception is defined this way.<br/>
It contains program was compiling and error positions with messages<br/>
of what went wrong during compilation.<br/>
*/
<br/>
class CompilationFailedException ( val programme: String ,<br/>
val messages: Iterable [ ( Int, String ) ] ) <br/>
extends Exception ( messages. map ( message = > message._1 + ". " + message._2 ) . mkString ( "n" ) ) <br/>

By the way, if you are interested in the format of comments - this is Circumflex Docco , a great thing for visual documentation.

Finally, incorrectly, it is necessary to say that Scala is developing rapidly and sometimes developers change the API. This code was successfully tested on version 2.9.0.1 , it should work on all 2.8.x and 2.9.x.

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


All Articles