├── .gitignore ├── README.md ├── dev.thihup.bytecode.annotation.core ├── pom.xml └── src │ └── main │ └── java │ ├── dev │ └── thihup │ │ └── bytecode │ │ └── annotation │ │ └── core │ │ └── Bytecode.java │ └── module-info.java ├── dev.thihup.bytecode.annotation.processor ├── pom.xml └── src │ └── main │ ├── java │ ├── dev │ │ └── thihup │ │ │ └── bytecode │ │ │ └── annotation │ │ │ └── processor │ │ │ └── BytecodeProcessor.java │ └── module-info.java │ └── resources │ └── META-INF │ └── services │ └── javax.annotation.processing.Processor ├── dev.thihup.bytecode.annotation.tests ├── pom.xml └── src │ ├── main │ └── java │ │ ├── dev │ │ └── thihup │ │ │ └── bytecode │ │ │ └── annotation │ │ │ └── examples │ │ │ ├── CondyBSMExample.java │ │ │ ├── CondyConstantBootstrapsExample.java │ │ │ ├── HelloWorld.java │ │ │ ├── IndyAddInts.java │ │ │ └── IndySimple.java │ │ └── module-info.java │ └── test │ └── java │ ├── dev │ └── thihup │ │ └── bytecode │ │ └── annotation │ │ └── tests │ │ ├── ConstantDynamicTest.java │ │ ├── HelloWorldTest.java │ │ └── InvokeDynamicTest.java │ └── module-info.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .factorypath 3 | .DS_Store 4 | .project 5 | .settings/ 6 | .vscode 7 | dependency-reduced-pom.xml 8 | nb-configuration.xml 9 | nbactions.xml 10 | nbproject/ 11 | target/ 12 | .idea 13 | *.iml 14 | *.original~ 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bytecode annotation processor 2 | ---------------------- 3 | 4 | This project allows the usage of inline bytecode in the Java programming language. 5 | It uses the [JASM syntax](https://wiki.openjdk.java.net/display/CodeTools/Appendix+A) for the instructions. 6 | 7 | One use case would be to use the InvokeDynamic instruction and/or the CONSTANT_Dynamic. Both 8 | features are not available in Java programming language. 9 | 10 | It is heavily inspired by the [Groovy Bytecode AST](https://github.com/melix/groovy-bytecode-ast/) project. 11 | 12 | However, this project uses only supported API. 13 | 14 | Example 15 | ------ 16 | ```java 17 | public class HelloWorld { 18 | 19 | @Bytecode( 20 | value = """ 21 | getstatic java/lang/System.out:"Ljava/io/PrintStream;"; 22 | ldc "Hello, world!"; 23 | invokevirtual java/io/PrintStream.println:"(Ljava/lang/String;)V"; 24 | return; 25 | """, 26 | className="dev.thihup.bytecode.annotation.examples.HelloWorldImpl" 27 | ) 28 | void myMethod() { 29 | HelloWorldImpl.invoke(); 30 | } 31 | 32 | } 33 | ``` 34 | See the [tests](dev.thihup.bytecode.annotation.tests) folder for more examples. 35 | 36 | Limitations 37 | ----------- 38 | 39 | - As we can only generate external classes, it is not possible to create an inner class to store the inline bytecode (the generated class is valid, but you won't be able to reference it in a Java source file); 40 | - For the same reason as above, it is not possible to access private field of the annotated class. -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | dev.thihup.bytecode.annotation.processor 9 | bytecode-annotation-processor-parent 10 | 1.0-SNAPSHOT 11 | 12 | 13 | bytecode-annotation-core 14 | 15 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.core/src/main/java/dev/thihup/bytecode/annotation/core/Bytecode.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.core; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Bytecode annotation 10 | * 11 | * It creates a new class with a method body specified in this annotation. 12 | */ 13 | @Retention(RetentionPolicy.SOURCE) 14 | @Target(ElementType.METHOD) 15 | public @interface Bytecode { 16 | 17 | /** 18 | * The bytecode of the method utilizing the JASM syntax 19 | */ 20 | String value(); 21 | 22 | /** 23 | * The class name of the class that contains the method to be invoked. 24 | * It uses the fully qualified name of the class (using dots). 25 | */ 26 | String className(); 27 | 28 | /** 29 | * The signature of the method to be invoked. 30 | * Default to "()V" 31 | */ 32 | String methodSignature() default "()V"; 33 | 34 | /** 35 | * The name of the method to be invoked. Default to "invoke" 36 | */ 37 | String methodName() default "invoke"; 38 | 39 | /** 40 | * The class file version 41 | * Default to "61:0" (Java 17) 42 | */ 43 | String classVersion() default "61:0"; 44 | 45 | /** 46 | * If specified, it will use the of the jasmCode instead of creating a Jasm code 47 | * from the other parameters. 48 | */ 49 | String jasmCode() default ""; 50 | } 51 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.core/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module dev.thihup.bytecode.annotation.core { 2 | exports dev.thihup.bytecode.annotation.core; 3 | } -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.processor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | dev.thihup.bytecode.annotation.processor 9 | bytecode-annotation-processor-parent 10 | 1.0-SNAPSHOT 11 | 12 | 13 | bytecode-annotation-processor 14 | 15 | 16 | 17 | dev.thihup.bytecode.annotation.processor 18 | bytecode-annotation-core 19 | ${project.version} 20 | 21 | 22 | org.openjdk.asmtools 23 | asmtools-core 24 | 7.0.b10-ea 25 | 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 34 | none 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.processor/src/main/java/dev/thihup/bytecode/annotation/processor/BytecodeProcessor.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.processor; 2 | 3 | import static java.util.stream.Collectors.toMap; 4 | 5 | import dev.thihup.bytecode.annotation.core.Bytecode; 6 | import java.io.IOException; 7 | import java.io.PrintWriter; 8 | import java.nio.file.FileVisitResult; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.SimpleFileVisitor; 12 | import java.nio.file.attribute.BasicFileAttributes; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import javax.annotation.processing.AbstractProcessor; 17 | import javax.annotation.processing.Filer; 18 | import javax.annotation.processing.RoundEnvironment; 19 | import javax.annotation.processing.SupportedAnnotationTypes; 20 | import javax.annotation.processing.SupportedSourceVersion; 21 | import javax.lang.model.SourceVersion; 22 | import javax.lang.model.element.AnnotationMirror; 23 | import javax.lang.model.element.Element; 24 | import javax.lang.model.element.TypeElement; 25 | import javax.tools.JavaFileObject; 26 | import org.openjdk.asmtools.jasm.Main; 27 | 28 | @SupportedAnnotationTypes("dev.thihup.bytecode.annotation.core.Bytecode") 29 | @SupportedSourceVersion(SourceVersion.RELEASE_17) 30 | public final class BytecodeProcessor extends AbstractProcessor { 31 | 32 | @Override 33 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 34 | Set elementsAnnotatedWith = 35 | roundEnv.getElementsAnnotatedWith(Bytecode.class); 36 | try { 37 | Path outputDirectory = Files.createTempDirectory("bytecodeProcessor"); 38 | for (Element element : elementsAnnotatedWith) { 39 | processMethod(outputDirectory, element); 40 | } 41 | 42 | Files.walkFileTree(outputDirectory, new DeleteAllFilesVisitor()); 43 | } catch (IOException e) { 44 | throw new RuntimeException(e); 45 | } 46 | return true; 47 | } 48 | 49 | private void processMethod(Path outputDirectory, Element element) throws IOException { 50 | Bytecode annotation = element.getAnnotation(Bytecode.class); 51 | 52 | String className = annotation.className(); 53 | String classNameWithSlashes = className.replace('.', '/'); 54 | String jasmCode = generateJasmCode(annotation, classNameWithSlashes); 55 | 56 | Path jasmFile = outputDirectory.resolve(className + ".jasm"); 57 | Files.writeString(jasmFile, jasmCode); 58 | 59 | compile(outputDirectory, jasmFile); 60 | 61 | Filer filer = processingEnv.getFiler(); 62 | JavaFileObject bytecode = filer.createClassFile(className, element); 63 | try (var outputStream = bytecode.openOutputStream()) { 64 | Path classFile = outputDirectory.resolve(classNameWithSlashes.concat(".class")); 65 | Files.copy(classFile, outputStream); 66 | } 67 | } 68 | 69 | private String generateJasmCode(Bytecode annotation, String classNameWithSlashes) { 70 | return !annotation.jasmCode().isEmpty() ? annotation.jasmCode() : """ 71 | class %s 72 | version %s { 73 | static Method "%s":"%s" stack 100 locals 100 { 74 | %s 75 | } 76 | } 77 | """.formatted(classNameWithSlashes, 78 | annotation.classVersion(), 79 | annotation.methodName(), annotation.methodSignature(), annotation.value()); 80 | } 81 | 82 | private void compile(Path outputDirectory, Path jasmFile) { 83 | Main compiler = new Main(new PrintWriter(System.out), "jasm") { 84 | { 85 | this.err = new PrintWriter(System.err); 86 | } 87 | }; 88 | boolean compile = compiler.compile( 89 | new String[]{"-d", outputDirectory.toString(), jasmFile.toString()}); 90 | if (!compile) { 91 | throw new RuntimeException("Compilation failed"); 92 | } 93 | } 94 | 95 | private static class DeleteAllFilesVisitor extends SimpleFileVisitor { 96 | 97 | @Override 98 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) 99 | throws IOException { 100 | Files.deleteIfExists(dir); 101 | return FileVisitResult.CONTINUE; 102 | } 103 | 104 | @Override 105 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 106 | throws IOException { 107 | Files.deleteIfExists(file); 108 | return FileVisitResult.CONTINUE; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.processor/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | import javax.annotation.processing.Processor; 2 | 3 | module dev.thihup.bytecode.annotation.processor { 4 | requires transitive dev.thihup.bytecode.annotation.core; 5 | requires java.compiler; 6 | requires asmtools.core; 7 | provides Processor with dev.thihup.bytecode.annotation.processor.BytecodeProcessor; 8 | } -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | dev.thihup.bytecode.annotation.processor.BytecodeProcessor -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | dev.thihup.bytecode.annotation.processor 9 | bytecode-annotation-processor-parent 10 | 1.0-SNAPSHOT 11 | 12 | 13 | bytecode-annotation-tests 14 | 15 | 16 | 17 | dev.thihup.bytecode.annotation.processor 18 | bytecode-annotation-core 19 | ${project.version} 20 | provided 21 | 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-compiler-plugin 29 | 30 | 31 | 32 | dev.thihup.bytecode.annotation.processor 33 | bytecode-annotation-processor 34 | ${project.version} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/main/java/dev/thihup/bytecode/annotation/examples/CondyBSMExample.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.examples; 2 | 3 | import dev.thihup.bytecode.annotation.core.Bytecode; 4 | import java.lang.invoke.MethodHandles; 5 | import java.util.List; 6 | 7 | public class CondyBSMExample { 8 | 9 | @Bytecode(value = 10 | """ 11 | ldc Dynamic REF_invokeStatic:dev/thihup/bytecode/annotation/examples/CondyBSMExample.fibonacci 12 | :"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/util/List;" 13 | :foo:"Ljava/util/List;"; 14 | areturn; 15 | """, 16 | methodSignature = "()Ljava/util/List;", 17 | className="dev.thihup.bytecode.annotation.examples.Example2" 18 | ) 19 | public List myMethod(int a, int b) { 20 | return Example2.invoke(); 21 | } 22 | 23 | static List fibonacci(MethodHandles.Lookup lookup, String name, Class methodType) throws Throwable { 24 | return List.of(0, 1, 1, 2, 3, 5, 8, 13, 21, 34); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/main/java/dev/thihup/bytecode/annotation/examples/CondyConstantBootstrapsExample.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.examples; 2 | 3 | import dev.thihup.bytecode.annotation.core.Bytecode; 4 | 5 | public class CondyConstantBootstrapsExample { 6 | 7 | @Bytecode(value = 8 | """ 9 | ldc Dynamic REF_invokeStatic:java/lang/invoke/ConstantBootstraps.primitiveClass 10 | :"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Class;" 11 | :I:"Ljava/lang/Class;"; 12 | areturn; 13 | """, 14 | methodSignature = "()Ljava/lang/Class;", 15 | className="dev.thihup.bytecode.annotation.examples.Example1" 16 | ) 17 | public Class myMethod() { 18 | return Example1.invoke(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/main/java/dev/thihup/bytecode/annotation/examples/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.examples; 2 | 3 | import dev.thihup.bytecode.annotation.core.Bytecode; 4 | 5 | public class HelloWorld { 6 | 7 | @Bytecode( 8 | value = """ 9 | getstatic java/lang/System.out:"Ljava/io/PrintStream;"; 10 | ldc "Hello, world!"; 11 | invokevirtual java/io/PrintStream.println:"(Ljava/lang/String;)V"; 12 | return; 13 | """, 14 | className="dev.thihup.bytecode.annotation.examples.HelloWorldImpl" 15 | ) 16 | public void myMethod() { 17 | HelloWorldImpl.invoke(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/main/java/dev/thihup/bytecode/annotation/examples/IndyAddInts.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.examples; 2 | 3 | import dev.thihup.bytecode.annotation.core.Bytecode; 4 | import java.lang.invoke.CallSite; 5 | import java.lang.invoke.ConstantCallSite; 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodType; 9 | 10 | public class IndyAddInts { 11 | 12 | @Bytecode(value = 13 | """ 14 | iload_0; 15 | iload_1; 16 | invokedynamic REF_invokeStatic: 17 | dev/thihup/bytecode/annotation/examples/IndyAddInts.superAddBSM: 18 | "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;": 19 | "foo":"(II)I"; 20 | ireturn; 21 | """, 22 | methodSignature = "(II)I", 23 | className="dev.thihup.bytecode.annotation.examples.AddInts" 24 | ) 25 | public int myMethod(int a, int b) { 26 | return AddInts.invoke(a, b); 27 | } 28 | 29 | static CallSite superAddBSM(MethodHandles.Lookup lookup, String methodName, MethodType methodType) throws Throwable { 30 | MethodHandle addExact = lookup.findStatic(Math.class, "addExact", 31 | MethodType.methodType(int.class, int.class, int.class)); 32 | 33 | return new ConstantCallSite(addExact.asType(methodType)); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/main/java/dev/thihup/bytecode/annotation/examples/IndySimple.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.examples; 2 | 3 | import dev.thihup.bytecode.annotation.core.Bytecode; 4 | import java.lang.invoke.CallSite; 5 | import java.lang.invoke.ConstantCallSite; 6 | import java.lang.invoke.MethodHandles; 7 | import java.lang.invoke.MethodType; 8 | 9 | public class IndySimple { 10 | 11 | @Bytecode(value = 12 | """ 13 | invokedynamic REF_invokeStatic: 14 | dev/thihup/bytecode/annotation/examples/IndySimple.myBSMWithoutArgs: 15 | "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;": 16 | "foo":"()Ljava/lang/String;"; 17 | areturn; 18 | """, 19 | methodSignature = "()Ljava/lang/String;", 20 | className="dev.thihup.bytecode.annotation.examples.IndySimpleWithoutArgs" 21 | ) 22 | public String myMethod() { 23 | return IndySimpleWithoutArgs.invoke(); 24 | } 25 | 26 | static CallSite myBSMWithoutArgs(MethodHandles.Lookup lookup, String methodName, MethodType methodType) throws Throwable { 27 | return new ConstantCallSite(MethodHandles.constant(String.class, "Hello World")); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module dev.thihup.bytecode.annotation.examples { 2 | exports dev.thihup.bytecode.annotation.examples; 3 | requires dev.thihup.bytecode.annotation.core; 4 | } -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/test/java/dev/thihup/bytecode/annotation/tests/ConstantDynamicTest.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.tests; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import dev.thihup.bytecode.annotation.examples.CondyBSMExample; 6 | import dev.thihup.bytecode.annotation.examples.CondyConstantBootstrapsExample; 7 | import dev.thihup.bytecode.annotation.examples.IndyAddInts; 8 | import dev.thihup.bytecode.annotation.examples.IndySimple; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ConstantDynamicTest { 13 | 14 | @Test 15 | void test() { 16 | assertEquals(int.class, new CondyConstantBootstrapsExample().myMethod()); 17 | } 18 | 19 | @Test 20 | void test2() { 21 | List fibo = new CondyBSMExample().myMethod(5, 7); 22 | assertEquals(10, fibo.size()); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/test/java/dev/thihup/bytecode/annotation/tests/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.tests; 2 | 3 | import dev.thihup.bytecode.annotation.examples.HelloWorld; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class HelloWorldTest { 7 | 8 | @Test 9 | void myMethod() { 10 | new HelloWorld().myMethod(); 11 | } 12 | } -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/test/java/dev/thihup/bytecode/annotation/tests/InvokeDynamicTest.java: -------------------------------------------------------------------------------- 1 | package dev.thihup.bytecode.annotation.tests; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import dev.thihup.bytecode.annotation.examples.IndyAddInts; 6 | import dev.thihup.bytecode.annotation.examples.IndySimple; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class InvokeDynamicTest { 10 | 11 | @Test 12 | void test() { 13 | assertEquals("Hello World", new IndySimple().myMethod()); 14 | } 15 | 16 | @Test 17 | void test2() { 18 | assertEquals(12, new IndyAddInts().myMethod(5, 7)); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /dev.thihup.bytecode.annotation.tests/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | open module dev.thihup.bytecode.annotation.tests { 2 | requires dev.thihup.bytecode.annotation.examples; 3 | requires org.junit.jupiter.api; 4 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | dev.thihup.bytecode.annotation.processor 8 | bytecode-annotation-processor-parent 9 | 1.0-SNAPSHOT 10 | pom 11 | 12 | 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | dev.thihup.bytecode.annotation.core 19 | dev.thihup.bytecode.annotation.processor 20 | dev.thihup.bytecode.annotation.tests 21 | 22 | 23 | 24 | 25 | 26 | org.apache.maven.plugins 27 | maven-compiler-plugin 28 | 3.8.1 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-surefire-plugin 33 | 3.0.0-M5 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.junit.jupiter 41 | junit-jupiter-api 42 | 5.8.2 43 | test 44 | 45 | 46 | 47 | --------------------------------------------------------------------------------