initial commit

This commit is contained in:
kirillius 2025-03-21 20:27:49 +03:00
commit 3071220d19
10 changed files with 366 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
/.idea/

17
README.md Normal file
View File

@ -0,0 +1,17 @@
### Usage example
```java
var compiler = new RuntimeCompiler();
var classname = "com.Test.App";
var source = """
package com.Test;
public class App implements Runnable {
public void run() {
System.out.println("It works! ");
}
}
""";
var compiled = compiler.compile(new SourceCode(classname, source));
var constructor = compiled.getConstructor();
var instance = (Runnable) constructor.newInstance();
instance.run();
```

55
pom.xml Normal file
View File

@ -0,0 +1,55 @@
<?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>org.example</groupId>
<artifactId>TestRuntimeCompiler</artifactId>
<version>1.0.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.easytesting/fest-assert-core -->
<dependency>
<groupId>org.easytesting</groupId>
<artifactId>fest-assert-core</artifactId>
<version>2.0M10</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,20 @@
package ru.kirillius.compiler;
import javax.tools.DiagnosticCollector;
import java.util.StringJoiner;
public class CompilationException extends Exception {
public CompilationException(String message, Throwable cause) {
super(message, cause);
}
private CompilationException(String message) {
super(message);
}
public static CompilationException build(DiagnosticCollector<?> diagnostics) {
var message = new StringJoiner("\n");
diagnostics.getDiagnostics().forEach(diagnostic -> message.add(diagnostic.toString()));
return new CompilationException(message.toString());
}
}

View File

@ -0,0 +1,24 @@
package ru.kirillius.compiler;
import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.URI;
public class CompiledFileBytecode extends SimpleJavaFileObject {
protected ByteArrayOutputStream bos = new ByteArrayOutputStream();
public CompiledFileBytecode(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}
public byte[] asByteArray() {
return bos.toByteArray();
}
@Override
public OutputStream openOutputStream() {
return bos;
}
}

View File

@ -0,0 +1,25 @@
package ru.kirillius.compiler;
import static java.util.Objects.requireNonNull;
public class RuntimeClassLoader extends ClassLoader {
private final RuntimeFileManager manager;
public RuntimeClassLoader(ClassLoader parent, RuntimeFileManager manager) {
super(parent);
this.manager = requireNonNull(manager, "manager must not be null");
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
var compiledClasses = manager.getClasses();
if (compiledClasses.containsKey(name)) {
var bytes = compiledClasses.get(name).asByteArray();
return defineClass(name, bytes, 0, bytes.length);
} else {
throw new ClassNotFoundException();
}
}
}

View File

@ -0,0 +1,35 @@
package ru.kirillius.compiler;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.util.Collections;
public class RuntimeCompiler {
private final RuntimeFileManager fileManager;
private final JavaCompiler compiler;
public RuntimeCompiler() {
compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new RuntimeException("System Java compiler is not available");
}
fileManager = new RuntimeFileManager(compiler.getStandardFileManager(null, null, null));
}
public Class<?> compile(SourceCode sourceCode) throws CompilationException {
var diagnostics = new DiagnosticCollector<>();
var task = compiler.getTask(null, fileManager, diagnostics, null, null, Collections.singletonList(sourceCode));
if (!task.call()) {
throw CompilationException.build(diagnostics);
}
try {
return fileManager.getClassLoader(null).loadClass(sourceCode.getClassName());
} catch (ClassNotFoundException e) {
throw new CompilationException("Unable to load compiled class", e);
}
}
}

View File

@ -0,0 +1,32 @@
package ru.kirillius.compiler;
import javax.tools.*;
import java.util.Hashtable;
import java.util.Map;
public class RuntimeFileManager extends ForwardingJavaFileManager<JavaFileManager> {
public RuntimeFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
classes = new Hashtable<>();
loader = new RuntimeClassLoader(this.getClass().getClassLoader(), this);
}
private final ClassLoader loader;
private final Map<String, CompiledFileBytecode> classes;
@Override
public ClassLoader getClassLoader(Location location) {
return loader;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
var bytecode = new CompiledFileBytecode(className, kind);
classes.put(className, bytecode);
return bytecode;
}
public Map<String, CompiledFileBytecode> getClasses() {
return classes;
}
}

View File

@ -0,0 +1,27 @@
package ru.kirillius.compiler;
import javax.tools.SimpleJavaFileObject;
import java.net.URI;
import static java.util.Objects.requireNonNull;
public class SourceCode extends SimpleJavaFileObject {
private final String sourceCode;
private final String className;
public SourceCode(String classname, String sourceCode) {
super(URI.create("string:///" + classname.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.sourceCode = requireNonNull(sourceCode, "sourceCode must not be null");
this.className = classname;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return sourceCode;
}
public String getClassName() {
return className;
}
}

View File

@ -0,0 +1,92 @@
package ru.kirillius.compiler;
import org.junit.jupiter.api.Test;
import java.util.function.Function;
import static org.fest.assertions.api.Assertions.assertThat;
class RuntimeCompilerTest {
private <T> T compile(SourceCode sourceCode, Class<T> type) {
var compiler = new RuntimeCompiler();
try {
var compiled = compiler.compile(sourceCode);
assertThat(compiled).isNotNull();
var constructor = compiled.getConstructor();
return (T) constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void TestBasicCompilation() {
var classname = "com.Test.App";
var source = """
package com.Test;
import java.util.function.Function;
public class App implements Function<Integer, Integer> {
@Override
public Integer apply(Integer i) {
return i*2;
}
}
""";
var function = (Function<Integer, Integer>) compile(new SourceCode(classname, source), Function.class);
assertThat(function.apply(1)).isEqualTo(2);
}
@Test
public void TestMultipleCompilation() {
for (var i = 0; i < 30; i++) {
var number = (int) (Math.random() * 1000);
var source = createNewSource(number);
var function = (Function<Integer, Integer>) compile(source, Function.class);
assertThat(function.apply(i)).isEqualTo(number + i);
}
}
@Test
public void CompileBrokenCode() {
var compiler = new RuntimeCompiler();
try {
compiler.compile(new SourceCode("broken1", "this code is broken"));
throw new RuntimeException();
} catch (Exception e) {
assertThat(e).isInstanceOf(CompilationException.class);
}
}
@Test
public void CompileInvalidClassname() {
var compiler = new RuntimeCompiler();
try {
compiler.compile(new SourceCode("broken2", "public class NotBroken{ public int x; }"));
throw new RuntimeException();
} catch (Exception e) {
assertThat(e).isInstanceOf(CompilationException.class);
}
}
private SourceCode createNewSource(int magicNumber) {
var classname = "com.Test.App" + magicNumber;
var source = """
package com.Test;
import java.util.function.Function;
public class App""" + magicNumber + """
implements Function<Integer, Integer> {
@Override
public Integer apply(Integer i) {
return i + """ + magicNumber + """
; }
}
""";
return new SourceCode(classname, source);
}
}