From 3071220d1937d80603a80b5516c2610f5fa59348 Mon Sep 17 00:00:00 2001 From: kirillius Date: Fri, 21 Mar 2025 20:27:49 +0300 Subject: [PATCH] initial commit --- .gitignore | 39 ++++++++ README.md | 17 ++++ pom.xml | 55 +++++++++++ .../compiler/CompilationException.java | 20 ++++ .../compiler/CompiledFileBytecode.java | 24 +++++ .../compiler/RuntimeClassLoader.java | 25 +++++ .../kirillius/compiler/RuntimeCompiler.java | 35 +++++++ .../compiler/RuntimeFileManager.java | 32 +++++++ .../ru/kirillius/compiler/SourceCode.java | 27 ++++++ .../compiler/RuntimeCompilerTest.java | 92 +++++++++++++++++++ 10 files changed, 366 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/ru/kirillius/compiler/CompilationException.java create mode 100644 src/main/java/ru/kirillius/compiler/CompiledFileBytecode.java create mode 100644 src/main/java/ru/kirillius/compiler/RuntimeClassLoader.java create mode 100644 src/main/java/ru/kirillius/compiler/RuntimeCompiler.java create mode 100644 src/main/java/ru/kirillius/compiler/RuntimeFileManager.java create mode 100644 src/main/java/ru/kirillius/compiler/SourceCode.java create mode 100644 src/test/java/ru/kirillius/compiler/RuntimeCompilerTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a91c35d --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..75162c2 --- /dev/null +++ b/README.md @@ -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(); +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2ee214e --- /dev/null +++ b/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.example + TestRuntimeCompiler + 1.0.0.0 + + + 21 + 21 + UTF-8 + + + + + + org.junit.jupiter + junit-jupiter-api + 5.11.4 + test + + + + org.junit.jupiter + junit-jupiter-engine + 5.11.4 + test + + + + org.easytesting + fest-assert-core + 2.0M10 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + \ No newline at end of file diff --git a/src/main/java/ru/kirillius/compiler/CompilationException.java b/src/main/java/ru/kirillius/compiler/CompilationException.java new file mode 100644 index 0000000..db0c12e --- /dev/null +++ b/src/main/java/ru/kirillius/compiler/CompilationException.java @@ -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()); + } +} diff --git a/src/main/java/ru/kirillius/compiler/CompiledFileBytecode.java b/src/main/java/ru/kirillius/compiler/CompiledFileBytecode.java new file mode 100644 index 0000000..2b5ec47 --- /dev/null +++ b/src/main/java/ru/kirillius/compiler/CompiledFileBytecode.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/ru/kirillius/compiler/RuntimeClassLoader.java b/src/main/java/ru/kirillius/compiler/RuntimeClassLoader.java new file mode 100644 index 0000000..745684c --- /dev/null +++ b/src/main/java/ru/kirillius/compiler/RuntimeClassLoader.java @@ -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(); + } + } + + +} \ No newline at end of file diff --git a/src/main/java/ru/kirillius/compiler/RuntimeCompiler.java b/src/main/java/ru/kirillius/compiler/RuntimeCompiler.java new file mode 100644 index 0000000..b19efc4 --- /dev/null +++ b/src/main/java/ru/kirillius/compiler/RuntimeCompiler.java @@ -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); + } + } +} + diff --git a/src/main/java/ru/kirillius/compiler/RuntimeFileManager.java b/src/main/java/ru/kirillius/compiler/RuntimeFileManager.java new file mode 100644 index 0000000..41e66e8 --- /dev/null +++ b/src/main/java/ru/kirillius/compiler/RuntimeFileManager.java @@ -0,0 +1,32 @@ +package ru.kirillius.compiler; + +import javax.tools.*; +import java.util.Hashtable; +import java.util.Map; + +public class RuntimeFileManager extends ForwardingJavaFileManager { + public RuntimeFileManager(StandardJavaFileManager standardManager) { + super(standardManager); + classes = new Hashtable<>(); + loader = new RuntimeClassLoader(this.getClass().getClassLoader(), this); + } + + private final ClassLoader loader; + private final Map 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 getClasses() { + return classes; + } +} \ No newline at end of file diff --git a/src/main/java/ru/kirillius/compiler/SourceCode.java b/src/main/java/ru/kirillius/compiler/SourceCode.java new file mode 100644 index 0000000..893e6d7 --- /dev/null +++ b/src/main/java/ru/kirillius/compiler/SourceCode.java @@ -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; + } +} \ No newline at end of file diff --git a/src/test/java/ru/kirillius/compiler/RuntimeCompilerTest.java b/src/test/java/ru/kirillius/compiler/RuntimeCompilerTest.java new file mode 100644 index 0000000..084d976 --- /dev/null +++ b/src/test/java/ru/kirillius/compiler/RuntimeCompilerTest.java @@ -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 compile(SourceCode sourceCode, Class 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 { + @Override + public Integer apply(Integer i) { + return i*2; + } + } + """; + + var function = (Function) 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) 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 { + @Override + public Integer apply(Integer i) { + return i + """ + magicNumber + """ + ; } + } + """; + + return new SourceCode(classname, source); + } +} \ No newline at end of file