Runtime code generation for the JVM interface Framework

  • Slides: 25
Download presentation
Runtime code generation for the JVM

Runtime code generation for the JVM

interface Framework { <T> Class<? extends T> secure(Class<T> type); } discovers at runtime @interface

interface Framework { <T> Class<? extends T> secure(Class<T> type); } discovers at runtime @interface Secured { String user(); } class Security. Holder { static String user = "ANONYMOUS"; } depends on does not know about class Service { @Secured(user = "ADMIN") void delete. Everything() { // delete everything. . . } }

class Secured. Service { extends Service { @Override @Secured(user = "ADMIN") void delete. Everything()

class Secured. Service { extends Service { @Override @Secured(user = "ADMIN") void delete. Everything() { if(!"ADMIN". equals(User. Holder. user)) { throw new Illegal. State. Exception("Wrong user"); } super. delete. Everything(); // delete everything. . . } } redefine class (build time, agent) create subclass (Liskov substitution) class Service { @Secured(user = "ADMIN") void delete. Everything() { // delete everything. . . } }

source code javac scalac groovyc jrubyc creates reads byte code 0 x. CAFEBABE class

source code javac scalac groovyc jrubyc creates reads byte code 0 x. CAFEBABE class loader interpreter runs JIT compiler JVM

Isn’t reflection meant for this? class Class { Method get. Declared. Method(String name, Class<?

Isn’t reflection meant for this? class Class { Method get. Declared. Method(String name, Class<? >. . . parameter. Types) throws No. Such. Method. Exception, Security. Exception; } class Method { Object invoke(Object obj, Object. . . args) throws Illegal. Access. Exception, Illegal. Argument. Exception, Invocation. Target. Exception; } Reflection implies neither type-safety nor a notion of fail-fast. Note: there are no performance gains when using code generation over reflection! Thus, runtime code generation only makes sense for user type enhancement: While the framework code is less type safe, this type-unsafety does not spoil the user‘s code.

Do-it-yourself as an alternative? class Service { void delete. Everything() { if(!"ADMIN". equals(User. Holder.

Do-it-yourself as an alternative? class Service { void delete. Everything() { if(!"ADMIN". equals(User. Holder. user)) { throw new Illegal. State. Exception("Wrong user"); } // delete everything. . . } } At best, this makes testing an issue. Maybe still the easiest approach for simple cross-cutting concerns. In general, declarative programming often results in readable and modular code.

The “black magic” prejudice. var service = { /* @Secured(user = "ADMIN") */ delete.

The “black magic” prejudice. var service = { /* @Secured(user = "ADMIN") */ delete. Everything: function () { // delete everything. . . } No type, no problem. } (“duck typing”) function run(service) { service. delete. Everything(); } In dynamic languages (also those running on the JVM) this concept is applied a lot! For framework implementors, type-safety is conceptually impossible. But with type information available, we are at least able to fail fast when generating code at runtime in case that types do not match.

The performance myth. There is no point in “byte code optimization”. int compute() {

The performance myth. There is no point in “byte code optimization”. int compute() { return i * Constant. Holder. value; } It’s not true that “reflection is slower than generated code”. Native. Method. Accessor Method: : invoke Generated. Method. Accessor### -Dsun. reflect. inflation. Threshold=# The JIT compiler knows its job pretty well. NEVER “optimize” byte code. Never use JNI for something you could also express as byte code. However, avoid reflective member lookup.

Java source code int foo() { return 1 + 2; } Java byte code

Java source code int foo() { return 1 + 2; } Java byte code ICONST_1 ICONST_2 IADD IRETURN 0 x 04 0 x 05 0 x 60 0 x. AC 2 operand stack 3 1

cglib Byte Buddy visitor API Javassist Method. Visitor method. Visitor =. . . method.

cglib Byte Buddy visitor API Javassist Method. Visitor method. Visitor =. . . method. Visitor. visit. Insn(Opcodes. ICONST_1); method. Visitor. visit. Insn(Opcodes. ICONST_2); method. Visitor. visit. Insn(Opcodes. IADD); method. Visitor. visit. Insn(Opcodes. IRETURN); tree API ASM / BCEL Method. Node method. Node =. . . Insn. List insn. List = method. Node. instructions; insn. List. add(new Insn. Node(Opcodes. ICONST_1)); insn. List. add(new Insn. Node(Opcodes. ICONST_2)); insn. List. add(new Insn. Node(Opcodes. IADD)); insn. List. add(new Insn. Node(Opcodes. IRETURN));

ASM / BCEL Javassist cglib Byte Buddy • Byte code-level API gives full freedom

ASM / BCEL Javassist cglib Byte Buddy • Byte code-level API gives full freedom • Requires knowledge of byte code (stack metaphor, JVM type system) • Requires a lot of manual work (stack sizes / stack map frames) • Byte code-level APIs are not type safe (jeopardy of verifier errors, visitor call order) • Byte code itself is little expressive • Low overhead (visitor APIs) • ASM is currently more popular than BCEL (used by the Open. JDK, considered as public API) • Versioning issues for ASM (especially v 3 to v 4)

ASM / BCEL Javassist cglib Byte Buddy "int foo() {" { + " return

ASM / BCEL Javassist cglib Byte Buddy "int foo() {" { + " return 1 + 2; " 2; + "}" } • Strings are not typed (“SQL quandary”) • Specifically: Security problems! • Makes debugging difficult (unlinked source code, exception stack traces) • Bound to Java as a language • The Javassist compiler lags behind javac • Requires special Java source code instructions for realizing cross-cutting concerns

ASM / BCEL Javassist cglib Byte Buddy generic delegation class Secured. Service extends Service

ASM / BCEL Javassist cglib Byte Buddy generic delegation class Secured. Service extends Service { @Override void delete. Everything() { method. Interceptor. intercept(this, if(!"ADMIN". equals(User. Holder. user)) { Service. class. get. Declared. Method("delete. Everything"), throw new Illegal. State. Exception("Wrong user"); } new Object[0], new $Method. Proxy()); super. delete. Everything(); } } class $Method. Proxy implements Method. Proxy { // inner class semantics, can call super } } interface Method. Interceptor { Object intercept(Object object, Method method, Object[] arguments, Method. Proxy proxy) throws Throwable }

ASM / BCEL Javassist cglib Byte Buddy • Discards all available type information •

ASM / BCEL Javassist cglib Byte Buddy • Discards all available type information • JIT compiler struggles with two-way-boxing (check out JIT-watch for evidence) • Interface dependency of intercepted classes • Delegation requires explicit class initialization (breaks build-time usage / class serialization) • Subclass instrumentation only (breaks annotation APIs / class identity) • “Feature complete” / little development • Little intuitive user-API

ASM / BCEL Javassist cglib Byte Buddy Class<? > dynamic. Type = new Byte.

ASM / BCEL Javassist cglib Byte Buddy Class<? > dynamic. Type = new Byte. Buddy(). subclass(Object. class). method(named("to. String")). intercept(value("Hello World!")). make(). load(get. Class(). get. Class. Loader(), Class. Loading. Strategy. Default. WRAPPER). get. Loaded(); assert. That(dynamic. Type. new. Instance(). to. String(), is("Hello World!"));

ASM / BCEL Javassist cglib Byte Buddy identifies best match Class<? > dynamic. Type

ASM / BCEL Javassist cglib Byte Buddy identifies best match Class<? > dynamic. Type = new Byte. Buddy(). subclass(Object. class). method(named("to. String")). intercept(to(My. Interceptor. class)). make(). load(get. Class(). get. Class. Loader(), Class. Loading. Strategy. Default. WRAPPER). get. Loaded(); class My. Interceptor { static String intercept() { return "Hello World"; } }

ASM / BCEL Javassist cglib Byte Buddy identifies only choice Class<? > dynamic. Type

ASM / BCEL Javassist cglib Byte Buddy identifies only choice Class<? > dynamic. Type = new Byte. Buddy(). subclass(Object. class). method(named("to. String")). intercept(to(My. Interceptor. class). filter(named("intercept"))). make(). load(get. Class(). get. Class. Loader(), Class. Loading. Strategy. Default. WRAPPER). get. Loaded(); class My. Interceptor { static String intercept() { return "Hello World"; } } Branching during instrumentation beats branching during invocation. Even though, this is sometimes optimized by the JIT compiler.

ASM / BCEL Javassist cglib Byte Buddy provides arguments Class<? > dynamic. Type =

ASM / BCEL Javassist cglib Byte Buddy provides arguments Class<? > dynamic. Type = new Byte. Buddy(). subclass(Object. class). method(named("to. String")). intercept(to(My. Interceptor. class)). make(). load(get. Class(). get. Class. Loader(), Class. Loading. Strategy. Default. WRAPPER). get. Loaded(); class My. Interceptor { static String intercept(@Origin Method m) { return "Hello World from " + m. get. Name(); } } Annotations that are not on the class path are ignored at runtime. Thus, Byte Buddy’s classes can be used without Byte Buddy on the class path.

ASM / BCEL Javassist cglib Byte Buddy @Origin Method|Class<? >|String Provides caller information @Super.

ASM / BCEL Javassist cglib Byte Buddy @Origin Method|Class<? >|String Provides caller information @Super. Call Runnable|Callable<? > Allows super method call @Default. Call Runnable|Callable<? > Allows default method call @All. Arguments T[] Provides boxed method arguments @Argument(index) T Provides argument at the given index @This T Provides caller instance @Super T Provides super method proxy

ASM / BCEL Javassist cglib class Foo { String bar() { return "bar"; }

ASM / BCEL Javassist cglib class Foo { String bar() { return "bar"; } } Byte Buddy ve rsi on 0. 3 Foo foo = new Foo(); new Byte. Buddy(). redefine(Foo. class). method(named("bar")). intercept(value("Hello World!")). make(). load(Foo. class. get. Class. Loader(), Class. Reloading. Strategy. installed. Agent()); assert. That(foo. bar(), is("Hello World!")); The instrumentation API does not allow introduction of new methods. This might change with JEP-159: Enhanced Class Redefiniton.

Byte Buddy cglib Javassist Java proxy (1) 60. 995 234. 488 145. 412 68.

Byte Buddy cglib Javassist Java proxy (1) 60. 995 234. 488 145. 412 68. 706 (2 a) 153. 800 804. 000 706. 878 973. 650 (2 b) 0. 001 0. 002 0. 009 0. 005 (3 a) 172. 126 2290. 246 1’ 480. 525 625. 778 n/a (3 b) 0. 002 0. 003 0. 019 0. 027 n/a All benchmarks run with JMH, source code: https: //github. com/raphw/byte-buddy (1) Extending the Object class without any methods but with a default constructor (2 a) Implementing an interface with 18 methods, method stubs (2 b) Executing a method of this interface (3 a) Extending a class with 18 methods, super method invocation (3 b) Executing a method of this class

But it does not work equally well on Android. Java virtual machine [stack, JIT]

But it does not work equally well on Android. Java virtual machine [stack, JIT] Dalvik virtual machine [register, JIT] Android runtime [register, AOT]

http: //rafael. codes @rafaelcodes http: //www. kantega. no http: //blogg. kantega. no http: //bytebuddy.

http: //rafael. codes @rafaelcodes http: //www. kantega. no http: //blogg. kantega. no http: //bytebuddy. net https: //github. com/raphw/byte-buddy