Add classfile-api module: java.lang.classfile (JEP 484) parse, generate, transform with timing instrumentation, ASM 9.11.1 comparison, version limits, JDK 24/25/26 API diff
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -0,0 +1,28 @@
|
||||
# classfile-api
|
||||
|
||||
Companion code for the ankurm.com article **The Java ClassFile API (JEP 484): Read, Generate and Transform Bytecode Without ASM**
|
||||
(https://ankurm.com/java-classfile-api-jep-484/).
|
||||
|
||||
`java.lang.classfile` was a preview API in JDK 22 and 23 (JEPs 457, 466) and is **final since JDK 24 (JEP 484)**: no `--enable-preview`.
|
||||
Everything here ran on Temurin 25.0.5+8; the API diff also ran on Temurin 24.0.2 and 26.0.2.1. The only dependency is ASM 9.11.1, used by the comparison classes.
|
||||
|
||||
| File | What it shows | Output |
|
||||
|---|---|---|
|
||||
| `Target.java`, `Timings.java` | the class we inspect/instrument, and the sink the injected code calls | |
|
||||
| `ParseDemo.java` | `ClassFile.of().parse`, `ClassModel` traversal, pattern-matching over elements and instructions | `output/01-parse.txt` |
|
||||
| `GenerateDemo.java` | `ClassFile.build`, `verify`, `Lookup.defineClass` vs `defineHiddenClass` | `output/02-generate.txt`, `03-javap-generated.txt` |
|
||||
| `BrokenGenerate.java` | dropped stack maps, wrong return type, stack underflow, swapped exception-table labels | `output/04-broken-generate.txt` |
|
||||
| `TimingTransform.java`, `TransformDemo.java` | `ClassTransform` / `MethodTransform` / `CodeTransform` inserting timing; naive vs exception-safe | `output/05-transform.txt`, `06-javap-instrumented.txt` |
|
||||
| `AsmParse.java`, `AsmGenerate.java`, `AsmInstrument.java`, `AsmGotcha.java` | the same three jobs in ASM 9.11.1, asserted equal; plus the `EXPAND_FRAMES` trap | `output/08`, `09`, `10`, `15` |
|
||||
| `VersionLimits.java` | a newer class file than the reader knows: `java.lang.classfile` vs ASM | `output/11-version-limits.txt` |
|
||||
| `Bench.java` | indicative parse and copy timing on 2000 `java.base` classes (thread CPU time, noisy) | `output/13-bench.txt` |
|
||||
| `scripts/run-all.sh` | API diff of `java.lang.classfile` across JDK 24, 25, 26 | `output/07-api-diff-24-25-26.txt` |
|
||||
| `scripts/spring-core-probe.sh` | what `spring-core` 7.0.10 ships for the ClassFile API | `output/14-spring-core-probe.txt` |
|
||||
|
||||
```bash
|
||||
JDK25=/path/to/jdk-25 JDK24=/path/to/jdk-24 JDK26=/path/to/jdk-26 ./scripts/run-all.sh # JDK24/JDK26 optional; needs Maven for the ASM jars
|
||||
JDK25=/path/to/jdk-25 ./scripts/spring-core-probe.sh
|
||||
```
|
||||
|
||||
The demos assert as they go (`PASS`/`FAIL`, non-zero exit on failure), like the other standalone modules; there is no JUnit. Output is byte-stable except `13-bench.txt`.
|
||||
Not reproduced: nothing was run on JDK 22/23 previews.
|
||||
@@ -0,0 +1,72 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.ParseDemo (25.0.5+8-LTS)
|
||||
== header ==
|
||||
bytes : 1488
|
||||
this : demo/Target
|
||||
super : java/lang/Object
|
||||
version : 69.0 (ClassFile.JAVA_25_VERSION = 69)
|
||||
flags : public,super
|
||||
constant pool : 69 slots
|
||||
|
||||
== fields ==
|
||||
final,private name Ljava/lang/String;
|
||||
private calls I
|
||||
|
||||
== methods ==
|
||||
public <init>(Ljava/lang/String;)V
|
||||
public,static fib(I)I
|
||||
public,static sumTo(I)J
|
||||
public greet(Ljava/lang/String;)Ljava/lang/String;
|
||||
public pause(J)V
|
||||
public,static boom()V
|
||||
public,static explode([I)I
|
||||
public calls()I
|
||||
|
||||
== class elements, in file order (the model is a stream of elements) ==
|
||||
AccessFlags
|
||||
ClassFileVersion
|
||||
Superclass
|
||||
Interfaces
|
||||
field name
|
||||
field calls
|
||||
method <init>
|
||||
method fib
|
||||
method sumTo
|
||||
method greet
|
||||
method pause
|
||||
method boom
|
||||
method explode
|
||||
method calls
|
||||
SourceFile=Target.java
|
||||
BoundInnerClassesAttribute
|
||||
|
||||
== fib(I)I, instruction by instruction ==
|
||||
ILOAD_0
|
||||
ICONST_2
|
||||
IF_ICMPGE
|
||||
ILOAD_0
|
||||
GOTO
|
||||
ILOAD_0
|
||||
ICONST_1
|
||||
ISUB
|
||||
INVOKESTATIC demo/Target.fib(I)I
|
||||
ILOAD_0
|
||||
ICONST_2
|
||||
ISUB
|
||||
INVOKESTATIC demo/Target.fib(I)I
|
||||
IADD
|
||||
IRETURN
|
||||
|
||||
== invoke targets in the whole class ==
|
||||
2 x demo/Target.fib
|
||||
1 x indy:makeConcatWithConstants
|
||||
1 x java/lang/IllegalStateException.<init>
|
||||
1 x java/lang/Object.<init>
|
||||
1 x java/lang/Thread.sleep
|
||||
|
||||
== a JDK class from the runtime image: java.lang.String ==
|
||||
bytes 50116, major 69, methods 171, fields 11, interfaces 5
|
||||
PASS Target.class is class-file version 69 (compiled with --release 25)
|
||||
PASS Target has 8 methods (constructor + 7)
|
||||
PASS fib calls itself twice
|
||||
PASS greet() uses invokedynamic string concatenation
|
||||
exit=0
|
||||
@@ -0,0 +1,13 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.GenerateDemo target/gen/demo (25.0.5+8-LTS)
|
||||
generated 501 bytes, major version 69 (wrote target/gen/demo/Generated.class)
|
||||
ClassFile.verify(bytes) -> 0 problem(s)
|
||||
PASS verify() finds nothing wrong with the generated class
|
||||
defineClass -> demo.Generated hidden=false findable by name=true
|
||||
[defineClass] add(2,3)=5 hello()=Hello from generated bytecode sumTo(100)=5050 classify(-4)=negative classify(4)=non-negative
|
||||
PASS defineClass: generated methods return the right values
|
||||
defineHiddenClass -> name starts with 'demo.Generated/': true hidden=true distinct from the normal class=true
|
||||
[hidden] add(2,3)=5 hello()=Hello from generated bytecode sumTo(100)=5050 classify(-4)=negative classify(4)=non-negative
|
||||
PASS hidden: generated methods return the right values
|
||||
Class.forName(hidden name) -> ClassNotFoundException (hidden classes cannot be found by name)
|
||||
PASS hidden class is hidden, the other is not
|
||||
exit=0
|
||||
@@ -0,0 +1,106 @@
|
||||
$ javap -c -p -v demo.Generated (the class GenerateDemo wrote; 25.0.5+8-LTS)
|
||||
public class demo.Generated
|
||||
minor version: 0
|
||||
major version: 69
|
||||
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
|
||||
this_class: #2 // demo/Generated
|
||||
super_class: #6 // java/lang/Object
|
||||
interfaces: 0, fields: 0, methods: 5, attributes: 0
|
||||
Constant pool:
|
||||
#1 = Utf8 demo/Generated
|
||||
#2 = Class #1 // demo/Generated
|
||||
#3 = Utf8 <init>
|
||||
#4 = Utf8 ()V
|
||||
#5 = Utf8 java/lang/Object
|
||||
#6 = Class #5 // java/lang/Object
|
||||
#7 = NameAndType #3:#4 // "<init>":()V
|
||||
#8 = Methodref #6.#7 // java/lang/Object."<init>":()V
|
||||
#9 = Utf8 add
|
||||
#10 = Utf8 (II)I
|
||||
#11 = Utf8 hello
|
||||
#12 = Utf8 ()Ljava/lang/String;
|
||||
#13 = Utf8 Hello from generated bytecode
|
||||
#14 = String #13 // Hello from generated bytecode
|
||||
#15 = Utf8 sumTo
|
||||
#16 = Utf8 (I)J
|
||||
#17 = Utf8 classify
|
||||
#18 = Utf8 (I)Ljava/lang/String;
|
||||
#19 = Utf8 negative
|
||||
#20 = String #19 // negative
|
||||
#21 = Utf8 non-negative
|
||||
#22 = String #21 // non-negative
|
||||
#23 = Utf8 Code
|
||||
#24 = Utf8 StackMapTable
|
||||
#25 = Utf8 java/lang/String
|
||||
#26 = Class #25 // java/lang/String
|
||||
{
|
||||
public demo.Generated();
|
||||
descriptor: ()V
|
||||
flags: (0x0001) ACC_PUBLIC
|
||||
Code:
|
||||
stack=1, locals=1, args_size=1
|
||||
0: aload_0
|
||||
1: invokespecial #8 // Method java/lang/Object."<init>":()V
|
||||
4: return
|
||||
|
||||
public static int add(int, int);
|
||||
descriptor: (II)I
|
||||
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
|
||||
Code:
|
||||
stack=2, locals=2, args_size=2
|
||||
0: iload_0
|
||||
1: iload_1
|
||||
2: iadd
|
||||
3: ireturn
|
||||
|
||||
public static java.lang.String hello();
|
||||
descriptor: ()Ljava/lang/String;
|
||||
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
|
||||
Code:
|
||||
stack=1, locals=0, args_size=0
|
||||
0: ldc #14 // String Hello from generated bytecode
|
||||
2: areturn
|
||||
|
||||
public static long sumTo(int);
|
||||
descriptor: (I)J
|
||||
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
|
||||
Code:
|
||||
stack=4, locals=4, args_size=1
|
||||
0: lconst_0
|
||||
1: lstore_1
|
||||
2: iconst_1
|
||||
3: istore_3
|
||||
4: iload_3
|
||||
5: iload_0
|
||||
6: if_icmpgt 20
|
||||
9: lload_1
|
||||
10: iload_3
|
||||
11: i2l
|
||||
12: ladd
|
||||
13: lstore_1
|
||||
14: iinc 3, 1
|
||||
17: goto 4
|
||||
20: lload_1
|
||||
21: lreturn
|
||||
StackMapTable: number_of_entries = 2
|
||||
frame_type = 253 /* append */
|
||||
offset_delta = 4
|
||||
locals = [ long, int ]
|
||||
frame_type = 15 /* same */
|
||||
|
||||
public static java.lang.String classify(int);
|
||||
descriptor: (I)Ljava/lang/String;
|
||||
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
|
||||
Code:
|
||||
stack=1, locals=1, args_size=1
|
||||
0: iload_0
|
||||
1: ifge 9
|
||||
4: ldc #20 // String negative
|
||||
6: goto 11
|
||||
9: ldc #22 // String non-negative
|
||||
11: areturn
|
||||
StackMapTable: number_of_entries = 2
|
||||
frame_type = 9 /* same */
|
||||
frame_type = 65 /* same_locals_1_stack_item */
|
||||
stack = [ class java/lang/String ]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.BrokenGenerate (25.0.5+8-LTS)
|
||||
DROP_STACK_MAPS: verify() -> 1 problem(s)
|
||||
Expecting a stackmap frame at branch target 8 in demo/Broken::pick(int) @1
|
||||
DROP_STACK_MAPS: defineClass -> java.lang.VerifyError
|
||||
Expecting a stackmap frame at branch target 8
|
||||
Exception Details:
|
||||
Location:
|
||||
PASS a branching method built without stack maps fails verification
|
||||
PASS the same code with default options verifies
|
||||
ireturn in a String method: build() succeeded (126 bytes)
|
||||
verify: Bad return type in demo/Wrong::bad() @1 (java/lang/String is not assignable from integer_type)
|
||||
PASS verify() catches a wrongly typed return
|
||||
PASS stack underflow is caught while building
|
||||
underflow: build() threw java.lang.IllegalArgumentException
|
||||
Operand stack underflow at bytecode offset 0 of method u()
|
||||
swapped labels: build() succeeded (173 bytes), verify() -> 0 problem(s)
|
||||
swapped labels: defineClass -> java.lang.ClassFormatError: Illegal exception table range in class file demo/Swapped
|
||||
PASS the JVM rejects the swapped exception-table labels
|
||||
exit=0
|
||||
@@ -0,0 +1,31 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.TransformDemo target/instrumented (25.0.5+8-LTS)
|
||||
original 1488 bytes, naive 1799 bytes, with exception handler 2155 bytes
|
||||
-- naive: time normal returns only (1799 bytes; verify() problems: 0)
|
||||
PASS naive: time normal returns only: demo.Target here is a different Class object than the one on the class path
|
||||
boom() threw java.lang.IllegalStateException: boom
|
||||
explode() threw ArrayIndexOutOfBoundsException
|
||||
fib recorded 177 call(s)
|
||||
sumTo recorded 1 call(s)
|
||||
greet recorded 1 call(s)
|
||||
pause recorded 1 call(s)
|
||||
boom recorded 0 call(s)
|
||||
explode recorded 0 call(s)
|
||||
PASS naive version never records the two methods that throw
|
||||
PASS fib(10) makes 177 calls and 177 timings were recorded (recursion included)
|
||||
-- full: also time when an exception leaves the method (2155 bytes; verify() problems: 0)
|
||||
PASS full: also time when an exception leaves the method: demo.Target here is a different Class object than the one on the class path
|
||||
boom() threw java.lang.IllegalStateException: boom
|
||||
explode() threw ArrayIndexOutOfBoundsException
|
||||
fib recorded 177 call(s)
|
||||
sumTo recorded 1 call(s)
|
||||
greet recorded 1 call(s)
|
||||
pause recorded 1 call(s)
|
||||
boom recorded 1 call(s)
|
||||
explode recorded 1 call(s)
|
||||
PASS exception-safe version records boom() and explode() once each
|
||||
PASS fib still recorded 177 times
|
||||
PASS sumTo and greet recorded once each
|
||||
PASS pause(50) was recorded as at least 50 ms (>= 50,000,000 ns)
|
||||
PASS fib(20) gives the same answer before and after
|
||||
PASS sumTo(12345) gives the same answer before and after
|
||||
exit=0
|
||||
@@ -0,0 +1,121 @@
|
||||
$ javap -c -p demo.Target (original, as javac produced it; fib and boom only)
|
||||
public static int fib(int);
|
||||
Code:
|
||||
0: iload_0
|
||||
1: iconst_2
|
||||
2: if_icmpge 9
|
||||
5: iload_0
|
||||
6: goto 22
|
||||
9: iload_0
|
||||
10: iconst_1
|
||||
11: isub
|
||||
12: invokestatic #13 // Method fib:(I)I
|
||||
15: iload_0
|
||||
16: iconst_2
|
||||
17: isub
|
||||
18: invokestatic #13 // Method fib:(I)I
|
||||
21: iadd
|
||||
22: ireturn
|
||||
|
||||
public static void boom();
|
||||
Code:
|
||||
0: new #31 // class java/lang/IllegalStateException
|
||||
3: dup
|
||||
4: ldc #33 // String boom
|
||||
6: invokespecial #35 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
|
||||
9: athrow
|
||||
|
||||
$ javap -c -p demo.Target (naive transform: records on normal return only)
|
||||
public static int fib(int);
|
||||
Code:
|
||||
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
3: lstore_1
|
||||
4: iload_0
|
||||
5: iconst_2
|
||||
6: if_icmpge 13
|
||||
9: iload_0
|
||||
10: goto 26
|
||||
13: iload_0
|
||||
14: iconst_1
|
||||
15: isub
|
||||
16: invokestatic #13 // Method fib:(I)I
|
||||
19: iload_0
|
||||
20: iconst_2
|
||||
21: isub
|
||||
22: invokestatic #13 // Method fib:(I)I
|
||||
25: iadd
|
||||
26: ldc #76 // String Target.fib
|
||||
28: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
31: lload_1
|
||||
32: lsub
|
||||
33: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
|
||||
36: ireturn
|
||||
|
||||
public static void boom();
|
||||
Code:
|
||||
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
3: lstore_0
|
||||
4: new #31 // class java/lang/IllegalStateException
|
||||
7: dup
|
||||
8: ldc #33 // String boom
|
||||
10: invokespecial #35 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
|
||||
13: athrow
|
||||
|
||||
$ javap -c -p demo.Target (full transform: plus a catch-all handler)
|
||||
public static int fib(int);
|
||||
Code:
|
||||
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
3: lstore_1
|
||||
4: iload_0
|
||||
5: iconst_2
|
||||
6: if_icmpge 13
|
||||
9: iload_0
|
||||
10: goto 26
|
||||
13: iload_0
|
||||
14: iconst_1
|
||||
15: isub
|
||||
16: invokestatic #13 // Method fib:(I)I
|
||||
19: iload_0
|
||||
20: iconst_2
|
||||
21: isub
|
||||
22: invokestatic #13 // Method fib:(I)I
|
||||
25: iadd
|
||||
26: ldc #76 // String Target.fib
|
||||
28: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
31: lload_1
|
||||
32: lsub
|
||||
33: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
|
||||
36: ireturn
|
||||
37: astore_3
|
||||
38: ldc #76 // String Target.fib
|
||||
40: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
43: lload_1
|
||||
44: lsub
|
||||
45: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
|
||||
48: aload_3
|
||||
49: athrow
|
||||
Exception table:
|
||||
from to target type
|
||||
4 37 37 any
|
||||
|
||||
public static void boom();
|
||||
Code:
|
||||
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
3: lstore_0
|
||||
4: new #31 // class java/lang/IllegalStateException
|
||||
7: dup
|
||||
8: ldc #33 // String boom
|
||||
10: invokespecial #35 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
|
||||
13: athrow
|
||||
14: astore_2
|
||||
15: ldc #90 // String Target.boom
|
||||
17: invokestatic #74 // Method java/lang/System.nanoTime:()J
|
||||
20: lload_0
|
||||
21: lsub
|
||||
22: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
|
||||
25: aload_2
|
||||
26: athrow
|
||||
Exception table:
|
||||
from to target type
|
||||
4 14 14 any
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
$ javap -public java.lang.classfile.** on each JDK, then diff by class (24: OpenJDK Runtime Environment Temurin-24.0.2+12 (build 24.0.2+12), 26: OpenJDK Runtime Environment Temurin-26.0.2.1+1 (build 26.0.2.1+1))
|
||||
|
||||
=== JDK 24 -> JDK 25 ===
|
||||
+ java.lang.classfile.ClassFile: public static final int JAVA_25_VERSION;
|
||||
~ java.lang.classfile.attribute.UnknownAttribute
|
||||
was: public interface java.lang.classfile.attribute.UnknownAttribute extends java.lang.classfile.Attribute<java.lang.classfile.attribute.UnknownAttribute>, java.lang.classfile.ClassElement, java.lang.classfile.MethodElement, java.lang.classfile.FieldElement {
|
||||
now: public interface java.lang.classfile.attribute.UnknownAttribute extends java.lang.classfile.Attribute<java.lang.classfile.attribute.UnknownAttribute>, java.lang.classfile.ClassElement, java.lang.classfile.MethodElement, java.lang.classfile.FieldElement, java.lang.classfile.CodeElement {
|
||||
+ java.lang.classfile.constantpool.ClassEntry: public abstract boolean matches(java.lang.constant.ClassDesc);
|
||||
+ java.lang.classfile.constantpool.MethodTypeEntry: public abstract boolean matches(java.lang.constant.MethodTypeDesc);
|
||||
+ java.lang.classfile.constantpool.ModuleEntry: public abstract boolean matches(java.lang.constant.ModuleDesc);
|
||||
+ java.lang.classfile.constantpool.PackageEntry: public abstract boolean matches(java.lang.constant.PackageDesc);
|
||||
+ java.lang.classfile.constantpool.StringEntry: public abstract boolean equalsString(java.lang.String);
|
||||
+ java.lang.classfile.constantpool.Utf8Entry: public abstract boolean isFieldType(java.lang.constant.ClassDesc);
|
||||
+ java.lang.classfile.constantpool.Utf8Entry: public abstract boolean isMethodType(java.lang.constant.MethodTypeDesc);
|
||||
239 classes in old, 239 in new, 9 difference(s)
|
||||
|
||||
=== JDK 25 -> JDK 26 ===
|
||||
+ java.lang.classfile.ClassFile: public static final int JAVA_26_VERSION;
|
||||
239 classes in old, 239 in new, 1 difference(s)
|
||||
|
||||
=== is it a preview API on JDK 24? ===
|
||||
$ javap -v java.lang.classfile.ClassFile | grep -ci PreviewFeature
|
||||
24.0.2+12: 0
|
||||
25.0.5+8-LTS: 0
|
||||
26.0.2.1+1: 0
|
||||
|
||||
=== @since of ClassFile ===
|
||||
jdk-24.0.2+12: * @since 24
|
||||
jdk-25.0.5+8: * @since 24
|
||||
jdk-26.0.2.1+1: * @since 24
|
||||
@@ -0,0 +1,18 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.AsmParse (25.0.5+8-LTS)
|
||||
ASM sees major version 69
|
||||
public <init>(Ljava/lang/String;)V
|
||||
public,static fib(I)I
|
||||
public,static sumTo(I)J
|
||||
public greet(Ljava/lang/String;)Ljava/lang/String;
|
||||
public pause(J)V
|
||||
public,static boom()V
|
||||
public,static explode([I)I
|
||||
public calls()I
|
||||
2 x demo/Target.fib
|
||||
1 x indy:makeConcatWithConstants
|
||||
1 x java/lang/IllegalStateException.<init>
|
||||
1 x java/lang/Object.<init>
|
||||
1 x java/lang/Thread.sleep
|
||||
PASS ASM and java.lang.classfile list identical method lines
|
||||
PASS ASM and java.lang.classfile count identical invoke targets
|
||||
exit=0
|
||||
@@ -0,0 +1,7 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.AsmGenerate (25.0.5+8-LTS)
|
||||
ASM generated 501 bytes; java.lang.classfile generated 501 bytes
|
||||
PASS ASM output passes ClassFile.verify()
|
||||
[ASM-generated] add(2,3)=5 hello()=Hello from generated bytecode sumTo(100)=5050 classify(-4)=negative classify(4)=non-negative
|
||||
PASS ASM-generated: generated methods return the right values
|
||||
PASS both generators emit the same method list
|
||||
exit=0
|
||||
@@ -0,0 +1,17 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.AsmInstrument (25.0.5+8-LTS)
|
||||
original 1488 bytes, ASM-instrumented 2152 bytes
|
||||
PASS ASM output passes ClassFile.verify()
|
||||
-- ASM AdviceAdapter version (2152 bytes; verify() problems: 0)
|
||||
PASS ASM AdviceAdapter version: demo.Target here is a different Class object than the one on the class path
|
||||
boom() threw java.lang.IllegalStateException: boom
|
||||
explode() threw ArrayIndexOutOfBoundsException
|
||||
fib recorded 177 call(s)
|
||||
sumTo recorded 1 call(s)
|
||||
greet recorded 1 call(s)
|
||||
pause recorded 1 call(s)
|
||||
boom recorded 1 call(s)
|
||||
explode recorded 1 call(s)
|
||||
PASS ASM version also records both throwing methods once
|
||||
PASS ASM version: fib recorded 177 times
|
||||
PASS ASM version: pause(50) recorded as at least 50 ms
|
||||
exit=0
|
||||
@@ -0,0 +1,22 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.VersionLimits (25.0.5+8-LTS)
|
||||
running on Java 25; ClassFile.latestMajorVersion() = 69; ASM Opcodes.V25=69, V28=72
|
||||
major 69:
|
||||
java.lang.classfile : parsed, 8 methods
|
||||
ASM 9.11.1 : parsed, 8 methods
|
||||
major 70:
|
||||
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 70
|
||||
ASM 9.11.1 : parsed, 8 methods
|
||||
major 72:
|
||||
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 72
|
||||
ASM 9.11.1 : parsed, 8 methods
|
||||
major 73:
|
||||
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 73
|
||||
ASM 9.11.1 : java.lang.IllegalArgumentException: Unsupported class file version 73.0
|
||||
major 99:
|
||||
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 99
|
||||
ASM 9.11.1 : java.lang.IllegalArgumentException: Unsupported class file version 99.0
|
||||
PASS major 69 parses with java.lang.classfile
|
||||
PASS one version above the JDK's own is rejected by java.lang.classfile
|
||||
PASS ASM 9.11.1 still reads major 72 (Java 28)
|
||||
PASS ASM 9.11.1 rejects major 73
|
||||
exit=0
|
||||
@@ -0,0 +1,3 @@
|
||||
Lines of code in the method(s) that do the job (non-blank, non-comment), counted by scripts/run-all.sh:
|
||||
generate demo.Generated java.lang.classfile 32 ASM 63
|
||||
timing instrumentation java.lang.classfile 61 ASM 43
|
||||
@@ -0,0 +1,18 @@
|
||||
--- run 1 of 3 ---
|
||||
running on Java 25.0.5+8-LTS, ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH
|
||||
2000 classes from java.base, 7001 KiB; 4 warm-up rounds, median of 9
|
||||
instructions seen: java.lang.classfile 478741, ASM 478741
|
||||
parse + visit every instruction : java.lang.classfile 119.4 ms ASM 166.1 ms (ASM / classfile = 1.39)
|
||||
parse + copy + write back : java.lang.classfile 38.6 ms ASM 78.9 ms (ASM / classfile = 2.05)
|
||||
--- run 2 of 3 ---
|
||||
running on Java 25.0.5+8-LTS, ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH
|
||||
2000 classes from java.base, 7001 KiB; 4 warm-up rounds, median of 9
|
||||
instructions seen: java.lang.classfile 478741, ASM 478741
|
||||
parse + visit every instruction : java.lang.classfile 63.9 ms ASM 166.2 ms (ASM / classfile = 2.60)
|
||||
parse + copy + write back : java.lang.classfile 26.8 ms ASM 88.1 ms (ASM / classfile = 3.29)
|
||||
--- run 3 of 3 ---
|
||||
running on Java 25.0.5+8-LTS, ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH
|
||||
2000 classes from java.base, 7001 KiB; 4 warm-up rounds, median of 9
|
||||
instructions seen: java.lang.classfile 478741, ASM 478741
|
||||
parse + visit every instruction : java.lang.classfile 48.8 ms ASM 44.2 ms (ASM / classfile = 0.90)
|
||||
parse + copy + write back : java.lang.classfile 18.8 ms ASM 101.6 ms (ASM / classfile = 5.39)
|
||||
@@ -0,0 +1,22 @@
|
||||
$ unzip -l spring-core-7.0.10.jar | grep 'META-INF/versions/24/.*\.class' | wc -l
|
||||
9
|
||||
|
||||
$ unzip -l spring-core-7.0.10.jar | grep 'versions/24.*ClassFile'
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate$Source.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata$Builder.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMetadataReader.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMethodMetadata$Source.class
|
||||
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMethodMetadata.class
|
||||
|
||||
$ unzip -l spring-core-7.0.10.jar | grep -c 'org/springframework/asm/.*\.class' (the repackaged ASM is still there)
|
||||
37
|
||||
|
||||
$ javap -v -cp <jar>!/META-INF/versions/24 ...ClassFileMetadataReader | grep java/lang/classfile (does it really call the JDK API?)
|
||||
java/lang/classfile/ClassFile
|
||||
java/lang/classfile/ClassModel
|
||||
|
||||
$ unzip -p spring-core-7.0.10.jar META-INF/MANIFEST.MF | grep -i multi-release
|
||||
Multi-Release: true
|
||||
@@ -0,0 +1,7 @@
|
||||
$ java -cp classes:asm-9.11.1.jar... demo.AsmGotcha (25.0.5+8-LTS)
|
||||
readerFlags = 0 -> java.lang.IllegalArgumentException
|
||||
LocalVariablesSorter only accepts expanded frames (see ClassReader.EXPAND_FRAMES)
|
||||
PASS the message names the missing flag
|
||||
readerFlags = EXPAND_FRAMES -> 2152 bytes, verify() problems: 0
|
||||
PASS with EXPAND_FRAMES the output verifies
|
||||
exit=0
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ankurm.javademos</groupId>
|
||||
<artifactId>classfile-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>classfile-api</name>
|
||||
<description>java.lang.classfile (JEP 484, final in JDK 24): read, generate and transform bytecode, compared with ASM</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>25</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<asm.version>9.11.1</asm.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- ASM is used ONLY by the comparison classes (Asm*.java) and Bench; the java.lang.classfile demos need no dependency. -->
|
||||
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>${asm.version}</version></dependency>
|
||||
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm-commons</artifactId><version>${asm.version}</version></dependency>
|
||||
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm-util</artifactId><version>${asm.version}</version></dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration><compilerArgs><arg>-Xlint:all</arg></compilerArgs></configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Owner-aware diff of two `javap -public` dumps (one per JDK). Usage: api_diff.py OLD.txt NEW.txt"""
|
||||
import sys
|
||||
|
||||
def load(p):
|
||||
cur, d = None, {}
|
||||
for line in open(p):
|
||||
line = line.rstrip("\n")
|
||||
if not line.strip():
|
||||
continue
|
||||
if not line.startswith(" "):
|
||||
cur = line.rstrip(" {").split(" extends ")[0].split(" implements ")[0].rsplit(" ", 1)[-1]
|
||||
d[cur] = {"__header__": line}
|
||||
elif line.strip() != "}":
|
||||
d[cur][line.strip()] = True
|
||||
return d
|
||||
|
||||
a, b = load(sys.argv[1]), load(sys.argv[2])
|
||||
changes = 0
|
||||
for c in sorted(set(a) | set(b)):
|
||||
if c not in a: print(f"+ class {c}"); changes += 1; continue
|
||||
if c not in b: print(f"- class {c}"); changes += 1; continue
|
||||
if a[c]["__header__"] != b[c]["__header__"]:
|
||||
print(f"~ {c}\n was: {a[c]['__header__']}\n now: {b[c]['__header__']}"); changes += 1
|
||||
for m in sorted(set(a[c]) - set(b[c]) - {"__header__"}): print(f"- {c}: {m}"); changes += 1
|
||||
for m in sorted(set(b[c]) - set(a[c]) - {"__header__"}): print(f"+ {c}: {m}"); changes += 1
|
||||
print(f"{len(a)} classes in old, {len(b)} in new, {changes} difference(s)")
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# classfile-api: regenerates every file in output/.
|
||||
# JDK25=/path/to/jdk-25 [JDK24=/path/to/jdk-24 JDK26=/path/to/jdk-26] ./scripts/run-all.sh
|
||||
# Needs Maven (or an existing target/lib with the ASM jars) for the ASM comparison. JDK24/JDK26 are optional: output/07 is skipped without them.
|
||||
set -uo pipefail
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$HERE"
|
||||
JDK25="${JDK25:-/opt/jdks/jdk-25.0.5+8}"
|
||||
J="$JDK25/bin"
|
||||
OUT="$HERE/output"; mkdir -p "$OUT"
|
||||
CP="target/c:target/lib/*"
|
||||
JV="$("$J/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p | sed 's/.*(build \(.*\))/\1/')"
|
||||
|
||||
# 1. ASM jars (the only dependency) and compile. Unsetting JAVA_TOOL_OPTIONS keeps proxy banners out of the transcripts.
|
||||
if [ ! -d target/lib ]; then
|
||||
JAVA_HOME="$JDK25" mvn -B -q -T1 dependency:copy-dependencies -DoutputDirectory=target/lib || exit 1
|
||||
fi
|
||||
unset JAVA_TOOL_OPTIONS
|
||||
rm -rf target/c target/gen target/instrumented; mkdir -p target/c
|
||||
echo "javac: $("$J/javac" -version 2>&1)"
|
||||
"$J/javac" --release 25 -Xlint:all -cp "target/lib/*" -d target/c src/main/java/demo/*.java || exit 1
|
||||
|
||||
run() { # run <outfile> <main class> [args] -- prints the command, then the output, then the exit code
|
||||
local f="$1"; shift
|
||||
{ echo "\$ java -cp classes:asm-9.11.1.jar... $* ($JV)"; "$J/java" -Xmx512m -cp "$CP" "$@" 2>&1; echo "exit=$?"; } > "$OUT/$f"
|
||||
}
|
||||
|
||||
run 01-parse.txt demo.ParseDemo
|
||||
run 02-generate.txt demo.GenerateDemo target/gen/demo
|
||||
{ echo "\$ javap -c -p -v demo.Generated (the class GenerateDemo wrote; $JV)"
|
||||
"$J/javap" -c -p -v -cp target/gen demo.Generated 2>&1 | grep -v -e '^Classfile ' -e '^ Last modified' -e '^ SHA-256'; } > "$OUT/03-javap-generated.txt"
|
||||
run 04-broken-generate.txt demo.BrokenGenerate
|
||||
run 05-transform.txt demo.TransformDemo target/instrumented
|
||||
meth() { awk -v pat="$1" '$0 ~ pat {f=1} f{print} f && /^$/ {f=0}'; } # one method block of javap -c output
|
||||
{ echo "\$ javap -c -p demo.Target (original, as javac produced it; fib and boom only)"
|
||||
"$J/javap" -c -p -cp target/c demo.Target | meth 'static int fib'
|
||||
"$J/javap" -c -p -cp target/c demo.Target | meth 'static void boom'
|
||||
echo "\$ javap -c -p demo.Target (naive transform: records on normal return only)"
|
||||
"$J/javap" -c -p -cp target/instrumented/naive demo.Target | meth 'static int fib'
|
||||
"$J/javap" -c -p -cp target/instrumented/naive demo.Target | meth 'static void boom'
|
||||
echo "\$ javap -c -p demo.Target (full transform: plus a catch-all handler)"
|
||||
"$J/javap" -c -p -cp target/instrumented/full demo.Target | meth 'static int fib'
|
||||
"$J/javap" -c -p -cp target/instrumented/full demo.Target | meth 'static void boom'
|
||||
} > "$OUT/06-javap-instrumented.txt"
|
||||
|
||||
# 2. ASM comparison, same tasks
|
||||
run 08-asm-parse.txt demo.AsmParse
|
||||
run 09-asm-generate.txt demo.AsmGenerate
|
||||
run 10-asm-instrument.txt demo.AsmInstrument
|
||||
run 15-asm-expand-frames.txt demo.AsmGotcha
|
||||
run 11-version-limits.txt demo.VersionLimits
|
||||
# size of the equivalent code, counting only the method(s) that do the job (non-blank, non-comment lines)
|
||||
body() { awk -v pat="$2" '$0 ~ pat {f=1} f && !/^[[:space:]]*(\/\/|\*|\/\*|$)/ {n++} f && /^ }$/ {f=0} END {print n+0}' "$1"; }
|
||||
{ echo "Lines of code in the method(s) that do the job (non-blank, non-comment), counted by scripts/run-all.sh:"
|
||||
printf "%-26s java.lang.classfile %3d ASM %3d\n" "generate demo.Generated" "$(body src/main/java/demo/GenerateDemo.java 'static byte\[\] generate')" "$(body src/main/java/demo/AsmGenerate.java 'static byte\[\] generate')"
|
||||
printf "%-26s java.lang.classfile %3d ASM %3d\n" "timing instrumentation" "$(( $(body src/main/java/demo/TimingTransform.java 'public static byte\[\] instrument') + $(body src/main/java/demo/TimingTransform.java 'private static final class Timer') ))" "$(body src/main/java/demo/AsmInstrument.java 'public static byte\[\] instrument')"
|
||||
} > "$OUT/12-line-counts.txt"
|
||||
# Bench: three separate JVMs, because a single run on a shared machine is not trustworthy
|
||||
if [ -z "${SKIP_BENCH:-}" ]; then
|
||||
{ for i in 1 2 3; do echo "--- run $i of 3 ---"; "$J/java" -Xmx512m -cp "$CP" demo.Bench 2>&1 | grep -v '^PASS'; done; } > "$OUT/13-bench.txt"
|
||||
fi
|
||||
|
||||
# 3. Public API of java.lang.classfile across JDK 24, 25, 26 (needs the other JDKs)
|
||||
if [ -n "${JDK24:-}" ] && [ -n "${JDK26:-}" ]; then
|
||||
api() { # api <jdk-home> <outfile>
|
||||
local classes
|
||||
classes=$("$1/bin/jimage" list "$1/lib/modules" | awk '/^Module: java.base/{f=1;next} /^Module:/{f=0} f' \
|
||||
| grep -E 'java/lang/classfile/.*\.class$' | grep -v -e package-info -e Snippets | sed 's/\.class$//;s#/#.#g;s/^ *//')
|
||||
# shellcheck disable=SC2086
|
||||
"$1/bin/javap" -public -cp none $classes 2>/dev/null | grep -v '^Compiled from' > "$2"
|
||||
}
|
||||
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||
api "$JDK24" "$T/24.txt"; api "$JDK25" "$T/25.txt"; api "$JDK26" "$T/26.txt"
|
||||
{ echo "\$ javap -public java.lang.classfile.** on each JDK, then diff by class (24: $("$JDK24/bin/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p), 26: $("$JDK26/bin/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p))"
|
||||
echo; echo "=== JDK 24 -> JDK 25 ==="; python3 -I scripts/api_diff.py "$T/24.txt" "$T/25.txt"
|
||||
echo; echo "=== JDK 25 -> JDK 26 ==="; python3 -I scripts/api_diff.py "$T/25.txt" "$T/26.txt"
|
||||
echo; echo "=== is it a preview API on JDK 24? ==="; echo "\$ javap -v java.lang.classfile.ClassFile | grep -ci PreviewFeature"
|
||||
for j in "$JDK24" "$JDK25" "$JDK26"; do echo "$("$j/bin/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p | sed 's/.*(build \(.*\))/\1/'): $("$j/bin/javap" -v java.lang.classfile.ClassFile | grep -ci PreviewFeature)"; done
|
||||
echo; echo "=== @since of ClassFile ==="; for j in "$JDK24" "$JDK25" "$JDK26"; do unzip -p "$j/lib/src.zip" java.base/java/lang/classfile/ClassFile.java | grep -m1 '@since' | sed "s|^|$(basename "$j"): |"; done
|
||||
} > "$OUT/07-api-diff-24-25-26.txt"
|
||||
else
|
||||
echo "JDK24/JDK26 not set: skipped output/07-api-diff-24-25-26.txt"
|
||||
fi
|
||||
echo done
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Who is migrating? Look inside spring-core's published jar instead of trusting a blog.
|
||||
# JDK25=/path/to/jdk-25 ./scripts/spring-core-probe.sh [spring-core-version] -> output/14-spring-core-probe.txt
|
||||
set -uo pipefail
|
||||
unset JAVA_TOOL_OPTIONS
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"; JDK25="${JDK25:-/opt/jdks/jdk-25.0.5+8}"; V="${1:-7.0.10}"
|
||||
W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT
|
||||
URL="https://repo1.maven.org/maven2/org/springframework/spring-core/$V/spring-core-$V.jar"
|
||||
for i in 1 2 3 4 5 6; do c=$(curl -s -o "$W/sc.jar" -w '%{http_code}' "$URL"); [ "$c" = 200 ] && break; sleep 20; done
|
||||
[ "$c" = 200 ] || { echo "download failed ($c)"; exit 1; }
|
||||
{
|
||||
echo "\$ unzip -l spring-core-$V.jar | grep 'META-INF/versions/24/.*\\.class' | wc -l"
|
||||
unzip -l "$W/sc.jar" | grep -E 'META-INF/versions/24/.*\.class' | wc -l
|
||||
echo; echo "\$ unzip -l spring-core-$V.jar | grep 'versions/24.*ClassFile'"
|
||||
unzip -l "$W/sc.jar" | grep -E 'versions/24/.*ClassFile' | awk '{print $4}'
|
||||
echo; echo "\$ unzip -l spring-core-$V.jar | grep -c 'org/springframework/asm/.*\\.class' (the repackaged ASM is still there)"
|
||||
unzip -l "$W/sc.jar" | grep -cE 'org/springframework/asm/.*\.class'
|
||||
echo; echo "\$ javap -v -cp <jar>!/META-INF/versions/24 ...ClassFileMetadataReader | grep java/lang/classfile (does it really call the JDK API?)"
|
||||
mkdir "$W/x"; unzip -q "$W/sc.jar" 'META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMetadataReader*' -d "$W/x"
|
||||
"$JDK25/bin/javap" -v -cp "$W/x/META-INF/versions/24" org.springframework.core.type.classreading.ClassFileMetadataReader | grep -o 'java/lang/classfile/[A-Za-z$/]*' | sort -u
|
||||
echo; echo "\$ unzip -p spring-core-$V.jar META-INF/MANIFEST.MF | grep -i multi-release"
|
||||
unzip -p "$W/sc.jar" META-INF/MANIFEST.MF | grep -i multi-release
|
||||
} > "$HERE/output/14-spring-core-probe.txt" 2>&1
|
||||
cat "$HERE/output/14-spring-core-probe.txt"
|
||||
@@ -0,0 +1,95 @@
|
||||
package demo;
|
||||
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
|
||||
/** The GenerateDemo task done with ASM 9.11.1: the same class, demo.Generated, same four methods. */
|
||||
public class AsmGenerate implements Opcodes {
|
||||
|
||||
static byte[] generate() {
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(V25, ACC_PUBLIC | ACC_SUPER, "demo/Generated", null, "java/lang/Object", null);
|
||||
|
||||
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
mv.visitCode();
|
||||
mv.visitVarInsn(ALOAD, 0);
|
||||
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
mv.visitInsn(RETURN);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
|
||||
mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "add", "(II)I", null, null);
|
||||
mv.visitCode();
|
||||
mv.visitVarInsn(ILOAD, 0);
|
||||
mv.visitVarInsn(ILOAD, 1);
|
||||
mv.visitInsn(IADD);
|
||||
mv.visitInsn(IRETURN);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
|
||||
mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "hello", "()Ljava/lang/String;", null, null);
|
||||
mv.visitCode();
|
||||
mv.visitLdcInsn("Hello from generated bytecode");
|
||||
mv.visitInsn(ARETURN);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
|
||||
mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "sumTo", "(I)J", null, null);
|
||||
mv.visitCode();
|
||||
Label loop = new Label(), end = new Label();
|
||||
mv.visitInsn(LCONST_0);
|
||||
mv.visitVarInsn(LSTORE, 1);
|
||||
mv.visitInsn(ICONST_1);
|
||||
mv.visitVarInsn(ISTORE, 3);
|
||||
mv.visitLabel(loop);
|
||||
mv.visitVarInsn(ILOAD, 3);
|
||||
mv.visitVarInsn(ILOAD, 0);
|
||||
mv.visitJumpInsn(IF_ICMPGT, end);
|
||||
mv.visitVarInsn(LLOAD, 1);
|
||||
mv.visitVarInsn(ILOAD, 3);
|
||||
mv.visitInsn(I2L);
|
||||
mv.visitInsn(LADD);
|
||||
mv.visitVarInsn(LSTORE, 1);
|
||||
mv.visitIincInsn(3, 1);
|
||||
mv.visitJumpInsn(GOTO, loop);
|
||||
mv.visitLabel(end);
|
||||
mv.visitVarInsn(LLOAD, 1);
|
||||
mv.visitInsn(LRETURN);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
|
||||
mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "classify", "(I)Ljava/lang/String;", null, null);
|
||||
mv.visitCode();
|
||||
Label nonNeg = new Label(), done = new Label();
|
||||
mv.visitVarInsn(ILOAD, 0);
|
||||
mv.visitJumpInsn(IFGE, nonNeg);
|
||||
mv.visitLdcInsn("negative");
|
||||
mv.visitJumpInsn(GOTO, done);
|
||||
mv.visitLabel(nonNeg);
|
||||
mv.visitLdcInsn("non-negative");
|
||||
mv.visitLabel(done);
|
||||
mv.visitInsn(ARETURN);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
|
||||
cw.visitEnd();
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Throwable {
|
||||
byte[] asm = generate();
|
||||
byte[] cfApi = GenerateDemo.generate(java.lang.classfile.ClassFile.of());
|
||||
System.out.println("ASM generated " + asm.length + " bytes; java.lang.classfile generated " + cfApi.length + " bytes");
|
||||
Check.that(java.lang.classfile.ClassFile.of().verify(asm).isEmpty(), "ASM output passes ClassFile.verify()");
|
||||
Class<?> c = MethodHandles.lookup().defineClass(asm);
|
||||
GenerateDemo.exercise(c, "ASM-generated");
|
||||
var a = java.lang.classfile.ClassFile.of().parse(asm);
|
||||
var b = java.lang.classfile.ClassFile.of().parse(cfApi);
|
||||
Check.that(ParseDemo.methodLines(a).equals(ParseDemo.methodLines(b)), "both generators emit the same method list");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package demo;
|
||||
|
||||
/** The first thing that went wrong writing the ASM version: AdviceAdapter without ClassReader.EXPAND_FRAMES. */
|
||||
public class AsmGotcha {
|
||||
public static void main(String[] args) {
|
||||
AsmInstrument.readerFlags = 0; // what you get if you copy a ClassVisitor example and pass 0
|
||||
try {
|
||||
AsmInstrument.instrument(Bytes.of(Target.class));
|
||||
Check.that(false, "AdviceAdapter without EXPAND_FRAMES should have thrown");
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.out.println("readerFlags = 0 -> " + e.getClass().getName());
|
||||
System.out.println(" " + e.getMessage());
|
||||
Check.that(e.getMessage().contains("EXPAND_FRAMES"), "the message names the missing flag");
|
||||
}
|
||||
AsmInstrument.readerFlags = org.objectweb.asm.ClassReader.EXPAND_FRAMES;
|
||||
byte[] ok = AsmInstrument.instrument(Bytes.of(Target.class));
|
||||
System.out.println("readerFlags = EXPAND_FRAMES -> " + ok.length + " bytes, verify() problems: " + java.lang.classfile.ClassFile.of().verify(ok).size());
|
||||
Check.that(java.lang.classfile.ClassFile.of().verify(ok).isEmpty(), "with EXPAND_FRAMES the output verifies");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package demo;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.commons.AdviceAdapter;
|
||||
|
||||
/** The TimingTransform task done with ASM 9.11.1 (AdviceAdapter from asm-commons): same semantics as the "full" ClassFile version. */
|
||||
public class AsmInstrument {
|
||||
static int readerFlags = ClassReader.EXPAND_FRAMES; // AdviceAdapter extends LocalVariablesSorter, which throws without it
|
||||
|
||||
public static byte[] instrument(byte[] original) {
|
||||
ClassReader cr = new ClassReader(original);
|
||||
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
String simple = cr.getClassName().substring(cr.getClassName().lastIndexOf('/') + 1);
|
||||
cr.accept(new ClassVisitor(Opcodes.ASM9, cw) {
|
||||
@Override public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] ex) {
|
||||
MethodVisitor mv = super.visitMethod(access, name, desc, sig, ex);
|
||||
if (name.equals("<init>") || (access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) return mv;
|
||||
return new AdviceAdapter(Opcodes.ASM9, mv, access, name, desc) {
|
||||
final Label tryStart = new Label(), tryEnd = new Label(), handler = new Label();
|
||||
int startSlot;
|
||||
|
||||
@Override protected void onMethodEnter() {
|
||||
startSlot = newLocal(Type.LONG_TYPE);
|
||||
invokeStatic(Type.getType(System.class), new org.objectweb.asm.commons.Method("nanoTime", "()J"));
|
||||
storeLocal(startSlot);
|
||||
visitLabel(tryStart);
|
||||
}
|
||||
|
||||
@Override protected void onMethodExit(int opcode) {
|
||||
if (opcode != ATHROW) emitRecord(); // athrow is covered by the catch-all handler below
|
||||
}
|
||||
|
||||
@Override public void visitMaxs(int maxStack, int maxLocals) {
|
||||
visitLabel(tryEnd);
|
||||
visitLabel(handler);
|
||||
int ex = newLocal(Type.getType(Throwable.class));
|
||||
storeLocal(ex);
|
||||
emitRecord();
|
||||
loadLocal(ex);
|
||||
visitInsn(ATHROW);
|
||||
visitTryCatchBlock(tryStart, tryEnd, handler, null);
|
||||
super.visitMaxs(maxStack, maxLocals);
|
||||
}
|
||||
|
||||
private void emitRecord() {
|
||||
visitLdcInsn(simple + "." + name);
|
||||
invokeStatic(Type.getType(System.class), new org.objectweb.asm.commons.Method("nanoTime", "()J"));
|
||||
loadLocal(startSlot);
|
||||
visitInsn(LSUB);
|
||||
invokeStatic(Type.getObjectType("demo/Timings"), new org.objectweb.asm.commons.Method("record", "(Ljava/lang/String;J)V"));
|
||||
}
|
||||
};
|
||||
}
|
||||
}, readerFlags);
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Throwable {
|
||||
byte[] original = Bytes.of(Target.class);
|
||||
byte[] viaAsm = instrument(original);
|
||||
System.out.println("original " + original.length + " bytes, ASM-instrumented " + viaAsm.length + " bytes");
|
||||
Check.that(java.lang.classfile.ClassFile.of().verify(viaAsm).isEmpty(), "ASM output passes ClassFile.verify()");
|
||||
|
||||
TransformDemo.report("ASM AdviceAdapter version", viaAsm);
|
||||
Check.that(Timings.get("Target.boom").count() == 1 && Timings.get("Target.explode").count() == 1, "ASM version also records both throwing methods once");
|
||||
Check.that(Timings.get("Target.fib").count() == 177, "ASM version: fib recorded 177 times");
|
||||
Check.that(Timings.get("Target.pause").totalNanos() >= 50_000_000L, "ASM version: pause(50) recorded as at least 50 ms");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package demo;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/** The ParseDemo task done with ASM 9.11.1 (visitor API): list methods, count invoke targets. */
|
||||
public class AsmParse {
|
||||
record Result(List<String> methodLines, TreeMap<String, Integer> invokes, int major) { }
|
||||
|
||||
static Result parse(byte[] bytes) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
TreeMap<String, Integer> invokes = new TreeMap<>();
|
||||
ClassReader cr = new ClassReader(bytes);
|
||||
cr.accept(new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] ex) {
|
||||
lines.add(flags(access) + name + desc);
|
||||
return new MethodVisitor(Opcodes.ASM9) {
|
||||
@Override public void visitMethodInsn(int op, String owner, String n, String d, boolean itf) {
|
||||
invokes.merge(owner + "." + n, 1, Integer::sum);
|
||||
}
|
||||
@Override public void visitInvokeDynamicInsn(String n, String d, org.objectweb.asm.Handle bsm, Object... args) {
|
||||
invokes.merge("indy:" + n, 1, Integer::sum);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, ClassReader.SKIP_DEBUG);
|
||||
return new Result(lines, invokes, cr.readShort(6));
|
||||
}
|
||||
|
||||
/** Same format as ParseDemo.flags: lower-case names, sorted, comma-joined. */
|
||||
static String flags(int access) {
|
||||
List<String> f = new ArrayList<>();
|
||||
if ((access & Opcodes.ACC_PUBLIC) != 0) f.add("public");
|
||||
if ((access & Opcodes.ACC_STATIC) != 0) f.add("static");
|
||||
return String.join(",", f) + " ";
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
byte[] bytes = Bytes.of(Target.class);
|
||||
Result r = parse(bytes);
|
||||
System.out.println("ASM sees major version " + r.major());
|
||||
r.methodLines().forEach(l -> System.out.println(" " + l));
|
||||
r.invokes().forEach((k, v) -> System.out.println(" " + v + " x " + k));
|
||||
|
||||
var cf = java.lang.classfile.ClassFile.of().parse(bytes);
|
||||
Check.that(r.methodLines().equals(ParseDemo.methodLines(cf)), "ASM and java.lang.classfile list identical method lines");
|
||||
Check.that(r.invokes().equals(ParseDemo.invokeCounts(cf)), "ASM and java.lang.classfile count identical invoke targets");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package demo;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.classfile.ClassModel;
|
||||
import java.lang.classfile.ClassTransform;
|
||||
import java.lang.classfile.CodeElement;
|
||||
import java.lang.classfile.Instruction;
|
||||
import java.lang.classfile.MethodModel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.ToLongFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A deliberately simple, INDICATIVE comparison (no JMH): parse and visit every instruction of the first N java.base classes,
|
||||
* and parse + copy + write them back. Warm-up rounds first, then the median of the measured rounds.
|
||||
* Time is the benchmark thread's own CPU time (not wall-clock), because the sandbox is a shared 2-vCPU machine and wall-clock
|
||||
* numbers swing with whatever else is running. JIT and GC threads are therefore NOT included. Read the ratio, not the milliseconds.
|
||||
*/
|
||||
public class Bench {
|
||||
static final int N = 2000, WARMUP = 4, ROUNDS = 9;
|
||||
static final java.lang.management.ThreadMXBean CPU = java.lang.management.ManagementFactory.getThreadMXBean();
|
||||
|
||||
static long cfWalk(byte[] b) {
|
||||
ClassModel cm = ClassFile.of().parse(b);
|
||||
long n = 0;
|
||||
for (MethodModel m : cm.methods()) {
|
||||
if (m.code().isPresent()) {
|
||||
for (CodeElement e : m.code().get()) if (e instanceof Instruction) n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static long asmWalk(byte[] b) {
|
||||
long[] n = {0};
|
||||
new ClassReader(b).accept(new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override public MethodVisitor visitMethod(int a, String nm, String d, String s, String[] e) {
|
||||
return new MethodVisitor(Opcodes.ASM9) {
|
||||
@Override public void visitInsn(int o) { n[0]++; }
|
||||
@Override public void visitIntInsn(int o, int x) { n[0]++; }
|
||||
@Override public void visitVarInsn(int o, int x) { n[0]++; }
|
||||
@Override public void visitTypeInsn(int o, String t) { n[0]++; }
|
||||
@Override public void visitFieldInsn(int o, String a, String b, String c) { n[0]++; }
|
||||
@Override public void visitMethodInsn(int o, String a, String b, String c, boolean i) { n[0]++; }
|
||||
@Override public void visitInvokeDynamicInsn(String a, String b, org.objectweb.asm.Handle h, Object... r) { n[0]++; }
|
||||
@Override public void visitJumpInsn(int o, org.objectweb.asm.Label l) { n[0]++; }
|
||||
@Override public void visitLdcInsn(Object v) { n[0]++; }
|
||||
@Override public void visitIincInsn(int v, int i) { n[0]++; }
|
||||
@Override public void visitTableSwitchInsn(int a, int b, org.objectweb.asm.Label d, org.objectweb.asm.Label... l) { n[0]++; }
|
||||
@Override public void visitLookupSwitchInsn(org.objectweb.asm.Label d, int[] k, org.objectweb.asm.Label[] l) { n[0]++; }
|
||||
@Override public void visitMultiANewArrayInsn(String d, int x) { n[0]++; }
|
||||
};
|
||||
}
|
||||
}, 0);
|
||||
return n[0];
|
||||
}
|
||||
|
||||
static long cfCopy(byte[] b) {
|
||||
ClassFile cf = ClassFile.of();
|
||||
return cf.transformClass(cf.parse(b), ClassTransform.ACCEPT_ALL).length;
|
||||
}
|
||||
|
||||
static long asmCopy(byte[] b) {
|
||||
ClassReader cr = new ClassReader(b);
|
||||
ClassWriter cw = new ClassWriter(cr, 0);
|
||||
cr.accept(cw, 0);
|
||||
return cw.toByteArray().length;
|
||||
}
|
||||
|
||||
static double medianMs(List<byte[]> classes, ToLongFunction<byte[]> f) {
|
||||
double[] t = new double[ROUNDS];
|
||||
long sink = 0;
|
||||
for (int i = 0; i < WARMUP; i++) for (byte[] b : classes) sink += f.applyAsLong(b);
|
||||
for (int r = 0; r < ROUNDS; r++) {
|
||||
long t0 = CPU.getCurrentThreadCpuTime();
|
||||
for (byte[] b : classes) sink += f.applyAsLong(b);
|
||||
t[r] = (CPU.getCurrentThreadCpuTime() - t0) / 1e6;
|
||||
}
|
||||
if (sink == 42) System.out.print("");
|
||||
Arrays.sort(t);
|
||||
return t[ROUNDS / 2];
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<byte[]> classes = new ArrayList<>();
|
||||
try (Stream<Path> s = Files.walk(Bytes.jrtRoot("java.base"))) {
|
||||
for (Path p : (Iterable<Path>) s.filter(x -> x.toString().endsWith(".class") && !x.toString().endsWith("module-info.class")).sorted().limit(N)::iterator) {
|
||||
classes.add(Files.readAllBytes(p));
|
||||
}
|
||||
}
|
||||
long total = classes.stream().mapToLong(b -> b.length).sum();
|
||||
long insnCf = 0, insnAsm = 0;
|
||||
for (byte[] b : classes) { insnCf += cfWalk(b); insnAsm += asmWalk(b); }
|
||||
System.out.println("running on Java " + Runtime.version() + ", ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH");
|
||||
System.out.println(classes.size() + " classes from java.base, " + total / 1024 + " KiB; " + WARMUP + " warm-up rounds, median of " + ROUNDS);
|
||||
System.out.println("instructions seen: java.lang.classfile " + insnCf + ", ASM " + insnAsm);
|
||||
Check.that(insnCf > 0 && insnCf == insnAsm, "both walkers see exactly the same number of instructions");
|
||||
|
||||
double cfW = medianMs(classes, Bench::cfWalk), asmW = medianMs(classes, Bench::asmWalk);
|
||||
double cfC = medianMs(classes, Bench::cfCopy), asmC = medianMs(classes, Bench::asmCopy);
|
||||
System.out.printf("parse + visit every instruction : java.lang.classfile %7.1f ms ASM %7.1f ms (ASM / classfile = %.2f)%n", cfW, asmW, asmW / cfW);
|
||||
System.out.printf("parse + copy + write back : java.lang.classfile %7.1f ms ASM %7.1f ms (ASM / classfile = %.2f)%n", cfC, asmC, asmC / cfC);
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package demo;
|
||||
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.classfile.Label;
|
||||
import java.lang.classfile.TypeKind;
|
||||
import java.lang.constant.ClassDesc;
|
||||
import java.lang.constant.MethodTypeDesc;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.List;
|
||||
|
||||
import static java.lang.constant.ConstantDescs.*;
|
||||
|
||||
/** Three ways to produce a bad class, and what each one looks like. */
|
||||
public class BrokenGenerate {
|
||||
static final ClassDesc C = ClassDesc.of("demo", "Broken");
|
||||
|
||||
/** static int pick(int n) { return n > 0 ? 1 : 2; } -- needs a StackMapTable because it branches. */
|
||||
static byte[] branching(ClassFile cf) {
|
||||
return cf.build(C, clb -> clb.withMethodBody("pick", MethodTypeDesc.of(CD_int, CD_int), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> {
|
||||
Label other = cob.newLabel(), done = cob.newLabel();
|
||||
cob.iload(0).ifle(other).iconst_1().goto_(done).labelBinding(other).iconst_2().labelBinding(done).ireturn();
|
||||
}));
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Throwable {
|
||||
// 1. Same method, stack maps dropped on request.
|
||||
byte[] noMaps = branching(ClassFile.of(ClassFile.StackMapsOption.DROP_STACK_MAPS));
|
||||
List<VerifyError> v = ClassFile.of().verify(noMaps);
|
||||
System.out.println("DROP_STACK_MAPS: verify() -> " + v.size() + " problem(s)");
|
||||
v.forEach(e -> System.out.println(" " + e.getMessage().lines().findFirst().orElse("")));
|
||||
try {
|
||||
MethodHandles.lookup().defineClass(noMaps);
|
||||
} catch (VerifyError e) {
|
||||
System.out.println("DROP_STACK_MAPS: defineClass -> java.lang.VerifyError");
|
||||
e.getMessage().lines().limit(3).forEach(l -> System.out.println(" " + l));
|
||||
}
|
||||
Check.that(!v.isEmpty(), "a branching method built without stack maps fails verification");
|
||||
|
||||
// 2. The default (stack maps generated when required) is fine for the very same code.
|
||||
Check.that(ClassFile.of().verify(branching(ClassFile.of())).isEmpty(), "the same code with default options verifies");
|
||||
|
||||
// 3. Type confusion: ireturn on a method declared to return String. The builder does not stop you.
|
||||
byte[] wrong = ClassFile.of().build(ClassDesc.of("demo", "Wrong"), clb -> clb.withMethodBody("bad",
|
||||
MethodTypeDesc.of(CD_String), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> cob.iconst_1().ireturn()));
|
||||
System.out.println("ireturn in a String method: build() succeeded (" + wrong.length + " bytes)");
|
||||
List<VerifyError> w = ClassFile.of().verify(wrong);
|
||||
w.forEach(e -> System.out.println(" verify: " + e.getMessage().lines().findFirst().orElse("")));
|
||||
Check.that(!w.isEmpty(), "verify() catches a wrongly typed return");
|
||||
|
||||
// 4. Stack underflow while generating with stack maps: the generator itself throws.
|
||||
try {
|
||||
ClassFile.of().build(ClassDesc.of("demo", "Under"), clb -> clb.withMethodBody("u", MethodTypeDesc.of(CD_int),
|
||||
ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> {
|
||||
Label l = cob.newLabel();
|
||||
cob.iadd().goto_(l).labelBinding(l).iconst_0().ireturn();
|
||||
}));
|
||||
System.out.println("underflow: build() succeeded");
|
||||
Check.that(false, "stack underflow is caught while building");
|
||||
} catch (RuntimeException e) {
|
||||
Check.that(e instanceof IllegalArgumentException, "stack underflow is caught while building");
|
||||
System.out.println("underflow: build() threw " + e.getClass().getName());
|
||||
System.out.println(" " + e.getMessage().lines().findFirst().orElse(""));
|
||||
}
|
||||
|
||||
// 5. exceptionCatchAll(start, end, handler): three Labels, so swapping them compiles. verify() says nothing; the JVM objects.
|
||||
byte[] swapped = ClassFile.of().build(ClassDesc.of("demo", "Swapped"), clb -> clb.withMethodBody("s",
|
||||
MethodTypeDesc.of(CD_int), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> {
|
||||
Label start = cob.newLabel(), end = cob.newLabel(), handler = cob.newLabel();
|
||||
cob.labelBinding(start).iconst_1().labelBinding(end).ireturn()
|
||||
.labelBinding(handler).pop().iconst_0().ireturn()
|
||||
.exceptionCatchAll(handler, start, end); // WRONG order on purpose; correct is (start, end, handler)
|
||||
}));
|
||||
System.out.println("swapped labels: build() succeeded (" + swapped.length + " bytes), verify() -> " + ClassFile.of().verify(swapped).size() + " problem(s)");
|
||||
try {
|
||||
MethodHandles.lookup().defineClass(swapped);
|
||||
Check.that(false, "the JVM rejects the swapped exception-table labels");
|
||||
} catch (ClassFormatError e) {
|
||||
System.out.println("swapped labels: defineClass -> " + e.getClass().getName() + ": " + e.getMessage());
|
||||
Check.that(true, "the JVM rejects the swapped exception-table labels");
|
||||
}
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package demo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Where class-file bytes come from in the demos. */
|
||||
public final class Bytes {
|
||||
private Bytes() { }
|
||||
|
||||
/** The bytes javac produced for one of our own classes, read from the class path. */
|
||||
public static byte[] of(Class<?> c) {
|
||||
try (InputStream in = c.getResourceAsStream(c.getSimpleName() + ".class")) {
|
||||
return in.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** A JDK class, read from the runtime image through the jrt: file system. */
|
||||
public static byte[] jdk(String module, String internalName) {
|
||||
try {
|
||||
FileSystem jrt = FileSystems.getFileSystem(URI.create("jrt:/"));
|
||||
return Files.readAllBytes(jrt.getPath("modules", module, internalName + ".class"));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Path jrtRoot(String module) {
|
||||
return FileSystems.getFileSystem(URI.create("jrt:/")).getPath("modules", module);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package demo;
|
||||
|
||||
/** Tiny assertion helper: prints PASS/FAIL and fails the process at the end, like the other standalone modules. */
|
||||
public final class Check {
|
||||
private static int failures;
|
||||
|
||||
private Check() { }
|
||||
|
||||
public static void that(boolean ok, String what) {
|
||||
System.out.println((ok ? "PASS " : "FAIL ") + what);
|
||||
if (!ok) failures++;
|
||||
}
|
||||
|
||||
public static void finish() {
|
||||
if (failures > 0) {
|
||||
System.out.println(failures + " check(s) FAILED");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package demo;
|
||||
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.classfile.Label;
|
||||
import java.lang.classfile.TypeKind;
|
||||
import java.lang.constant.ClassDesc;
|
||||
import java.lang.constant.MethodTypeDesc;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static java.lang.constant.ConstantDescs.*;
|
||||
|
||||
/** Generating a class from scratch with ClassFile.build, then defining it at runtime. */
|
||||
public class GenerateDemo {
|
||||
static final ClassDesc GENERATED = ClassDesc.of("demo", "Generated");
|
||||
|
||||
/** public class demo.Generated { add(int,int), hello(), sumTo(int), classify(int) }. */
|
||||
static byte[] generate(ClassFile cf) {
|
||||
return cf.build(GENERATED, clb -> clb
|
||||
.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_SUPER)
|
||||
.withMethodBody(INIT_NAME, MTD_void, ClassFile.ACC_PUBLIC, cob -> cob
|
||||
.aload(0)
|
||||
.invokespecial(CD_Object, INIT_NAME, MTD_void)
|
||||
.return_())
|
||||
// static int add(int a, int b) { return a + b; }
|
||||
.withMethodBody("add", MethodTypeDesc.of(CD_int, CD_int, CD_int), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> cob
|
||||
.iload(0).iload(1).iadd().ireturn())
|
||||
// static String hello() { return "Hello from generated bytecode"; }
|
||||
.withMethodBody("hello", MethodTypeDesc.of(CD_String), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> cob
|
||||
.ldc("Hello from generated bytecode").areturn())
|
||||
// static long sumTo(int n) { long s = 0; for (int i = 1; i <= n; i++) s += i; return s; }
|
||||
.withMethodBody("sumTo", MethodTypeDesc.of(CD_long, CD_int), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> {
|
||||
int s = cob.allocateLocal(TypeKind.LONG);
|
||||
int i = cob.allocateLocal(TypeKind.INT);
|
||||
Label loop = cob.newLabel(), end = cob.newLabel();
|
||||
cob.lconst_0().lstore(s)
|
||||
.iconst_1().istore(i)
|
||||
.labelBinding(loop)
|
||||
.iload(i).iload(0).if_icmpgt(end)
|
||||
.lload(s).iload(i).i2l().ladd().lstore(s)
|
||||
.iinc(i, 1)
|
||||
.goto_(loop)
|
||||
.labelBinding(end)
|
||||
.lload(s).lreturn();
|
||||
})
|
||||
// static String classify(int n) { return n < 0 ? "negative" : "non-negative"; }
|
||||
.withMethodBody("classify", MethodTypeDesc.of(CD_String, CD_int), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, cob -> cob
|
||||
.iload(0)
|
||||
.ifThenElse(java.lang.classfile.Opcode.IFLT,
|
||||
t -> t.ldc("negative"),
|
||||
e -> e.ldc("non-negative"))
|
||||
.areturn()));
|
||||
}
|
||||
|
||||
static void exercise(Class<?> c, String how) throws Throwable {
|
||||
MethodHandles.Lookup l = MethodHandles.publicLookup();
|
||||
MethodHandle add = l.findStatic(c, "add", MethodType.methodType(int.class, int.class, int.class));
|
||||
MethodHandle hello = l.findStatic(c, "hello", MethodType.methodType(String.class));
|
||||
MethodHandle sum = l.findStatic(c, "sumTo", MethodType.methodType(long.class, int.class));
|
||||
MethodHandle cls = l.findStatic(c, "classify", MethodType.methodType(String.class, int.class));
|
||||
int a = (int) add.invoke(2, 3);
|
||||
String h = (String) hello.invoke();
|
||||
long s = (long) sum.invoke(100);
|
||||
String neg = (String) cls.invoke(-4), pos = (String) cls.invoke(4);
|
||||
System.out.println("[" + how + "] add(2,3)=" + a + " hello()=" + h + " sumTo(100)=" + s
|
||||
+ " classify(-4)=" + neg + " classify(4)=" + pos);
|
||||
Check.that(a == 5 && s == 5050L && "negative".equals(neg) && "non-negative".equals(pos), how + ": generated methods return the right values");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Throwable {
|
||||
ClassFile cf = ClassFile.of();
|
||||
byte[] bytes = generate(cf);
|
||||
Path outDir = Path.of(args.length > 0 ? args[0] : "target/gen/demo");
|
||||
Files.createDirectories(outDir);
|
||||
Files.write(outDir.resolve("Generated.class"), bytes);
|
||||
System.out.println("generated " + bytes.length + " bytes, major version " + cf.parse(bytes).majorVersion()
|
||||
+ " (wrote " + outDir.resolve("Generated.class") + ")");
|
||||
|
||||
// 1. Verify without loading: returns a list of VerifyError, empty if the class is well formed.
|
||||
List<VerifyError> problems = cf.verify(bytes);
|
||||
System.out.println("ClassFile.verify(bytes) -> " + problems.size() + " problem(s)");
|
||||
Check.that(problems.isEmpty(), "verify() finds nothing wrong with the generated class");
|
||||
|
||||
// 2. Define it as an ordinary class in this package (Lookup.defineClass needs the same package as the lookup class).
|
||||
MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
Class<?> normal = lookup.defineClass(bytes);
|
||||
System.out.println("defineClass -> " + normal.getName() + " hidden=" + normal.isHidden()
|
||||
+ " findable by name=" + (Class.forName("demo.Generated") == normal));
|
||||
exercise(normal, "defineClass");
|
||||
|
||||
// 3. Define the same bytes as a hidden class: no name anyone can resolve, unloadable with its defining loader's GC.
|
||||
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(bytes, true);
|
||||
Class<?> hidden = hiddenLookup.lookupClass();
|
||||
System.out.println("defineHiddenClass -> name starts with 'demo.Generated/': " + hidden.getName().startsWith("demo.Generated/")
|
||||
+ " hidden=" + hidden.isHidden() + " distinct from the normal class=" + (hidden != normal));
|
||||
exercise(hidden, "hidden");
|
||||
try {
|
||||
Class.forName(hidden.getName());
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.out.println("Class.forName(hidden name) -> ClassNotFoundException (hidden classes cannot be found by name)");
|
||||
}
|
||||
Check.that(hidden.isHidden() && !normal.isHidden(), "hidden class is hidden, the other is not");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package demo;
|
||||
|
||||
import java.lang.classfile.ClassElement;
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.classfile.ClassModel;
|
||||
import java.lang.classfile.CodeElement;
|
||||
import java.lang.classfile.FieldModel;
|
||||
import java.lang.classfile.Instruction;
|
||||
import java.lang.classfile.MethodModel;
|
||||
import java.lang.classfile.attribute.SourceFileAttribute;
|
||||
import java.lang.classfile.instruction.InvokeDynamicInstruction;
|
||||
import java.lang.classfile.instruction.InvokeInstruction;
|
||||
import java.lang.reflect.AccessFlag;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Reading a class with java.lang.classfile: ClassFile.of().parse(...) and walking the model. */
|
||||
public class ParseDemo {
|
||||
|
||||
/** One line per method: flags name descriptor. Shared with the ASM comparison, which must print the same lines. */
|
||||
static List<String> methodLines(ClassModel cm) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (MethodModel m : cm.methods()) {
|
||||
lines.add(flags(m.flags().flags()) + m.methodName().stringValue() + m.methodType().stringValue());
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
static String flags(java.util.Set<AccessFlag> fs) {
|
||||
return fs.stream().map(f -> f.name().toLowerCase()).sorted().collect(Collectors.joining(",")) + " ";
|
||||
}
|
||||
|
||||
/** Calls per "owner.name" in all method bodies, using a pattern-matching switch over code elements. */
|
||||
static TreeMap<String, Integer> invokeCounts(ClassModel cm) {
|
||||
TreeMap<String, Integer> counts = new TreeMap<>();
|
||||
for (MethodModel m : cm.methods()) {
|
||||
m.code().ifPresent(code -> {
|
||||
for (CodeElement e : code) {
|
||||
switch (e) {
|
||||
case InvokeInstruction i -> counts.merge(
|
||||
i.owner().asInternalName() + "." + i.name().stringValue(), 1, Integer::sum);
|
||||
case InvokeDynamicInstruction i -> counts.merge(
|
||||
"indy:" + i.name().stringValue(), 1, Integer::sum);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
byte[] bytes = Bytes.of(Target.class);
|
||||
ClassModel cm = ClassFile.of().parse(bytes);
|
||||
|
||||
System.out.println("== header ==");
|
||||
System.out.println("bytes : " + bytes.length);
|
||||
System.out.println("this : " + cm.thisClass().asInternalName());
|
||||
System.out.println("super : " + cm.superclass().orElseThrow().asInternalName());
|
||||
System.out.println("version : " + cm.majorVersion() + "." + cm.minorVersion()
|
||||
+ " (ClassFile.JAVA_25_VERSION = " + ClassFile.JAVA_25_VERSION + ")");
|
||||
System.out.println("flags : " + flags(cm.flags().flags()));
|
||||
System.out.println("constant pool : " + cm.constantPool().size() + " slots");
|
||||
|
||||
System.out.println("\n== fields ==");
|
||||
for (FieldModel f : cm.fields()) {
|
||||
System.out.println(flags(f.flags().flags()) + f.fieldName().stringValue() + " " + f.fieldType().stringValue());
|
||||
}
|
||||
|
||||
System.out.println("\n== methods ==");
|
||||
methodLines(cm).forEach(System.out::println);
|
||||
|
||||
System.out.println("\n== class elements, in file order (the model is a stream of elements) ==");
|
||||
for (ClassElement ce : cm) {
|
||||
String d = switch (ce) {
|
||||
case MethodModel m -> "method " + m.methodName().stringValue();
|
||||
case FieldModel f -> "field " + f.fieldName().stringValue();
|
||||
case SourceFileAttribute a -> "SourceFile=" + a.sourceFile().stringValue();
|
||||
default -> ce.getClass().getSimpleName().replaceAll("Impl$", "");
|
||||
};
|
||||
System.out.println(" " + d);
|
||||
}
|
||||
|
||||
System.out.println("\n== fib(I)I, instruction by instruction ==");
|
||||
MethodModel fib = cm.methods().stream().filter(m -> m.methodName().equalsString("fib")).findFirst().orElseThrow();
|
||||
for (CodeElement e : fib.code().orElseThrow()) {
|
||||
if (e instanceof Instruction i) {
|
||||
String extra = e instanceof InvokeInstruction inv
|
||||
? " " + inv.owner().asInternalName() + "." + inv.name().stringValue() + inv.type().stringValue() : "";
|
||||
System.out.println(" " + i.opcode() + extra);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("\n== invoke targets in the whole class ==");
|
||||
TreeMap<String, Integer> counts = invokeCounts(cm);
|
||||
counts.forEach((k, v) -> System.out.println(" " + v + " x " + k));
|
||||
|
||||
System.out.println("\n== a JDK class from the runtime image: java.lang.String ==");
|
||||
byte[] sb = Bytes.jdk("java.base", "java/lang/String");
|
||||
ClassModel string = ClassFile.of().parse(sb);
|
||||
System.out.println("bytes " + sb.length + ", major " + string.majorVersion() + ", methods " + string.methods().size()
|
||||
+ ", fields " + string.fields().size() + ", interfaces " + string.interfaces().size());
|
||||
|
||||
Check.that(cm.majorVersion() == ClassFile.JAVA_25_VERSION, "Target.class is class-file version 69 (compiled with --release 25)");
|
||||
Check.that(cm.methods().size() == 8, "Target has 8 methods (constructor + 7)");
|
||||
Check.that(counts.getOrDefault("demo/Target.fib", 0) == 2, "fib calls itself twice");
|
||||
Check.that(counts.containsKey("indy:makeConcatWithConstants"), "greet() uses invokedynamic string concatenation");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package demo;
|
||||
|
||||
/** The class we inspect and instrument. Deliberately small, and it covers the interesting shapes:
|
||||
* a recursive method, a loop, string concatenation (invokedynamic), a method that sleeps and one that throws. */
|
||||
public class Target {
|
||||
private final String name;
|
||||
private int calls;
|
||||
|
||||
public Target(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static int fib(int n) {
|
||||
return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
|
||||
public static long sumTo(int n) {
|
||||
long s = 0;
|
||||
for (int i = 1; i <= n; i++) {
|
||||
s += i;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public String greet(String who) {
|
||||
calls++;
|
||||
return "Hello, " + who + " from " + name;
|
||||
}
|
||||
|
||||
public void pause(long millis) throws InterruptedException {
|
||||
Thread.sleep(millis);
|
||||
}
|
||||
|
||||
public static void boom() {
|
||||
throw new IllegalStateException("boom");
|
||||
}
|
||||
|
||||
public static int explode(int[] a) {
|
||||
return a[5]; // throws from the JVM (AIOOBE), not from an explicit athrow
|
||||
}
|
||||
|
||||
public int calls() {
|
||||
return calls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package demo;
|
||||
|
||||
import java.lang.classfile.ClassBuilder;
|
||||
import java.lang.classfile.ClassElement;
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.classfile.ClassModel;
|
||||
import java.lang.classfile.ClassTransform;
|
||||
import java.lang.classfile.CodeBuilder;
|
||||
import java.lang.classfile.CodeElement;
|
||||
import java.lang.classfile.CodeTransform;
|
||||
import java.lang.classfile.Label;
|
||||
import java.lang.classfile.MethodModel;
|
||||
import java.lang.classfile.MethodTransform;
|
||||
import java.lang.classfile.TypeKind;
|
||||
import java.lang.classfile.instruction.ReturnInstruction;
|
||||
import java.lang.constant.ClassDesc;
|
||||
import java.lang.constant.MethodTypeDesc;
|
||||
|
||||
import static java.lang.constant.ConstantDescs.*;
|
||||
|
||||
/**
|
||||
* Inserts timing around every method body of a class using java.lang.classfile transforms.
|
||||
*
|
||||
* <pre>
|
||||
* ClassTransform -- looks at each class element; for a method it hands the MethodModel to a MethodTransform
|
||||
* MethodTransform -- transformingCode(...) forwards the Code attribute to a CodeTransform
|
||||
* CodeTransform -- sees every instruction; atStart/atEnd bracket the whole body
|
||||
* </pre>
|
||||
*/
|
||||
public final class TimingTransform {
|
||||
static final ClassDesc SYSTEM = ClassDesc.of("java.lang.System");
|
||||
static final ClassDesc TIMINGS = ClassDesc.of("demo.Timings");
|
||||
static final MethodTypeDesc NANO_TIME = MethodTypeDesc.of(CD_long);
|
||||
static final MethodTypeDesc RECORD = MethodTypeDesc.of(CD_void, CD_String, CD_long);
|
||||
|
||||
private TimingTransform() { }
|
||||
|
||||
/** @param alsoOnException false = time only normal returns (the naive version); true = also when an exception leaves the method */
|
||||
public static byte[] instrument(byte[] original, boolean alsoOnException) {
|
||||
ClassFile cf = ClassFile.of();
|
||||
ClassModel model = cf.parse(original);
|
||||
String owner = model.thisClass().asInternalName();
|
||||
|
||||
ClassTransform perClass = (ClassBuilder cb, ClassElement ce) -> {
|
||||
if (ce instanceof MethodModel mm && mm.code().isPresent() && !mm.methodName().equalsString("<init>")) {
|
||||
String label = owner.substring(owner.lastIndexOf('/') + 1) + "." + mm.methodName().stringValue();
|
||||
cb.transformMethod(mm, MethodTransform.transformingCode(
|
||||
CodeTransform.ofStateful(() -> new Timer(label, alsoOnException))));
|
||||
} else {
|
||||
cb.with(ce); // everything else is copied unchanged
|
||||
}
|
||||
};
|
||||
return cf.transformClass(model, perClass);
|
||||
}
|
||||
|
||||
/** One instance per method body (ofStateful), so the local-variable slot and labels are per method. */
|
||||
private static final class Timer implements CodeTransform {
|
||||
private final String label;
|
||||
private final boolean alsoOnException;
|
||||
private int startSlot;
|
||||
private Label tryStart, tryEnd;
|
||||
|
||||
Timer(String label, boolean alsoOnException) {
|
||||
this.label = label;
|
||||
this.alsoOnException = alsoOnException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void atStart(CodeBuilder b) {
|
||||
startSlot = b.allocateLocal(TypeKind.LONG); // a fresh local; max_locals is recomputed for us
|
||||
b.invokestatic(SYSTEM, "nanoTime", NANO_TIME).lstore(startSlot);
|
||||
if (alsoOnException) {
|
||||
tryStart = b.newLabel();
|
||||
tryEnd = b.newLabel();
|
||||
b.labelBinding(tryStart);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(CodeBuilder b, CodeElement e) {
|
||||
if (e instanceof ReturnInstruction) {
|
||||
emitRecord(b); // the return value is on the stack; our code is stack-neutral
|
||||
}
|
||||
b.with(e); // forward the original element
|
||||
}
|
||||
|
||||
@Override
|
||||
public void atEnd(CodeBuilder b) {
|
||||
if (!alsoOnException) return;
|
||||
b.labelBinding(tryEnd);
|
||||
Label handler = b.newLabel();
|
||||
b.labelBinding(handler);
|
||||
int ex = b.allocateLocal(TypeKind.REFERENCE);
|
||||
b.astore(ex);
|
||||
emitRecord(b);
|
||||
b.aload(ex).athrow();
|
||||
b.exceptionCatchAll(tryStart, tryEnd, handler); // (start, end, handler): catch-any over the original body
|
||||
}
|
||||
|
||||
private void emitRecord(CodeBuilder b) {
|
||||
b.ldc(label)
|
||||
.invokestatic(SYSTEM, "nanoTime", NANO_TIME)
|
||||
.lload(startSlot)
|
||||
.lsub()
|
||||
.invokestatic(TIMINGS, "record", RECORD);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package demo;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
/** Sink for the timing code we inject. Public, because the injected bytecode calls it from another class loader. */
|
||||
public final class Timings {
|
||||
public record Stat(long count, long totalNanos) { }
|
||||
|
||||
private static final Map<String, LongAdder> COUNT = new ConcurrentHashMap<>();
|
||||
private static final Map<String, LongAdder> NANOS = new ConcurrentHashMap<>();
|
||||
|
||||
private Timings() { }
|
||||
|
||||
/** Called from instrumented code. Signature (String, long) is mirrored in the ClassFile and ASM transformers. */
|
||||
public static void record(String method, long nanos) {
|
||||
COUNT.computeIfAbsent(method, k -> new LongAdder()).increment();
|
||||
NANOS.computeIfAbsent(method, k -> new LongAdder()).add(nanos);
|
||||
}
|
||||
|
||||
public static Stat get(String method) {
|
||||
LongAdder c = COUNT.get(method);
|
||||
return c == null ? new Stat(0, 0) : new Stat(c.sum(), NANOS.get(method).sum());
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
COUNT.clear();
|
||||
NANOS.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package demo;
|
||||
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/** Instrument demo.Target with TimingTransform, load the result in an isolated class loader, call it, read the timings. */
|
||||
public class TransformDemo {
|
||||
|
||||
/** Defines `transformed` as demo.Target in its own loader; everything else (Timings, ...) comes from the parent. */
|
||||
static final class Loader extends ClassLoader {
|
||||
private final byte[] target;
|
||||
Loader(byte[] target) { super(TransformDemo.class.getClassLoader()); this.target = target; }
|
||||
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name.equals("demo.Target")) {
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
Class<?> c = findLoadedClass(name);
|
||||
if (c == null) c = defineClass(name, target, 0, target.length);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
}
|
||||
|
||||
static void drive(Class<?> target) throws Throwable {
|
||||
var fib = target.getMethod("fib", int.class);
|
||||
var sum = target.getMethod("sumTo", int.class);
|
||||
var boom = target.getMethod("boom");
|
||||
var explode = target.getMethod("explode", int[].class);
|
||||
var pause = target.getMethod("pause", long.class);
|
||||
var greet = target.getMethod("greet", String.class);
|
||||
Object t = target.getConstructor(String.class).newInstance("loader-2");
|
||||
|
||||
fib.invoke(null, 10);
|
||||
sum.invoke(null, 1000);
|
||||
greet.invoke(t, "Ankur");
|
||||
pause.invoke(t, 50L);
|
||||
try { boom.invoke(null); } catch (java.lang.reflect.InvocationTargetException e) { System.out.println(" boom() threw " + e.getCause()); }
|
||||
try { explode.invoke(null, (Object) new int[3]); } catch (java.lang.reflect.InvocationTargetException e) { System.out.println(" explode() threw " + e.getCause().getClass().getSimpleName()); }
|
||||
}
|
||||
|
||||
static void report(String title, byte[] bytes) throws Throwable {
|
||||
Timings.reset();
|
||||
System.out.println("-- " + title + " (" + bytes.length + " bytes; verify() problems: " + ClassFile.of().verify(bytes).size() + ")");
|
||||
Class<?> c = new Loader(bytes).loadClass("demo.Target");
|
||||
Check.that(c != Target.class, title + ": demo.Target here is a different Class object than the one on the class path");
|
||||
drive(c);
|
||||
for (String m : List.of("fib", "sumTo", "greet", "pause", "boom", "explode")) {
|
||||
Timings.Stat s = Timings.get("Target." + m);
|
||||
System.out.printf(" %-8s recorded %3d call(s)%n", m, s.count());
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Throwable {
|
||||
byte[] original = Bytes.of(Target.class);
|
||||
Path dir = Path.of(args.length > 0 ? args[0] : "target/instrumented");
|
||||
Files.createDirectories(dir.resolve("naive/demo"));
|
||||
Files.createDirectories(dir.resolve("full/demo"));
|
||||
|
||||
byte[] naive = TimingTransform.instrument(original, false);
|
||||
byte[] full = TimingTransform.instrument(original, true);
|
||||
Files.write(dir.resolve("naive/demo/Target.class"), naive);
|
||||
Files.write(dir.resolve("full/demo/Target.class"), full);
|
||||
System.out.println("original " + original.length + " bytes, naive " + naive.length + " bytes, with exception handler " + full.length + " bytes");
|
||||
|
||||
report("naive: time normal returns only", naive);
|
||||
Timings.Stat naiveBoom = Timings.get("Target.boom"), naiveExplode = Timings.get("Target.explode"), naiveFib = Timings.get("Target.fib");
|
||||
Check.that(naiveBoom.count() == 0 && naiveExplode.count() == 0, "naive version never records the two methods that throw");
|
||||
Check.that(naiveFib.count() == 177, "fib(10) makes 177 calls and 177 timings were recorded (recursion included)");
|
||||
|
||||
report("full: also time when an exception leaves the method", full);
|
||||
Check.that(Timings.get("Target.boom").count() == 1 && Timings.get("Target.explode").count() == 1, "exception-safe version records boom() and explode() once each");
|
||||
Check.that(Timings.get("Target.fib").count() == 177, "fib still recorded 177 times");
|
||||
Check.that(Timings.get("Target.sumTo").count() == 1 && Timings.get("Target.greet").count() == 1, "sumTo and greet recorded once each");
|
||||
Check.that(Timings.get("Target.pause").totalNanos() >= 50_000_000L, "pause(50) was recorded as at least 50 ms (>= 50,000,000 ns)");
|
||||
|
||||
// Behaviour is unchanged: the transformed class still computes the same answers.
|
||||
Class<?> c = new Loader(full).loadClass("demo.Target");
|
||||
Check.that((int) c.getMethod("fib", int.class).invoke(null, 20) == Target.fib(20), "fib(20) gives the same answer before and after");
|
||||
Check.that(((Long) c.getMethod("sumTo", int.class).invoke(null, 12345)) == Target.sumTo(12345), "sumTo(12345) gives the same answer before and after");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package demo;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.lang.classfile.ClassFile;
|
||||
|
||||
/** What happens when the class file is newer than the reader. Patches the major-version bytes of Target.class. */
|
||||
public class VersionLimits {
|
||||
static byte[] withMajor(byte[] original, int major) {
|
||||
byte[] b = original.clone();
|
||||
b[6] = (byte) (major >> 8);
|
||||
b[7] = (byte) major;
|
||||
return b;
|
||||
}
|
||||
|
||||
static String tryClassFile(byte[] b) {
|
||||
try {
|
||||
var cm = ClassFile.of().parse(b);
|
||||
return "parsed, " + cm.methods().size() + " methods";
|
||||
} catch (Throwable t) {
|
||||
return t.getClass().getName() + ": " + t.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
static String tryAsm(byte[] b) {
|
||||
try {
|
||||
int[] n = {0};
|
||||
new ClassReader(b).accept(new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override public org.objectweb.asm.MethodVisitor visitMethod(int a, String nm, String d, String s, String[] e) { n[0]++; return null; }
|
||||
}, 0);
|
||||
return "parsed, " + n[0] + " methods";
|
||||
} catch (Throwable t) {
|
||||
return t.getClass().getName() + ": " + t.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
byte[] t = Bytes.of(Target.class);
|
||||
System.out.println("running on Java " + Runtime.version().feature() + "; ClassFile.latestMajorVersion() = " + ClassFile.latestMajorVersion()
|
||||
+ "; ASM Opcodes.V25=" + Opcodes.V25 + ", V28=" + Opcodes.V28);
|
||||
for (int major : new int[]{69, 70, 72, 73, 99}) {
|
||||
byte[] b = withMajor(t, major);
|
||||
System.out.println("major " + major + ":");
|
||||
System.out.println(" java.lang.classfile : " + tryClassFile(b));
|
||||
System.out.println(" ASM 9.11.1 : " + tryAsm(b));
|
||||
}
|
||||
Check.that(tryClassFile(withMajor(t, 69)).startsWith("parsed"), "major 69 parses with java.lang.classfile");
|
||||
Check.that(tryClassFile(withMajor(t, ClassFile.latestMajorVersion() + 1)).startsWith("java.lang.IllegalArgumentException"), "one version above the JDK's own is rejected by java.lang.classfile");
|
||||
Check.that(tryAsm(withMajor(t, Opcodes.V28)).startsWith("parsed"), "ASM 9.11.1 still reads major 72 (Java 28)");
|
||||
Check.that(tryAsm(withMajor(t, Opcodes.V28 + 1)).startsWith("java.lang.IllegalArgumentException"), "ASM 9.11.1 rejects major 73");
|
||||
Check.finish();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user