/* * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.mdsal.binding.dom.codec.impl; import static com.google.common.base.Verify.verify; import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; import static org.opendaylight.mdsal.binding.dom.codec.impl.ByteBuddyUtils.THIS; import static org.opendaylight.mdsal.binding.dom.codec.impl.ByteBuddyUtils.getField; import static org.opendaylight.mdsal.binding.dom.codec.impl.ByteBuddyUtils.invokeMethod; import static org.opendaylight.mdsal.binding.dom.codec.impl.ByteBuddyUtils.putField; import com.google.common.base.MoreObjects.ToStringHelper; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.VarHandle; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.description.type.TypeDescription.ForLoadedType; import net.bytebuddy.description.type.TypeDescription.Generic; import net.bytebuddy.dynamic.DynamicType.Builder; import net.bytebuddy.dynamic.scaffold.InstrumentedType; import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.Implementation.Context; import net.bytebuddy.implementation.bytecode.Addition; import net.bytebuddy.implementation.bytecode.ByteCodeAppender; import net.bytebuddy.implementation.bytecode.Multiplication; import net.bytebuddy.implementation.bytecode.StackManipulation; import net.bytebuddy.implementation.bytecode.assign.TypeCasting; import net.bytebuddy.implementation.bytecode.constant.ClassConstant; import net.bytebuddy.implementation.bytecode.constant.IntegerConstant; import net.bytebuddy.implementation.bytecode.constant.TextConstant; import net.bytebuddy.implementation.bytecode.member.MethodReturn; import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess; import net.bytebuddy.jar.asm.Label; import net.bytebuddy.jar.asm.MethodVisitor; import net.bytebuddy.jar.asm.Opcodes; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.binding.dom.codec.impl.ClassGeneratorBridge.LocalNameProvider; import org.opendaylight.mdsal.binding.dom.codec.impl.ClassGeneratorBridge.NodeContextSupplierProvider; import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader; import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader.ClassGenerator; import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader.GeneratorResult; import org.opendaylight.yangtools.yang.binding.DataObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Private support for generating {@link CodecDataObject} and {@link AugmentableCodecDataObject} specializations. * *

* Code generation here is probably more involved than usual mainly due to the fact we *really* want to express the * strong connection between a generated class to the extent possible. In most cases (grouping-generated types) this * involves one level of indirection, which is a safe approach. If we are dealing with a type generated outside of a * grouping statement, though, we are guaranteed instantiation-invariance and hence can hard-wire to a runtime-constant * {@link NodeContextSupplier} -- which provides significant boost to JITs ability to optimize code -- especially with * inlining and constant propagation. * *

* The accessor mapping performance is critical due to users typically not taking care of storing the results acquired * by an invocation, assuming the accessors are backed by a normal field -- which of course is not true, as the results * are lazily computed. * *

* The design is such that for a particular structure like: *

 *     container foo {
 *         leaf bar {
 *             type string;
 *         }
 *     }
 * 
* we end up generating a class with the following layout: *
 *     public final class Foo$$$codecImpl extends CodecDataObject implements Foo {
 *         private static final VarHandle getBar$$$V;
 *         private volatile Object getBar;
 *
 *         public Foo$$$codecImpl(NormalizedNodeContainer data) {
 *             super(data);
 *         }
 *
 *         public Bar getBar() {
 *             return (Bar) codecMember(getBar$$$V, "bar");
 *         }
 *     }
 * 
* *

* This strategy minimizes the bytecode footprint and follows the generally good idea of keeping common logic in a * single place in a maintainable form. The glue code is extremely light (~6 instructions), which is beneficial on both * sides of invocation: *

* *

* The second point is important here, as it allows the invocation logic around VarHandle to completely disappear, * becoming synonymous with operations on a field. Even though the field itself is declared as volatile, it is only ever * accessed through helper method using VarHandles -- and those helpers are using relaxed field ordering * of {@code getAcquire()}/{@code setRelease()} memory semantics. * *

* Furthermore there are distinct {@code codecMember} methods, each of which supports a different invocation style: *

* The third mode of operation requires that the object being implemented is not defined in a {@code grouping}, because * it welds the object to a particular namespace -- hence it trades namespace mobility for access speed. * *

* The sticky point here is the NodeContextSupplier, as it is a heap object which cannot normally be looked up from the * static context in which the static class initializer operates -- so we need perform some sort of a trick here. * Even though ByteBuddy provides facilities for bridging references to type fields, those facilities operate on * volatile fields -- hence they do not quite work for us. * *

* Another alternative, which we used in Javassist-generated DataObjectSerializers, is to muck with the static field * using reflection -- which works, but requires redefinition of Field.modifiers, which is something Java 9+ complains * about quite noisily. * *

* We take a different approach here, which takes advantage of the fact we are in control of both code generation (here) * and class loading (in {@link CodecClassLoader}). The process is performed in four steps: *

* *

* This strategy works due to close cooperation with the target ClassLoader, as the entire code generation and loading * block runs with the class loading lock for this FQCN and the reference is not leaked until the process completes. */ abstract class CodecDataObjectGenerator> implements ClassGenerator { // Not reusable definition: we can inline NodeContextSuppliers without a problem // FIXME: 6.0.0: wire this implementation, which requires that BindingRuntimeTypes provides information about types // being generated from within a grouping private static final class Fixed> extends CodecDataObjectGenerator implements NodeContextSupplierProvider { private final ImmutableMap properties; Fixed(final TypeDescription superClass, final ImmutableMap properties, final @Nullable Method keyMethod) { super(superClass, keyMethod); this.properties = requireNonNull(properties); } @Override Builder generateGetters(final Builder builder) { Builder tmp = builder; for (Method method : properties.keySet()) { LOG.trace("Generating for fixed method {}", method); final String methodName = method.getName(); final TypeDescription retType = TypeDescription.ForLoadedType.of(method.getReturnType()); tmp = tmp.defineMethod(methodName, retType, PUB_FINAL).intercept( new SupplierGetterMethodImplementation(methodName, retType)); } return tmp; } @Override ArrayList getterMethods() { return new ArrayList<>(properties.keySet()); } @Override public NodeContextSupplier resolveNodeContextSupplier(final String methodName) { final Optional> found = properties.entrySet().stream() .filter(entry -> methodName.equals(entry.getKey().getName())).findAny(); verify(found.isPresent(), "Failed to find property for %s in %s", methodName, this); return verifyNotNull(found.get().getValue()); } } // Reusable definition: we have to rely on context lookups private static final class Reusable> extends CodecDataObjectGenerator implements LocalNameProvider { private final ImmutableMap simpleProperties; private final Map> daoProperties; Reusable(final TypeDescription superClass, final ImmutableMap simpleProperties, final Map> daoProperties, final @Nullable Method keyMethod) { super(superClass, keyMethod); this.simpleProperties = requireNonNull(simpleProperties); this.daoProperties = requireNonNull(daoProperties); } @Override Builder generateGetters(final Builder builder) { Builder tmp = builder; for (Method method : simpleProperties.keySet()) { LOG.trace("Generating for simple method {}", method); final String methodName = method.getName(); final TypeDescription retType = TypeDescription.ForLoadedType.of(method.getReturnType()); tmp = tmp.defineMethod(methodName, retType, PUB_FINAL).intercept( new SimpleGetterMethodImplementation(methodName, retType)); } for (Entry> entry : daoProperties.entrySet()) { final Method method = entry.getKey(); LOG.trace("Generating for structured method {}", method); final String methodName = method.getName(); final TypeDescription retType = TypeDescription.ForLoadedType.of(method.getReturnType()); tmp = tmp.defineMethod(methodName, retType, PUB_FINAL).intercept( new StructuredGetterMethodImplementation(methodName, retType, entry.getValue())); } return tmp; } @Override ArrayList getterMethods() { final ArrayList ret = new ArrayList<>(simpleProperties.size() + daoProperties.size()); ret.addAll(simpleProperties.keySet()); ret.addAll(daoProperties.keySet()); return ret; } @Override public String resolveLocalName(final String methodName) { final Optional> found = simpleProperties.entrySet().stream() .filter(entry -> methodName.equals(entry.getKey().getName())).findAny(); verify(found.isPresent(), "Failed to find property for %s in %s", methodName, this); return found.get().getValue().getSchema().getQName().getLocalName(); } } private static final Logger LOG = LoggerFactory.getLogger(CodecDataObjectGenerator.class); private static final Generic BB_BOOLEAN = TypeDefinition.Sort.describe(boolean.class); private static final Generic BB_DATAOBJECT = TypeDefinition.Sort.describe(DataObject.class); private static final Generic BB_HELPER = TypeDefinition.Sort.describe(ToStringHelper.class); private static final Generic BB_INT = TypeDefinition.Sort.describe(int.class); private static final TypeDescription BB_CDO = ForLoadedType.of(CodecDataObject.class); private static final TypeDescription BB_ACDO = ForLoadedType.of(AugmentableCodecDataObject.class); private static final Comparator METHOD_BY_ALPHABET = Comparator.comparing(Method::getName); private static final StackManipulation ARRAYS_EQUALS = invokeMethod(Arrays.class, "equals", byte[].class, byte[].class); private static final StackManipulation OBJECTS_EQUALS = invokeMethod(Objects.class, "equals", Object.class, Object.class); private static final StackManipulation HELPER_ADD = invokeMethod(ToStringHelper.class, "add", String.class, Object.class); private static final StackManipulation FIRST_ARG_REF = MethodVariableAccess.REFERENCE.loadFrom(1); private static final int PROT_FINAL = Opcodes.ACC_PROTECTED | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC; private static final int PUB_FINAL = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC; private static final ByteBuddy BB = new ByteBuddy(); private final TypeDescription superClass; private final Method keyMethod; CodecDataObjectGenerator(final TypeDescription superClass, final @Nullable Method keyMethod) { this.superClass = requireNonNull(superClass); this.keyMethod = keyMethod; } static > Class generate(final CodecClassLoader loader, final Class bindingInterface, final ImmutableMap simpleProperties, final Map> daoProperties, final Method keyMethod) { return loader.generateClass(bindingInterface, "codecImpl", new Reusable<>(BB_CDO, simpleProperties, daoProperties, keyMethod)); } static > Class generateAugmentable( final CodecClassLoader loader, final Class bindingInterface, final ImmutableMap simpleProperties, final Map> daoProperties, final Method keyMethod) { return loader.generateClass(bindingInterface, "codecImpl", new Reusable<>(BB_ACDO, simpleProperties, daoProperties, keyMethod)); } @Override public final GeneratorResult generateClass(final CodecClassLoader loeader, final String fqcn, final Class bindingInterface) { LOG.trace("Generating class {}", fqcn); final Generic bindingDef = TypeDefinition.Sort.describe(bindingInterface); @SuppressWarnings("unchecked") Builder builder = (Builder) BB.subclass(Generic.Builder.parameterizedType(superClass, bindingDef).build()) .visit(ByteBuddyUtils.computeFrames()).name(fqcn).implement(bindingDef); builder = generateGetters(builder); if (keyMethod != null) { LOG.trace("Generating for key {}", keyMethod); final String methodName = keyMethod.getName(); final TypeDescription retType = TypeDescription.ForLoadedType.of(keyMethod.getReturnType()); builder = builder.defineMethod(methodName, retType, PUB_FINAL).intercept( new KeyMethodImplementation(methodName, retType)); } // Index all property methods, turning them into "getFoo()" invocations, retaining order. We will be using // those invocations in each of the three methods. Note that we do not glue the invocations to 'this', as we // will be invoking them on 'other' in codecEquals() final ArrayList properties = getterMethods(); // Make sure properties are alpha-sorted properties.sort(METHOD_BY_ALPHABET); final ImmutableMap methods = Maps.uniqueIndex(properties, ByteBuddyUtils::invokeMethod); // Final bits: return GeneratorResult.of(builder // codecHashCode() ... .defineMethod("codecHashCode", BB_INT, PROT_FINAL) .intercept(new Implementation.Simple(new CodecHashCode(methods))) // ... codecEquals() ... .defineMethod("codecEquals", BB_BOOLEAN, PROT_FINAL).withParameter(BB_DATAOBJECT) .intercept(codecEquals(methods)) // ... and codecFillToString() ... .defineMethod("codecFillToString", BB_HELPER, PROT_FINAL).withParameter(BB_HELPER) .intercept(codecFillToString(methods)) // ... and build it .make()); } abstract Builder generateGetters(Builder builder); abstract ArrayList getterMethods(); private static Implementation codecEquals(final ImmutableMap properties) { // Label for 'return false;' final Label falseLabel = new Label(); // Condition for 'if (!...)' final StackManipulation ifFalse = ByteBuddyUtils.ifEq(falseLabel); final List manipulations = new ArrayList<>(properties.size() * 6 + 5); for (Entry entry : properties.entrySet()) { // if (!java.util.(Objects|Arrays).equals(getFoo(), other.getFoo())) { // return false; // } manipulations.add(THIS); manipulations.add(entry.getKey()); manipulations.add(FIRST_ARG_REF); manipulations.add(entry.getKey()); manipulations.add(entry.getValue().getReturnType().isArray() ? ARRAYS_EQUALS : OBJECTS_EQUALS); manipulations.add(ifFalse); } // return true; manipulations.add(IntegerConstant.ONE); manipulations.add(MethodReturn.INTEGER); // L0: return false; manipulations.add(ByteBuddyUtils.markLabel(falseLabel)); manipulations.add(IntegerConstant.ZERO); manipulations.add(MethodReturn.INTEGER); return new Implementation.Simple(manipulations.toArray(new StackManipulation[0])); } private static Implementation codecFillToString(final ImmutableMap properties) { final List manipulations = new ArrayList<>(properties.size() * 4 + 2); // push 'return helper' to stack... manipulations.add(FIRST_ARG_REF); for (Entry entry : properties.entrySet()) { // .add("getFoo", getFoo()) manipulations.add(new TextConstant(entry.getValue().getName())); manipulations.add(THIS); manipulations.add(entry.getKey()); manipulations.add(HELPER_ADD); } // ... execute 'return helper' manipulations.add(MethodReturn.REFERENCE); return new Implementation.Simple(manipulations.toArray(new StackManipulation[0])); } private abstract static class AbstractMethodImplementation implements Implementation { private static final Generic BB_HANDLE = TypeDefinition.Sort.describe(VarHandle.class); private static final Generic BB_OBJECT = TypeDefinition.Sort.describe(Object.class); private static final StackManipulation OBJECT_CLASS = ClassConstant.of(TypeDescription.OBJECT); private static final StackManipulation LOOKUP = invokeMethod(MethodHandles.class, "lookup"); private static final StackManipulation FIND_VAR_HANDLE = invokeMethod(Lookup.class, "findVarHandle", Class.class, String.class, Class.class); static final int PRIV_CONST = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC; private static final int PRIV_VOLATILE = Opcodes.ACC_PRIVATE | Opcodes.ACC_VOLATILE | Opcodes.ACC_SYNTHETIC; final TypeDescription retType; // getFoo final String methodName; // getFoo$$$V final String handleName; AbstractMethodImplementation(final String methodName, final TypeDescription retType) { this.methodName = requireNonNull(methodName); this.retType = requireNonNull(retType); this.handleName = methodName + "$$$V"; } @Override public InstrumentedType prepare(final InstrumentedType instrumentedType) { final InstrumentedType tmp = instrumentedType // private static final VarHandle getFoo$$$V; .withField(new FieldDescription.Token(handleName, PRIV_CONST, BB_HANDLE)) // private volatile Object getFoo; .withField(new FieldDescription.Token(methodName, PRIV_VOLATILE, BB_OBJECT)); return tmp.withInitializer(new ByteCodeAppender.Simple( // TODO: acquiring lookup is expensive, we should share it across all initialization // getFoo$$$V = MethodHandles.lookup().findVarHandle(This.class, "getFoo", Object.class); LOOKUP, ClassConstant.of(tmp), new TextConstant(methodName), OBJECT_CLASS, FIND_VAR_HANDLE, putField(tmp, handleName))); } } private static final class KeyMethodImplementation extends AbstractMethodImplementation { private static final StackManipulation CODEC_KEY = invokeMethod(CodecDataObject.class, "codecKey", VarHandle.class); KeyMethodImplementation(final String methodName, final TypeDescription retType) { super(methodName, retType); } @Override public ByteCodeAppender appender(final Target implementationTarget) { return new ByteCodeAppender.Simple( // return (FooType) codecKey(getFoo$$$V); THIS, getField(implementationTarget.getInstrumentedType(), handleName), CODEC_KEY, TypeCasting.to(retType), MethodReturn.REFERENCE); } } /* * A simple leaf method, which looks up child by a String constant. This is slightly more complicated because we * want to make sure we are using the same String instance as the one stored in associated DataObjectCodecContext, * so that during lookup we perform an identity check instead of comparing content -- speeding things up as well * as minimizing footprint. Since that string is not guaranteed to be interned in the String Pool, we cannot rely * on the constant pool entry to resolve to the same object. */ private static final class SimpleGetterMethodImplementation extends AbstractMethodImplementation { private static final StackManipulation CODEC_MEMBER = invokeMethod(CodecDataObject.class, "codecMember", VarHandle.class, String.class); private static final StackManipulation BRIDGE_RESOLVE = invokeMethod(ClassGeneratorBridge.class, "resolveLocalName", String.class); private static final Generic BB_STRING = TypeDefinition.Sort.describe(String.class); // getFoo$$$S private final String stringName; SimpleGetterMethodImplementation(final String methodName, final TypeDescription retType) { super(methodName, retType); this.stringName = methodName + "$$$S"; } @Override public InstrumentedType prepare(final InstrumentedType instrumentedType) { final InstrumentedType tmp = super.prepare(instrumentedType) // private static final String getFoo$$$S; .withField(new FieldDescription.Token(stringName, PRIV_CONST, BB_STRING)); return tmp.withInitializer(new ByteCodeAppender.Simple( // getFoo$$$S = CodecDataObjectBridge.resolveString("getFoo"); new TextConstant(methodName), BRIDGE_RESOLVE, putField(tmp, stringName))); } @Override public ByteCodeAppender appender(final Target implementationTarget) { final TypeDescription instrumentedType = implementationTarget.getInstrumentedType(); return new ByteCodeAppender.Simple( // return (FooType) codecMember(getFoo$$$V, getFoo$$$S); THIS, getField(instrumentedType, handleName), getField(instrumentedType, stringName), CODEC_MEMBER, TypeCasting.to(retType), MethodReturn.REFERENCE); } } private static final class StructuredGetterMethodImplementation extends AbstractMethodImplementation { private static final StackManipulation CODEC_MEMBER = invokeMethod(CodecDataObject.class, "codecMember", VarHandle.class, Class.class); private final Class bindingClass; StructuredGetterMethodImplementation(final String methodName, final TypeDescription retType, final Class bindingClass) { super(methodName, retType); this.bindingClass = requireNonNull(bindingClass); } @Override public ByteCodeAppender appender(final Target implementationTarget) { return new ByteCodeAppender.Simple( // return (FooType) codecMember(getFoo$$$V, FooType.class); THIS, getField(implementationTarget.getInstrumentedType(), handleName), ClassConstant.of(TypeDefinition.Sort.describe(bindingClass).asErasure()), CODEC_MEMBER, TypeCasting.to(retType), MethodReturn.REFERENCE); } } private static final class SupplierGetterMethodImplementation extends AbstractMethodImplementation { private static final StackManipulation CODEC_MEMBER = invokeMethod(CodecDataObject.class, "codecMember", VarHandle.class, NodeContextSupplier.class); private static final StackManipulation BRIDGE_RESOLVE = invokeMethod(ClassGeneratorBridge.class, "resolveNodeContextSupplier", String.class); private static final Generic BB_NCS = TypeDefinition.Sort.describe(NodeContextSupplier.class); // getFoo$$$C private final String contextName; SupplierGetterMethodImplementation(final String methodName, final TypeDescription retType) { super(methodName, retType); contextName = methodName + "$$$C"; } @Override public InstrumentedType prepare(final InstrumentedType instrumentedType) { final InstrumentedType tmp = super.prepare(instrumentedType) // private static final NodeContextSupplier getFoo$$$C; .withField(new FieldDescription.Token(contextName, PRIV_CONST, BB_NCS)); return tmp.withInitializer(new ByteCodeAppender.Simple( // getFoo$$$C = CodecDataObjectBridge.resolve("getFoo"); new TextConstant(methodName), BRIDGE_RESOLVE, putField(tmp, contextName))); } @Override public ByteCodeAppender appender(final Target implementationTarget) { final TypeDescription instrumentedType = implementationTarget.getInstrumentedType(); return new ByteCodeAppender.Simple( // return (FooType) codecMember(getFoo$$$V, getFoo$$$C); THIS, getField(instrumentedType, handleName), getField(instrumentedType, contextName), CODEC_MEMBER, TypeCasting.to(retType), MethodReturn.REFERENCE); } } private static final class CodecHashCode implements ByteCodeAppender { private static final StackManipulation THIRTY_ONE = IntegerConstant.forValue(31); private static final StackManipulation LOAD_RESULT = MethodVariableAccess.INTEGER.loadFrom(1); private static final StackManipulation STORE_RESULT = MethodVariableAccess.INTEGER.storeAt(1); private static final StackManipulation ARRAYS_HASHCODE = invokeMethod(Arrays.class, "hashCode", byte[].class); private static final StackManipulation OBJECTS_HASHCODE = invokeMethod(Objects.class, "hashCode", Object.class); private final ImmutableMap properties; CodecHashCode(final ImmutableMap properties) { this.properties = requireNonNull(properties); } @Override public Size apply(final MethodVisitor methodVisitor, final Context implementationContext, final MethodDescription instrumentedMethod) { final List manipulations = new ArrayList<>(properties.size() * 8 + 4); // int result = 1; manipulations.add(IntegerConstant.ONE); manipulations.add(STORE_RESULT); for (Entry entry : properties.entrySet()) { // result = 31 * result + java.util.(Objects,Arrays).hashCode(getFoo()); manipulations.add(THIRTY_ONE); manipulations.add(LOAD_RESULT); manipulations.add(Multiplication.INTEGER); manipulations.add(THIS); manipulations.add(entry.getKey()); manipulations.add(entry.getValue().getReturnType().isArray() ? ARRAYS_HASHCODE : OBJECTS_HASHCODE); manipulations.add(Addition.INTEGER); manipulations.add(STORE_RESULT); } // return result; manipulations.add(LOAD_RESULT); manipulations.add(MethodReturn.INTEGER); StackManipulation.Size operandStackSize = new StackManipulation.Compound(manipulations) .apply(methodVisitor, implementationContext); return new Size(operandStackSize.getMaximalSize(), instrumentedMethod.getStackSize() + 1); } } }