From f6f3556fd543e3af5a239f5e8666c6305a6b8f5a Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Tue, 23 Apr 2019 22:28:22 +0200 Subject: [PATCH 1/1] Switch NormalizedNode->Binding codegen to ByteBuddy This patch switches code generation for streamers and codec-based binding objects to use ByteBuddy instead of Javassist. While this increases the size of our jar considerably, it is leads to more maintainable and safer code. JIRA: MDSAL-444 Change-Id: Ib810dd0df6e569b0bb20603a5b0f0180c28ec25c Signed-off-by: Robert Varga --- binding/mdsal-binding-dom-codec/pom.xml | 53 +- .../impl/AugmentableCodecDataObject.java | 2 +- .../dom/codec/impl/BindingCodecContext.java | 15 +- .../dom/codec/impl/ByteBuddyUtils.java | 170 +++++++ .../dom/codec/impl/CodecDataObject.java | 2 +- .../dom/codec/impl/CodecDataObjectBridge.java | 10 +- .../codec/impl/CodecDataObjectCustomizer.java | 226 --------- .../codec/impl/CodecDataObjectGenerator.java | 464 ++++++++++++++++++ .../codec/impl/DataObjectCodecContext.java | 31 +- .../dom/codec/impl/DataObjectStreamer.java | 1 - .../codec/impl/DataObjectStreamerBridge.java | 48 -- .../impl/DataObjectStreamerCustomizer.java | 327 ------------ .../impl/DataObjectStreamerGenerator.java | 453 +++++++++++++++++ .../codec/impl/OpaqueNodeCodecContext.java | 31 +- .../dom/codec/loader/CodecClassLoader.java | 212 ++++---- .../codec/loader/LeafCodecClassLoader.java | 11 +- .../codec/loader/RootCodecClassLoader.java | 20 +- .../dom/codec/loader/StaticClassPool.java | 85 ---- .../dom/codec/loader/package-info.java | 14 +- 19 files changed, 1303 insertions(+), 872 deletions(-) create mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/ByteBuddyUtils.java delete mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectCustomizer.java create mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectGenerator.java delete mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerBridge.java delete mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerCustomizer.java create mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerGenerator.java delete mode 100644 binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/StaticClassPool.java diff --git a/binding/mdsal-binding-dom-codec/pom.xml b/binding/mdsal-binding-dom-codec/pom.xml index 6927a3062b..84c66fcb10 100644 --- a/binding/mdsal-binding-dom-codec/pom.xml +++ b/binding/mdsal-binding-dom-codec/pom.xml @@ -19,11 +19,22 @@ mdsal-binding-dom-codec bundle + + net.bytebuddy + org.opendaylight.mdsal.binding.dom.codec.jar.bytebuddy + + org.javassist javassist + + + net.bytebuddy + byte-buddy + 1.9.12 + org.opendaylight.mdsal mdsal-binding-generator-impl @@ -62,13 +73,51 @@ ${project.groupId}.${project.artifactId} org.opendaylight.mdsal.binding.dom.codec.*, - org.opendaylight.mdsal.binding.dom.codec.gen.impl.*, - org.opendaylight.mdsal.binding.dom.codec.impl.*, ;-split-package:=error + + org.opendaylight.mdsal.binding.dom.codec.loader, + + + !net.bytebuddy.*, + * + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + false + true + true + true + true + + + ${shade.source} + ${shade.target} + + + + + net.bytebuddy:byte-buddy + + + + + + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/AugmentableCodecDataObject.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/AugmentableCodecDataObject.java index 43edeadb86..3cf75fbf70 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/AugmentableCodecDataObject.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/AugmentableCodecDataObject.java @@ -42,7 +42,7 @@ public abstract class AugmentableCodecDataObject> cachedAugmentations; - public AugmentableCodecDataObject(final DataObjectCodecContext context, + protected AugmentableCodecDataObject(final DataObjectCodecContext context, final NormalizedNodeContainer data) { super(data); this.context = requireNonNull(context, "Context must not be null"); diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/BindingCodecContext.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/BindingCodecContext.java index 2c12aaa26c..daa0b9239f 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/BindingCodecContext.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/BindingCodecContext.java @@ -31,8 +31,6 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import javassist.CannotCompileException; -import javassist.NotFoundException; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.binding.dom.codec.api.BindingCodecTree; @@ -40,7 +38,6 @@ import org.opendaylight.mdsal.binding.dom.codec.api.BindingCodecTreeNode; import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataObjectCodecTreeNode; import org.opendaylight.mdsal.binding.dom.codec.impl.NodeCodecContext.CodecContextFactory; import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader; -import org.opendaylight.mdsal.binding.dom.codec.loader.StaticClassPool; import org.opendaylight.mdsal.binding.dom.codec.util.BindingSchemaMapping; import org.opendaylight.mdsal.binding.generator.util.BindingRuntimeContext; import org.opendaylight.mdsal.binding.model.api.GeneratedType; @@ -105,12 +102,10 @@ final class BindingCodecContext implements CodecContextFactory, BindingCodecTree private final LoadingCache, DataObjectStreamer> streamers = CacheBuilder.newBuilder().build( new CacheLoader, DataObjectStreamer>() { @Override - public DataObjectStreamer load(final Class key) throws CannotCompileException, IOException, - NotFoundException, ReflectiveOperationException { - final Class streamer = loader.generateSubclass(DataObjectStreamerCustomizer.CT_DOS, key, "streamer", - DataObjectStreamerCustomizer.create(BindingCodecContext.this, key)); - - final Field instance = streamer.getDeclaredField(DataObjectStreamerCustomizer.INSTANCE_FIELD); + public DataObjectStreamer load(final Class key) throws ReflectiveOperationException { + final Class streamer = DataObjectStreamerGenerator.generateStreamer(loader, BindingCodecContext.this, + key); + final Field instance = streamer.getDeclaredField(DataObjectStreamerGenerator.INSTANCE_FIELD); return (DataObjectStreamer) instance.get(null); } }); @@ -122,7 +117,7 @@ final class BindingCodecContext implements CodecContextFactory, BindingCodecTree } }); - private final @NonNull CodecClassLoader loader = StaticClassPool.createLoader(); + private final @NonNull CodecClassLoader loader = CodecClassLoader.create(); private final InstanceIdentifierCodec instanceIdentifierCodec; private final IdentityCodec identityCodec; private final BindingNormalizedNodeCodecRegistry registry; diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/ByteBuddyUtils.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/ByteBuddyUtils.java new file mode 100644 index 0000000000..c5bdc771b5 --- /dev/null +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/ByteBuddyUtils.java @@ -0,0 +1,170 @@ +/* + * 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 java.util.Objects.requireNonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldDescription.ForLoadedField; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.method.MethodDescription.ForLoadedMethod; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.bytecode.StackManipulation; +import net.bytebuddy.implementation.bytecode.member.FieldAccess; +import net.bytebuddy.implementation.bytecode.member.FieldAccess.Defined; +import net.bytebuddy.implementation.bytecode.member.MethodInvocation; +import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.pool.TypePool; + +final class ByteBuddyUtils { + static final StackManipulation THIS = MethodVariableAccess.loadThis(); + + private ByteBuddyUtils() { + + } + + static StackManipulation invokeMethod(final Method method) { + return MethodInvocation.invoke(describe(method)); + } + + static StackManipulation invokeMethod(final Class clazz, final String name, final Class... args) { + return MethodInvocation.invoke(describe(clazz, name, args)); + } + + static AsmVisitorWrapper computeFrames() { + return ComputeFrames.INSTANCE; + } + + static StackManipulation ifEq(final Label label) { + return new IfEq(label); + } + + static StackManipulation markLabel(final Label label) { + return new Mark(label); + } + + static StackManipulation loadThis() { + return THIS; + } + + static StackManipulation getField(final Field field) { + return FieldAccess.forField(new ForLoadedField(field).asDefined()).read(); + } + + static StackManipulation getField(final TypeDescription instrumentedType, final String fieldName) { + return fieldAccess(instrumentedType, fieldName).read(); + } + + static StackManipulation putField(final TypeDescription instrumentedType, final String fieldName) { + return fieldAccess(instrumentedType, fieldName).write(); + } + + private static ForLoadedMethod describe(final Method method) { + return new ForLoadedMethod(method); + } + + private static ForLoadedMethod describe(final Class clazz, final String name, final Class... args) { + return describe(getMethod(clazz, name, args)); + } + + private static Defined fieldAccess(final TypeDescription instrumentedType, final String fieldName) { + return FieldAccess.forField(instrumentedType.getDeclaredFields().filter(ElementMatchers.named(fieldName)) + .getOnly()); + } + + /** + * Utility wrapper to force ASM to compute frames. + */ + private enum ComputeFrames implements AsmVisitorWrapper { + INSTANCE; + + @Override + public int mergeWriter(final int flags) { + return flags | ClassWriter.COMPUTE_FRAMES; + } + + @Override + public int mergeReader(final int flags) { + return flags | ClassWriter.COMPUTE_FRAMES; + } + + @Override + public ClassVisitor wrap(final TypeDescription td, final ClassVisitor cv, final Implementation.Context ctx, + final TypePool tp, final FieldList fields, final MethodList methods, + final int wflags, final int rflags) { + return cv; + } + } + + /** + * IFEQ opcode invocation, jumping to a particular label. + */ + private static final class IfEq implements StackManipulation { + private static final StackManipulation.Size SIZE = new StackManipulation.Size(-1, 0); + + private final Label label; + + IfEq(final Label label) { + this.label = requireNonNull(label); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public StackManipulation.Size apply(final MethodVisitor mv, final Implementation.Context ctx) { + mv.visitJumpInsn(Opcodes.IFEQ, label); + return SIZE; + } + } + + /** + * A label definition, marking the spot where IfEq should jump. + */ + private static final class Mark implements StackManipulation { + private static final StackManipulation.Size SIZE = new StackManipulation.Size(0, 0); + + private final Label label; + + Mark(final Label label) { + this.label = requireNonNull(label); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public StackManipulation.Size apply(final MethodVisitor mv, final Implementation.Context ctx) { + mv.visitLabel(label); + return SIZE; + } + } + + private static Method getMethod(final Class clazz, final String name, final Class... args) { + try { + return clazz.getDeclaredMethod(name, args); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObject.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObject.java index b63e2928a7..cec788f3fd 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObject.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObject.java @@ -36,7 +36,7 @@ public abstract class CodecDataObject implements DataObjec private volatile Integer cachedHashcode = null; - public CodecDataObject(final NormalizedNodeContainer data) { + protected CodecDataObject(final NormalizedNodeContainer data) { this.data = requireNonNull(data, "Data must not be null"); } diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectBridge.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectBridge.java index 8324f11dd8..cfc3994423 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectBridge.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectBridge.java @@ -19,7 +19,7 @@ import org.eclipse.jdt.annotation.Nullable; */ @Beta public final class CodecDataObjectBridge { - private static final ThreadLocal CURRENT_CUSTOMIZER = new ThreadLocal<>(); + private static final ThreadLocal CURRENT_CUSTOMIZER = new ThreadLocal<>(); private CodecDataObjectBridge() { @@ -33,13 +33,13 @@ public final class CodecDataObjectBridge { return current().resolveKey(methodName); } - static @Nullable CodecDataObjectCustomizer setup(final @NonNull CodecDataObjectCustomizer next) { - final CodecDataObjectCustomizer prev = CURRENT_CUSTOMIZER.get(); + static @Nullable CodecDataObjectGenerator setup(final @NonNull CodecDataObjectGenerator next) { + final CodecDataObjectGenerator prev = CURRENT_CUSTOMIZER.get(); CURRENT_CUSTOMIZER.set(verifyNotNull(next)); return prev; } - static void tearDown(final @Nullable CodecDataObjectCustomizer prev) { + static void tearDown(final @Nullable CodecDataObjectGenerator prev) { if (prev == null) { CURRENT_CUSTOMIZER.remove(); } else { @@ -47,7 +47,7 @@ public final class CodecDataObjectBridge { } } - private static @NonNull CodecDataObjectCustomizer current() { + private static @NonNull CodecDataObjectGenerator current() { return verifyNotNull(CURRENT_CUSTOMIZER.get(), "No customizer attached"); } } diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectCustomizer.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectCustomizer.java deleted file mode 100644 index f9a3b11540..0000000000 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectCustomizer.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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 com.google.common.base.MoreObjects.ToStringHelper; -import com.google.common.base.Supplier; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import javassist.CannotCompileException; -import javassist.CtClass; -import javassist.CtField; -import javassist.CtMethod; -import javassist.NotFoundException; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader; -import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader.Customizer; -import org.opendaylight.mdsal.binding.dom.codec.loader.StaticClassPool; -import org.opendaylight.yangtools.yang.binding.DataObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Private support for generating AbstractDataObject specializations. - */ -final class CodecDataObjectCustomizer implements Customizer { - private static final Logger LOG = LoggerFactory.getLogger(CodecDataObjectCustomizer.class); - private static final CtClass CT_ARFU = StaticClassPool.findClass(AtomicReferenceFieldUpdater.class); - private static final CtClass CT_BOOLEAN = StaticClassPool.findClass(boolean.class); - private static final CtClass CT_DATAOBJECT = StaticClassPool.findClass(DataObject.class); - private static final CtClass CT_HELPER = StaticClassPool.findClass(ToStringHelper.class); - private static final CtClass CT_IIC = StaticClassPool.findClass(IdentifiableItemCodec.class); - private static final CtClass CT_INT = StaticClassPool.findClass(int.class); - private static final CtClass CT_NCS = StaticClassPool.findClass(NodeContextSupplier.class); - private static final CtClass CT_OBJECT = StaticClassPool.findClass(Object.class); - private static final CtClass[] EMPTY_ARGS = new CtClass[0]; - private static final CtClass[] EQUALS_ARGS = new CtClass[] { CT_DATAOBJECT }; - private static final CtClass[] TOSTRING_ARGS = new CtClass[] { CT_HELPER }; - - private final ImmutableMap properties; - private final Entry keyMethod; - - CodecDataObjectCustomizer(final ImmutableMap properties, - final @Nullable Entry keyMethod) { - this.properties = requireNonNull(properties); - this.keyMethod = keyMethod; - } - - @Override - public List> customize(final CodecClassLoader loader, final CtClass bindingClass, final CtClass generated) - throws NotFoundException, CannotCompileException { - // Generate members for all methods ... - LOG.trace("Generating class {}", generated.getName()); - generated.addInterface(bindingClass); - - for (Method method : properties.keySet()) { - generateMethod(loader, generated, method, CT_NCS, "resolve"); - } - if (keyMethod != null) { - generateMethod(loader, generated, keyMethod.getKey(), CT_IIC, "resolveKey"); - } - - // Final bits: codecHashCode() ... - final CtMethod codecHashCode = new CtMethod(CT_INT, "codecHashCode", EMPTY_ARGS, generated); - codecHashCode.setModifiers(Modifier.PROTECTED | Modifier.FINAL); - codecHashCode.setBody(codecHashCodeBody()); - generated.addMethod(codecHashCode); - - // ... equals - final CtMethod codecEquals = new CtMethod(CT_BOOLEAN, "codecEquals", EQUALS_ARGS, generated); - codecEquals.setModifiers(Modifier.PROTECTED | Modifier.FINAL); - codecEquals.setBody(codecEqualsBody(bindingClass.getName())); - generated.addMethod(codecEquals); - - // ... and codecFillToString() - final CtMethod codecFillToString = new CtMethod(CT_HELPER, "codecFillToString", TOSTRING_ARGS, generated); - codecFillToString.setModifiers(Modifier.PROTECTED | Modifier.FINAL); - codecFillToString.setBody(codecFillToStringBody()); - generated.addMethod(codecFillToString); - - generated.setModifiers(Modifier.FINAL | Modifier.PUBLIC); - return ImmutableList.of(); - } - - @Override - public Class customizeLoading(final @NonNull Supplier> loader) { - final CodecDataObjectCustomizer prev = CodecDataObjectBridge.setup(this); - try { - final Class result = loader.get(); - - /* - * This a bit of magic to support NodeContextSupplier constants. These constants need to be resolved while - * we have the information needed to find them -- that information is being held in this instance and we - * leak it to a thread-local variable held by CodecDataObjectBridge. - * - * By default the JVM will defer class initialization to first use, which unfortunately is too late for - * us, and hence we need to force class to initialize. - */ - try { - Class.forName(result.getName(), true, result.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new LinkageError("Failed to find newly-defined " + result, e); - } - - return result; - } finally { - CodecDataObjectBridge.tearDown(prev); - } - } - - - @NonNull NodeContextSupplier resolve(final @NonNull 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()); - } - - @NonNull IdentifiableItemCodec resolveKey(final @NonNull String methodName) { - return verifyNotNull(verifyNotNull(keyMethod, "No key method attached for %s in %s", methodName, this) - .getValue()); - } - - private String codecHashCodeBody() { - final StringBuilder sb = new StringBuilder() - .append("{\n") - .append("final int prime = 31;\n") - .append("int result = 1;\n"); - - for (Method method : properties.keySet()) { - sb.append("result = prime * result + java.util.").append(utilClass(method)).append(".hashCode(") - .append(method.getName()).append("());\n"); - } - - return sb.append("return result;\n") - .append('}').toString(); - } - - private String codecEqualsBody(final String ifaceName) { - final StringBuilder sb = new StringBuilder() - .append("{\n") - .append("final ").append(ifaceName).append(" other = $1;") - .append("return true"); - - for (Method method : properties.keySet()) { - final String methodName = method.getName(); - sb.append("\n&& java.util.").append(utilClass(method)).append(".equals(").append(methodName) - .append("(), other.").append(methodName).append("())"); - } - - return sb.append(";\n") - .append('}').toString(); - } - - private String codecFillToStringBody() { - final StringBuilder sb = new StringBuilder() - .append("{\n") - .append("return $1"); - for (Method method : properties.keySet()) { - final String methodName = method.getName(); - sb.append("\n.add(\"").append(methodName).append("\", ").append(methodName).append("())"); - } - - return sb.append(";\n") - .append('}').toString(); - } - - private static void generateMethod(final CodecClassLoader loader, final CtClass generated, final Method method, - final CtClass contextType, final String resolveMethod) throws CannotCompileException, NotFoundException { - LOG.trace("Generating for method {}", method); - final String methodName = method.getName(); - final String methodArfu = methodName + "$$$ARFU"; - final String methodNcs = methodName + "$$$NCS"; - - // NodeContextSupplier ... - final CtField ncsField = new CtField(contextType, methodNcs, generated); - ncsField.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL); - generated.addField(ncsField, new StringBuilder().append(CodecDataObjectBridge.class.getName()) - .append('.').append(resolveMethod).append("(\"").append(methodName).append("\")").toString()); - - // ... AtomicReferenceFieldUpdater ... - final CtField arfuField = new CtField(CT_ARFU, methodArfu, generated); - arfuField.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL); - generated.addField(arfuField, new StringBuilder().append(CT_ARFU.getName()).append(".newUpdater(") - .append(generated.getName()).append(".class, java.lang.Object.class, \"").append(methodName) - .append("\")").toString()); - - // ... corresponding volatile field ... - final CtField field = new CtField(CT_OBJECT, methodName, generated); - field.setModifiers(Modifier.PRIVATE | Modifier.VOLATILE); - generated.addField(field); - - // ... and the getter - final Class retType = method.getReturnType(); - final CtMethod getter = new CtMethod(loader.findClass(retType), methodName, EMPTY_ARGS, generated); - final String retName = retType.isArray() ? retType.getSimpleName() : retType.getName(); - - getter.setBody(new StringBuilder() - .append("{\n") - .append("return (").append(retName).append(") codecMember(").append(methodArfu).append(", ") - .append(methodNcs).append(");\n") - .append('}').toString()); - getter.setModifiers(Modifier.PUBLIC | Modifier.FINAL); - generated.addMethod(getter); - } - - private static String utilClass(final Method method) { - // We can either have objects or byte[], we cannot have Object[] - return method.getReturnType().isArray() ? "Arrays" : "Objects"; - } -} \ No newline at end of file diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectGenerator.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectGenerator.java new file mode 100644 index 0000000000..2f42087e08 --- /dev/null +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/CodecDataObjectGenerator.java @@ -0,0 +1,464 @@ +/* + * 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.base.Supplier; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +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.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.NonNull; +import org.eclipse.jdt.annotation.Nullable; +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 and BindingCodecContext in terms of a true constant, which boils down to + * {@code private static final NodeContextSupplier NCS}. Having such constants 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 AtomicRefereceFieldUpdater<Foo$$$codecImpl, Object> getBar$$$A;
+ *         private static final NodeContextSupplier getBar$$$C;
+ *         private volatile Object getBar;
+ *
+ *         public Foo$$$codecImpl(NormalizedNodeContainer data) {
+ *             super(data);
+ *         }
+ *
+ *         public Bar getBar() {
+ *             return (Bar) codecMember(getBar$$$A, getBar$$$C);
+ *         }
+ *     }
+ * 
+ * + *

+ * 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: + * - generated method can readily be inlined into the caller + * - it forms a call site into which codeMember() can be inlined with both AtomicReferenceFieldUpdater and + * NodeContextSupplier being constant + * + *

+ * The second point is important here, as it allows the invocation logic around AtomicRefereceFieldUpdater to completely + * disappear, becoming synonymous with operations of a volatile field. NodeContextSupplier being constant also means + * it will resolve to one of its two implementations, allowing NodeContextSupplier.get() to be resolved to a constant + * (pointing to the supplier itself) or to a simple volatile read (which will be non-null after first access). + * + *

+ * 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. + * + *

+ * Eventhough 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: + *

    + *
  • During code generation, the context fields are pointed towards {@link CodecDataObjectBridge#resolve(String)} and + * {@link CodecDataObjectBridge#resolveKey(String)} methods, which are public and static, hence perfectly usable + * in the context of a class initializer.
  • + *
  • During class loading of generated byte code, the original instance of the generator is called to wrap the actual + * class loading operation. At this point the generator installs itself as the current generator for this thread via + * {@link CodecDataObjectBridge#setup(CodecDataObjectGenerator)} and allows the class to be loaded. + *
  • After the class has been loaded, but before the call returns, we will force the class to initialize, at which + * point the static invocations will be redirect to {@link #resolve(String)} and {@link #resolveKey(String)} + * methods, thus initializing the fields to the intended constants.
  • + *
  • Before returning from the class loading call, the generator will detach itself via + * {@link CodecDataObjectBridge#tearDown(CodecDataObjectGenerator)}.
  • + *
+ * + *

+ * 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. + */ +final class CodecDataObjectGenerator> implements ClassGenerator { + 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 Generic BB_IIC = TypeDefinition.Sort.describe(IdentifiableItemCodec.class); + private static final Generic BB_NCS = TypeDefinition.Sort.describe(NodeContextSupplier.class); + + private static final StackManipulation BRIDGE_RESOLVE = invokeMethod(CodecDataObjectBridge.class, + "resolve", String.class); + private static final StackManipulation BRIDGE_RESOLVE_KEY = invokeMethod(CodecDataObjectBridge.class, + "resolveKey", String.class); + private static final StackManipulation CODEC_MEMBER = invokeMethod(CodecDataObject.class, + "codecMember", AtomicReferenceFieldUpdater.class, NodeContextSupplier.class); + private static final StackManipulation CODEC_MEMBER_KEY = invokeMethod(CodecDataObject.class, + "codecMember", AtomicReferenceFieldUpdater.class, IdentifiableItemCodec.class); + + 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 Builder CDO; + private static final Builder ACDO; + + static { + final ByteBuddy bb = new ByteBuddy(); + CDO = bb.subclass(CodecDataObject.class).visit(ByteBuddyUtils.computeFrames()).modifiers(PUB_FINAL); + ACDO = bb.subclass(AugmentableCodecDataObject.class).visit(ByteBuddyUtils.computeFrames()).modifiers(PUB_FINAL); + } + + private final ImmutableMap properties; + private final Entry keyMethod; + private final Builder template; + + private CodecDataObjectGenerator(final Builder template, + final ImmutableMap properties, + final @Nullable Entry keyMethod) { + this.template = requireNonNull(template); + this.properties = requireNonNull(properties); + this.keyMethod = keyMethod; + } + + static > Class generate(final CodecClassLoader loader, + final Class bindingInterface, final ImmutableMap properties, + final Entry keyMethod) { + return loader.generateClass(bindingInterface, "codecImpl", + new CodecDataObjectGenerator<>(CDO, properties, keyMethod)); + } + + static > Class generateAugmentable( + final CodecClassLoader loader, final Class bindingInterface, + final ImmutableMap properties, + final Entry keyMethod) { + return loader.generateClass(bindingInterface, "codecImpl", + new CodecDataObjectGenerator<>(ACDO, properties, keyMethod)); + } + + @Override + public GeneratorResult generateClass(final CodecClassLoader loeader, final String fqcn, + final Class bindingInterface) { + LOG.trace("Generating class {}", fqcn); + + @SuppressWarnings("unchecked") + Builder builder = (Builder) template.name(fqcn).implement(bindingInterface); + + for (Method method : properties.keySet()) { + LOG.trace("Generating for method {}", method); + final String methodName = method.getName(); + final TypeDescription retType = TypeDescription.ForLoadedType.of(method.getReturnType()); + builder = builder.defineMethod(methodName, retType, PUB_FINAL) + .intercept(new MethodImplementation(BB_NCS, BRIDGE_RESOLVE, CODEC_MEMBER, methodName, retType)); + } + + if (keyMethod != null) { + LOG.trace("Generating for key {}", keyMethod); + final Method method = keyMethod.getKey(); + final String methodName = method.getName(); + final TypeDescription retType = TypeDescription.ForLoadedType.of(method.getReturnType()); + builder = builder.defineMethod(methodName, retType, PUB_FINAL) + .intercept(new MethodImplementation(BB_IIC, BRIDGE_RESOLVE_KEY, CODEC_MEMBER_KEY, 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 ImmutableMap methods = Maps.uniqueIndex(properties.keySet(), + 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()); + } + + @Override + public Class customizeLoading(final @NonNull Supplier> loader) { + final CodecDataObjectGenerator prev = CodecDataObjectBridge.setup(this); + try { + final Class result = loader.get(); + + /* + * This a bit of magic to support NodeContextSupplier constants. These constants need to be resolved while + * we have the information needed to find them -- that information is being held in this instance and we + * leak it to a thread-local variable held by CodecDataObjectBridge. + * + * By default the JVM will defer class initialization to first use, which unfortunately is too late for + * us, and hence we need to force class to initialize. + */ + try { + Class.forName(result.getName(), true, result.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new LinkageError("Failed to find newly-defined " + result, e); + } + + return result; + } finally { + CodecDataObjectBridge.tearDown(prev); + } + } + + @NonNull NodeContextSupplier resolve(final @NonNull 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()); + } + + @NonNull IdentifiableItemCodec resolveKey(final @NonNull String methodName) { + return verifyNotNull(verifyNotNull(keyMethod, "No key method attached for %s in %s", methodName, this) + .getValue()); + } + + 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 static final class MethodImplementation implements Implementation { + private static final Generic BB_ARFU = TypeDefinition.Sort.describe(AtomicReferenceFieldUpdater.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 ARFU_NEWUPDATER = invokeMethod(AtomicReferenceFieldUpdater.class, + "newUpdater", Class.class, Class.class, String.class); + + private 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; + + private final Generic contextType; + private final StackManipulation resolveMethod; + private final StackManipulation codecMember; + private final TypeDescription retType; + + // getFoo + private final String methodName; + // getFoo$$$A + private final String arfuName; + // getFoo$$$C + private final String contextName; + + MethodImplementation(final Generic contextType, final StackManipulation resolveMethod, + final StackManipulation codecMember, final String methodName, final TypeDescription retType) { + this.contextType = requireNonNull(contextType); + this.resolveMethod = requireNonNull(resolveMethod); + this.codecMember = requireNonNull(codecMember); + this.methodName = requireNonNull(methodName); + this.retType = requireNonNull(retType); + this.arfuName = methodName + "$$$A"; + this.contextName = methodName + "$$$C"; + } + + @Override + public InstrumentedType prepare(final InstrumentedType instrumentedType) { + final InstrumentedType tmp = instrumentedType + // private static final AtomicReferenceFieldUpdater getFoo$$$A; + .withField(new FieldDescription.Token(arfuName, PRIV_CONST, BB_ARFU)) + // private static final getFoo$$$C; + .withField(new FieldDescription.Token(contextName, PRIV_CONST, contextType)) + // private volatile Object getFoo; + .withField(new FieldDescription.Token(methodName, PRIV_VOLATILE, BB_OBJECT)); + + // "getFoo" + final TextConstant methodNameText = new TextConstant(methodName); + + return tmp + .withInitializer(new ByteCodeAppender.Simple( + // getFoo$$$A = AtomicReferenceFieldUpdater.newUpdater(This.class, Object.class, "getFoo"); + ClassConstant.of(tmp), + OBJECT_CLASS, + methodNameText, + ARFU_NEWUPDATER, + putField(tmp, arfuName), + // getFoo$$$C = CodecDataObjectBridge.("getFoo"); + methodNameText, + resolveMethod, + putField(tmp, contextName))); + } + + @Override + public ByteCodeAppender appender(final Target implementationTarget) { + final TypeDescription instrumentedType = implementationTarget.getInstrumentedType(); + return new ByteCodeAppender.Simple( + // return (FooType) codecMember(getFoo$$$A, getFoo$$$C); + THIS, + getField(instrumentedType, arfuName), + getField(instrumentedType, contextName), + codecMember, + 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); + } + } +} diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectCodecContext.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectCodecContext.java index cb9fa451fb..6505b3afdb 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectCodecContext.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectCodecContext.java @@ -12,10 +12,10 @@ import static com.google.common.base.Verify.verify; import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; +import com.google.common.annotations.Beta; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; -import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -29,12 +29,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import javassist.CannotCompileException; -import javassist.CtClass; -import javassist.NotFoundException; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.mdsal.binding.dom.codec.loader.StaticClassPool; import org.opendaylight.mdsal.binding.generator.api.ClassLoadingStrategy; import org.opendaylight.mdsal.binding.model.api.JavaTypeName; import org.opendaylight.mdsal.binding.model.api.Type; @@ -64,7 +60,11 @@ import org.opendaylight.yangtools.yang.model.util.SchemaNodeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -abstract class DataObjectCodecContext +/** + * This class is an implementation detail. It is public only due to technical reasons and may change at any time. + */ +@Beta +public abstract class DataObjectCodecContext extends DataContainerCodecContext { private static final class Augmentations implements Immutable { final ImmutableMap> byYang; @@ -87,9 +87,6 @@ abstract class DataObjectCodecContext METHOD_BY_ALPHABET = Comparator.comparing(Method::getName); private static final Augmentations EMPTY_AUGMENTATIONS = new Augmentations(ImmutableMap.of(), ImmutableMap.of()); - private static final CtClass SUPERCLASS = StaticClassPool.findClass(CodecDataObject.class); - private static final CtClass AUGMENTABLE_SUPERCLASS = StaticClassPool.findClass( - AugmentableCodecDataObject.class); private final ImmutableMap leafChild; private final ImmutableMap byYang; @@ -168,27 +165,21 @@ abstract class DataObjectCodecContext> generatedClass; final MethodType ctorType; if (Augmentable.class.isAssignableFrom(bindingClass)) { this.possibleAugmentations = factory().getRuntimeContext().getAvailableAugmentationTypes(getSchema()); - superClass = AUGMENTABLE_SUPERCLASS; + generatedClass = CodecDataObjectGenerator.generateAugmentable(prototype.getFactory().getLoader(), + bindingClass, propBuilder.build(), keyMethod); ctorType = AUGMENTABLE_CONSTRUCTOR_TYPE; } else { this.possibleAugmentations = ImmutableMap.of(); - superClass = SUPERCLASS; + generatedClass = CodecDataObjectGenerator.generate(prototype.getFactory().getLoader(), bindingClass, + propBuilder.build(), keyMethod); ctorType = CONSTRUCTOR_TYPE; } reloadAllAugmentations(); - final Class generatedClass; - try { - generatedClass = prototype.getFactory().getLoader().generateSubclass(superClass, bindingClass, "codecImpl", - new CodecDataObjectCustomizer(propBuilder.build(), keyMethod)); - } catch (CannotCompileException | IOException | NotFoundException e) { - throw new LinkageError("Failed to generated class for " + bindingClass, e); - } - final MethodHandle ctor; try { ctor = MethodHandles.publicLookup().findConstructor(generatedClass, ctorType); diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamer.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamer.java index f48ade1b21..dd42c1e6e1 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamer.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamer.java @@ -164,7 +164,6 @@ public abstract class DataObjectStreamer implements DataOb writer.endNode(); } - private static void commonStreamLeafset(final BindingStreamEventWriter writer, final List value) throws IOException { for (Object entry : value) { diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerBridge.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerBridge.java deleted file mode 100644 index e75ee77e4c..0000000000 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerBridge.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.verifyNotNull; - -import com.google.common.annotations.Beta; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; - -/** - * Bridge for initializing {@link DataObjectStreamer} instance constants during class loading time. This class is public - * only due to implementation restrictions and can change at any time. - */ -// FIXME: this bridge is only necessary to work around Javassist compiler resolution of dependencies. If we switch to -// a more bytecode-oriented framework, this bridge becomes superfluous. -// -@Beta -public final class DataObjectStreamerBridge { - private static final ThreadLocal CURRENT_CUSTOMIZER = new ThreadLocal<>(); - - private DataObjectStreamerBridge() { - - } - - public static @NonNull DataObjectStreamer resolve(final @NonNull String methodName) { - return verifyNotNull(CURRENT_CUSTOMIZER.get(), "No customizer attached").resolve(methodName); - } - - static @Nullable DataObjectStreamerCustomizer setup(final @NonNull DataObjectStreamerCustomizer next) { - final DataObjectStreamerCustomizer prev = CURRENT_CUSTOMIZER.get(); - CURRENT_CUSTOMIZER.set(verifyNotNull(next)); - return prev; - } - - static void tearDown(final @Nullable DataObjectStreamerCustomizer prev) { - if (prev == null) { - CURRENT_CUSTOMIZER.remove(); - } else { - CURRENT_CUSTOMIZER.set(prev); - } - } -} diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerCustomizer.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerCustomizer.java deleted file mode 100644 index f958c15fb3..0000000000 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerCustomizer.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * 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 com.google.common.annotations.Beta; -import com.google.common.base.Supplier; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import javassist.CannotCompileException; -import javassist.CtClass; -import javassist.CtField; -import javassist.CtMethod; -import javassist.Modifier; -import javassist.NotFoundException; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.mdsal.binding.dom.codec.impl.NodeCodecContext.CodecContextFactory; -import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader; -import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader.Customizer; -import org.opendaylight.mdsal.binding.dom.codec.loader.StaticClassPool; -import org.opendaylight.mdsal.binding.dom.codec.util.BindingSchemaMapping; -import org.opendaylight.mdsal.binding.model.api.GeneratedType; -import org.opendaylight.mdsal.binding.model.api.MethodSignature; -import org.opendaylight.mdsal.binding.model.api.ParameterizedType; -import org.opendaylight.mdsal.binding.model.api.Type; -import org.opendaylight.mdsal.binding.spec.naming.BindingMapping; -import org.opendaylight.yangtools.yang.binding.Augmentable; -import org.opendaylight.yangtools.yang.binding.BindingStreamEventWriter; -import org.opendaylight.yangtools.yang.binding.DataObject; -import org.opendaylight.yangtools.yang.binding.DataObjectSerializerRegistry; -import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode; -import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode; -import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode; -import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; -import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; -import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; -import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; -import org.opendaylight.yangtools.yang.model.api.DocumentedNode.WithStatus; -import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; -import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; -import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; -import org.opendaylight.yangtools.yang.model.api.NotificationDefinition; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Beta -public final class DataObjectStreamerCustomizer implements Customizer { - static final CtClass CT_DOS = StaticClassPool.findClass(DataObjectStreamer.class); - static final String INSTANCE_FIELD = "INSTANCE"; - - private static final Logger LOG = LoggerFactory.getLogger(DataObjectStreamerCustomizer.class); - private static final String UNKNOWN_SIZE = BindingStreamEventWriter.class.getName() + ".UNKNOWN_SIZE"; - - private static final CtClass CT_VOID = StaticClassPool.findClass(void.class); - private static final CtClass[] SERIALIZE_ARGS = new CtClass[] { - StaticClassPool.findClass(DataObjectSerializerRegistry.class), - StaticClassPool.findClass(DataObject.class), - StaticClassPool.findClass(BindingStreamEventWriter.class) - }; - - private final Map> constants = new HashMap<>(); - private final ImmutableMap props; - private final CodecContextFactory registry; - private final DataNodeContainer schema; - private final String startEvent; - private final Class type; - - DataObjectStreamerCustomizer(final CodecContextFactory registry, final GeneratedType genType, - final DataNodeContainer schema, final Class type, final String startEvent) { - this.registry = requireNonNull(registry); - this.schema = requireNonNull(schema); - this.type = requireNonNull(type); - this.startEvent = requireNonNull(startEvent); - props = collectAllProperties(genType); - } - - public static DataObjectStreamerCustomizer create(final CodecContextFactory registry, final Class type) { - final Entry typeAndSchema = registry.getRuntimeContext().getTypeWithSchema(type); - final WithStatus schema = typeAndSchema.getValue(); - - final String startEvent; - if (schema instanceof ContainerSchemaNode || schema instanceof NotificationDefinition) { - startEvent = "startContainerNode(" + type.getName() + ".class," + UNKNOWN_SIZE; - } else if (schema instanceof ListSchemaNode) { - final ListSchemaNode casted = (ListSchemaNode) schema; - if (!casted.getKeyDefinition().isEmpty()) { - startEvent = "startMapEntryNode(obj." + BindingMapping.IDENTIFIABLE_KEY_NAME + "(), " + UNKNOWN_SIZE; - } else { - startEvent = "startUnkeyedListItem(" + UNKNOWN_SIZE; - } - } else if (schema instanceof AugmentationSchemaNode) { - startEvent = "startAugmentationNode(" + type.getName() + ".class"; - } else if (schema instanceof CaseSchemaNode) { - startEvent = "startCase(" + type.getName() + ".class, " + UNKNOWN_SIZE; - } else { - throw new UnsupportedOperationException("Schema type " + schema.getClass() + " is not supported"); - } - - return new DataObjectStreamerCustomizer(registry, typeAndSchema.getKey(), (DataNodeContainer) schema, type, - startEvent); - } - - @Override - public List> customize(final CodecClassLoader loader, final CtClass bindingClass, final CtClass generated) - throws CannotCompileException, NotFoundException, IOException { - LOG.trace("Definining streamer {}", generated.getName()); - - final CtField instanceField = new CtField(generated, INSTANCE_FIELD, generated); - instanceField.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL); - generated.addField(instanceField, "new " + generated.getName() + "()"); - - // This results in a body - final String objType = bindingClass.getName(); - final StringBuilder sb = new StringBuilder() - .append("{\n") - .append("final ").append(objType).append(" obj = (").append(objType).append(") $2;\n") - .append("$3.").append(startEvent).append(");\n"); - - final List> dependencies = emitChildren(sb, loader, generated); - if (Augmentable.class.isAssignableFrom(type)) { - sb.append("streamAugmentations($1, $3, obj);\n"); - } - - sb.append("$3.endNode();\n") - .append('}'); - - final CtMethod serialize = new CtMethod(CT_VOID, "serialize", SERIALIZE_ARGS, generated); - serialize.setModifiers(Modifier.PUBLIC); - serialize.setBody(sb.toString()); - generated.addMethod(serialize); - - generated.setModifiers(Modifier.PUBLIC | Modifier.FINAL); - LOG.trace("Definition of {} done", generated.getName()); - - return dependencies; - } - - @Override - public Class customizeLoading(final @NonNull Supplier> loader) { - if (constants.isEmpty()) { - return loader.get(); - } - - final DataObjectStreamerCustomizer prev = DataObjectStreamerBridge.setup(this); - try { - final Class result = loader.get(); - - /* - * This a bit of magic to support DataObjectStreamer constants. These constants need to be resolved while - * we have the information needed to find them -- that information is being held in this instance and we - * leak it to a thread-local variable held by DataObjectStreamerBridge. - * - * By default the JVM will defer class initialization to first use, which unfortunately is too late for - * us, and hence we need to force class to initialize. - */ - try { - Class.forName(result.getName(), true, result.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new LinkageError("Failed to find newly-defined " + result, e); - } - - return result; - } finally { - DataObjectStreamerBridge.tearDown(prev); - } - } - - @NonNull DataObjectStreamer resolve(final @NonNull String methodName) { - final Class target = verifyNotNull(constants.get(methodName), "Cannot resolve type of %s", - methodName); - return verifyNotNull(registry.getDataObjectSerializer(target), "Cannot find serializer for %s", target); - } - - private List> emitChildren(final StringBuilder sb, final CodecClassLoader loader, - final CtClass generated) throws CannotCompileException { - final List> dependencies = new ArrayList<>(); - - for (final DataSchemaNode schemaChild : schema.getChildNodes()) { - if (!schemaChild.isAugmenting()) { - final String getterName = BindingSchemaMapping.getGetterMethodName(schemaChild); - final Method getter; - try { - getter = type.getMethod(getterName); - } catch (NoSuchMethodException e) { - throw new IllegalStateException("Failed to find getter " + getterName, e); - } - - final Class dependency = emitChild(sb, loader, generated, getterName, getter.getReturnType(), - schemaChild); - if (dependency != null) { - LOG.trace("Require dependency {}", dependency); - dependencies.add(dependency); - } - } - } - - return dependencies; - } - - private @Nullable Class emitChild(final StringBuilder sb, final CodecClassLoader loader, final CtClass generated, - final String getterName, final Class returnType, final DataSchemaNode child) - throws CannotCompileException { - if (child instanceof LeafSchemaNode) { - sb.append("streamLeaf($3, \"").append(child.getQName().getLocalName()).append("\", obj.") - .append(getterName).append("());\n"); - return null; - } - if (child instanceof ContainerSchemaNode) { - final Class itemClass = returnType.asSubclass(DataObject.class); - final String constField = declareDependency(generated, getterName, itemClass); - - sb.append("streamContainer(").append(constField).append(", $1, $3, obj.").append(getterName) - .append("());\n"); - return registry.getDataObjectSerializer(itemClass).getClass(); - } - if (child instanceof ListSchemaNode) { - final Type childType = props.get(getterName); - verify(childType instanceof ParameterizedType, "Unexpected type %s for %s", childType, getterName); - final Type valueType = ((ParameterizedType) childType).getActualTypeArguments()[0]; - final Class valueClass; - try { - valueClass = loader.loadClass(valueType.getFullyQualifiedName()); - } catch (ClassNotFoundException e) { - throw new LinkageError("Failed to load " + valueType, e); - } - - verify(DataObject.class.isAssignableFrom(valueClass), "Value type %s of %s is not a DataObject", valueClass, - returnType); - final Class itemClass = valueClass.asSubclass(DataObject.class); - final ListSchemaNode casted = (ListSchemaNode) child; - - sb.append("stream"); - if (casted.getKeyDefinition().isEmpty()) { - sb.append("List"); - } else { - if (casted.isUserOrdered()) { - sb.append("Ordered"); - } - sb.append("Map"); - } - - final String constField = declareDependency(generated, getterName, itemClass); - sb.append('(').append(valueClass.getName()).append(".class, ").append(constField).append(", $1, $3, obj.") - .append(getterName).append("());\n"); - return registry.getDataObjectSerializer(itemClass).getClass(); - } - if (child instanceof AnyXmlSchemaNode) { - sb.append("streamAnyxml($3, \"").append(child.getQName().getLocalName()).append("\", obj.") - .append(getterName).append("());\n"); - return null; - } - if (child instanceof LeafListSchemaNode) { - sb.append("stream"); - if (((LeafListSchemaNode) child).isUserOrdered()) { - sb.append("Ordered"); - } - sb.append("LeafList($3, \"").append(child.getQName().getLocalName()).append("\", obj.") - .append(getterName).append("());\n"); - return null; - } - if (child instanceof ChoiceSchemaNode) { - sb.append("streamChoice(").append(returnType.getName()).append(".class, $1, $3, obj.").append(getterName) - .append("());\n"); - return null; - } - - LOG.debug("Ignoring {} due to unhandled schema {}", getterName, child); - return null; - } - - /* - * Javassist not quite helpful in our environment. We really want to output plain bytecode so that it links - * using normal ClassLoader mechanics (supported via CodecClassLoader), but Javassist's compiler really gets in - * the way of keeping things simple by requiring CtClass references to dependencies at the the time we set the - * implementation body. In order to side-step those requirements, we rely on defining references to our dependencies - * as constants and fill them up via customizeLoading(). - * - * This method defines the constants for later use. Should we migrate to a more byte-code oriented framework - * (like ByteBuddy), we will pay some cost in assembling the method bodies, we can ditch the constants, as we - * provide INSTANCE_FIELD which can readily be reused and CodecClassLoader will resolve the dependencies without - * any problems. - */ - private String declareDependency(final CtClass generated, final String getterName, - final Class bindingClass) throws CannotCompileException { - final String fieldName = getterName + "_STREAMER"; - - final CtField instanceField = new CtField(CT_DOS, fieldName, generated); - instanceField.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL); - generated.addField(instanceField, - DataObjectStreamerBridge.class.getName() + ".resolve(\"" + getterName + "\")"); - - verify(constants.put(getterName, bindingClass) == null, "Duplicate dependency for %s", getterName); - return fieldName; - } - - private static ImmutableMap collectAllProperties(final GeneratedType type) { - final Map props = new HashMap<>(); - collectAllProperties(type, props); - return ImmutableMap.copyOf(props); - } - - private static void collectAllProperties(final GeneratedType type, final Map hashMap) { - for (final MethodSignature definition : type.getMethodDefinitions()) { - hashMap.put(definition.getName(), definition.getReturnType()); - } - for (final Type parent : type.getImplements()) { - if (parent instanceof GeneratedType) { - collectAllProperties((GeneratedType) parent, hashMap); - } - } - } -} diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerGenerator.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerGenerator.java new file mode 100644 index 0000000000..fa7b0c7443 --- /dev/null +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataObjectStreamerGenerator.java @@ -0,0 +1,453 @@ +/* + * 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 java.util.Objects.requireNonNull; +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.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +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.TypeDefinition.Sort; +import net.bytebuddy.description.type.TypeDescription; +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.ByteCodeAppender; +import net.bytebuddy.implementation.bytecode.Duplication; +import net.bytebuddy.implementation.bytecode.StackManipulation; +import net.bytebuddy.implementation.bytecode.TypeCreation; +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.MethodInvocation; +import net.bytebuddy.implementation.bytecode.member.MethodReturn; +import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatchers; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.mdsal.binding.dom.codec.impl.NodeCodecContext.CodecContextFactory; +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.mdsal.binding.dom.codec.util.BindingSchemaMapping; +import org.opendaylight.mdsal.binding.model.api.GeneratedType; +import org.opendaylight.mdsal.binding.model.api.MethodSignature; +import org.opendaylight.mdsal.binding.model.api.ParameterizedType; +import org.opendaylight.mdsal.binding.model.api.Type; +import org.opendaylight.yangtools.yang.binding.Augmentable; +import org.opendaylight.yangtools.yang.binding.BindingStreamEventWriter; +import org.opendaylight.yangtools.yang.binding.DataContainer; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.DataObjectSerializerRegistry; +import org.opendaylight.yangtools.yang.binding.Identifiable; +import org.opendaylight.yangtools.yang.binding.Identifier; +import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode; +import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode; +import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DocumentedNode.WithStatus; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.NotificationDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class DataObjectStreamerGenerator> implements ClassGenerator { + static final String INSTANCE_FIELD = "INSTANCE"; + + private static final Logger LOG = LoggerFactory.getLogger(DataObjectStreamerGenerator.class); + private static final Generic BB_VOID = TypeDefinition.Sort.describe(void.class); + private static final Generic BB_DATAOBJECT = TypeDefinition.Sort.describe(DataObject.class); + private static final Generic BB_DOSR = TypeDefinition.Sort.describe(DataObjectSerializerRegistry.class); + private static final Generic BB_BESV = TypeDefinition.Sort.describe(BindingStreamEventWriter.class); + private static final Generic BB_IOX = TypeDefinition.Sort.describe(IOException.class); + + private static final int PUB_FINAL = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC; + private static final int PUB_CONST = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL + | Opcodes.ACC_SYNTHETIC; + + private static final Builder TEMPLATE = new ByteBuddy().subclass(DataObjectStreamer.class) + .modifiers(PUB_FINAL); + + private static final StackManipulation REG = MethodVariableAccess.REFERENCE.loadFrom(1); + private static final StackManipulation OBJ = MethodVariableAccess.REFERENCE.loadFrom(2); + private static final StackManipulation STREAM = MethodVariableAccess.REFERENCE.loadFrom(3); + private static final StackManipulation UNKNOWN_SIZE = IntegerConstant.forValue( + BindingStreamEventWriter.UNKNOWN_SIZE); + + private static final StackManipulation START_AUGMENTATION_NODE = invokeMethod(BindingStreamEventWriter.class, + "startAugmentationNode", Class.class); + private static final StackManipulation START_CASE = invokeMethod(BindingStreamEventWriter.class, + "startCase", Class.class, int.class); + private static final StackManipulation START_CONTAINER_NODE = invokeMethod(BindingStreamEventWriter.class, + "startContainerNode", Class.class, int.class); + private static final StackManipulation END_NODE = invokeMethod(BindingStreamEventWriter.class, + "endNode"); + + // startMapEntryNode(obj.key(), UNKNOWN_SIZE); + private static final StackManipulation START_MAP_ENTRY_NODE = new StackManipulation.Compound( + OBJ, + invokeMethod(Identifiable.class, "key"), + UNKNOWN_SIZE, + invokeMethod(BindingStreamEventWriter.class, "startMapEntryNode", Identifier.class, int.class)); + + // startUnkeyedListItem(UNKNOWN_SIZE); + private static final StackManipulation START_UNKEYED_LIST_ITEM = new StackManipulation.Compound( + UNKNOWN_SIZE, + invokeMethod(BindingStreamEventWriter.class, "startUnkeyedListItem", int.class)); + + private static final StackManipulation STREAM_ANYXML = invokeMethod(DataObjectStreamer.class, + "streamAnyxml", BindingStreamEventWriter.class, String.class, Object.class); + private static final StackManipulation STREAM_CHOICE = invokeMethod(DataObjectStreamer.class, + "streamChoice", Class.class, DataObjectSerializerRegistry.class, BindingStreamEventWriter.class, + DataContainer.class); + private static final StackManipulation STREAM_CONTAINER = invokeMethod(DataObjectStreamer.class, + "streamContainer", DataObjectStreamer.class, DataObjectSerializerRegistry.class, BindingStreamEventWriter.class, + DataObject.class); + private static final StackManipulation STREAM_LEAF = invokeMethod(DataObjectStreamer.class, + "streamLeaf", BindingStreamEventWriter.class, String.class, Object.class); + private static final StackManipulation STREAM_LEAF_LIST = invokeMethod(DataObjectStreamer.class, + "streamLeafList", + BindingStreamEventWriter.class, String.class, List.class); + private static final StackManipulation STREAM_ORDERED_LEAF_LIST = invokeMethod(DataObjectStreamer.class, + "streamOrderedLeafList", BindingStreamEventWriter.class, String.class, List.class); + private static final StackManipulation STREAM_LIST = invokeMethod(DataObjectStreamer.class, + "streamList", Class.class, DataObjectStreamer.class, DataObjectSerializerRegistry.class, + BindingStreamEventWriter.class, List.class); + private static final StackManipulation STREAM_MAP = invokeMethod(DataObjectStreamer.class, + "streamMap", Class.class, DataObjectStreamer.class, DataObjectSerializerRegistry.class, + BindingStreamEventWriter.class, List.class); + private static final StackManipulation STREAM_ORDERED_MAP = invokeMethod(DataObjectStreamer.class, + "streamOrderedMap", Class.class, DataObjectStreamer.class, DataObjectSerializerRegistry.class, + BindingStreamEventWriter.class, List.class); + + // streamAugmentations(reg, stream, obj); + private static final StackManipulation STREAM_AUGMENTATIONS = new StackManipulation.Compound( + REG, + STREAM, + OBJ, + invokeMethod(DataObjectStreamer.class, "streamAugmentations", DataObjectSerializerRegistry.class, + BindingStreamEventWriter.class, Augmentable.class)); + + private final CodecContextFactory registry; + private final StackManipulation startEvent; + private final DataNodeContainer schema; + private final Class type; + private final GeneratedType genType; + + private DataObjectStreamerGenerator(final CodecContextFactory registry, final GeneratedType genType, + final DataNodeContainer schema, final Class type, final StackManipulation startEvent) { + this.registry = requireNonNull(registry); + this.genType = requireNonNull(genType); + this.schema = requireNonNull(schema); + this.type = requireNonNull(type); + this.startEvent = requireNonNull(startEvent); + } + + static Class> generateStreamer(final CodecClassLoader loader, + final CodecContextFactory registry, final Class type) { + + final Entry typeAndSchema = registry.getRuntimeContext().getTypeWithSchema(type); + final WithStatus schema = typeAndSchema.getValue(); + + final StackManipulation startEvent; + if (schema instanceof ContainerSchemaNode || schema instanceof NotificationDefinition) { + startEvent = classUnknownSizeMethod(START_CONTAINER_NODE, type); + } else if (schema instanceof ListSchemaNode) { + startEvent = ((ListSchemaNode) schema).getKeyDefinition().isEmpty() ? START_UNKEYED_LIST_ITEM + : START_MAP_ENTRY_NODE; + } else if (schema instanceof AugmentationSchemaNode) { + // startAugmentationNode(Foo.class); + startEvent = new StackManipulation.Compound( + ClassConstant.of(Sort.describe(type).asErasure()), + START_AUGMENTATION_NODE); + } else if (schema instanceof CaseSchemaNode) { + startEvent = classUnknownSizeMethod(START_CASE, type); + } else { + throw new UnsupportedOperationException("Schema type " + schema.getClass() + " is not supported"); + } + + return loader.generateClass(type, "streamer", + new DataObjectStreamerGenerator<>(registry, typeAndSchema.getKey(), (DataNodeContainer) schema, type, + startEvent)); + } + + @Override + public GeneratorResult generateClass(final CodecClassLoader loader, final String fqcn, + final Class bindingInterface) { + LOG.trace("Definining streamer {}", fqcn); + + @SuppressWarnings("unchecked") + Builder builder = (Builder) TEMPLATE.name(fqcn); + + final ImmutableMap props = collectAllProperties(genType); + final List children = new ArrayList<>(props.size()); + for (final DataSchemaNode schemaChild : schema.getChildNodes()) { + if (!schemaChild.isAugmenting()) { + final String getterName = BindingSchemaMapping.getGetterMethodName(schemaChild); + final Method getter; + try { + getter = type.getMethod(getterName); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to find getter " + getterName, e); + } + + final ChildStream child = createStream(loader, props, schemaChild, getter); + if (child != null) { + children.add(child); + } + } + } + + final ImmutableList.Builder> depBuilder = ImmutableList.builder(); + for (ChildStream child : children) { + final Class dependency = child.getDependency(); + if (dependency != null) { + depBuilder.add(dependency); + } + } + + final GeneratorResult result = GeneratorResult.of(builder + .defineMethod("serialize", BB_VOID, PUB_FINAL) + .withParameters(BB_DOSR, BB_DATAOBJECT, BB_BESV) + .throwing(BB_IOX) + .intercept(new SerializeImplementation(bindingInterface, startEvent, children)).make(), depBuilder.build()); + + LOG.trace("Definition of {} done", fqcn); + return result; + } + + private ChildStream createStream(final CodecClassLoader loader, final ImmutableMap props, + final DataSchemaNode childSchema, final Method getter) { + if (childSchema instanceof LeafSchemaNode) { + return qnameChildStream(STREAM_LEAF, getter, childSchema); + } + if (childSchema instanceof ContainerSchemaNode) { + return containerChildStream(getter); + } + if (childSchema instanceof ListSchemaNode) { + final String getterName = getter.getName(); + final Type childType = props.get(getterName); + verify(childType instanceof ParameterizedType, "Unexpected type %s for %s", childType, getterName); + final Type valueType = ((ParameterizedType) childType).getActualTypeArguments()[0]; + final Class valueClass; + try { + valueClass = loader.loadClass(valueType.getFullyQualifiedName()); + } catch (ClassNotFoundException e) { + throw new LinkageError("Failed to load " + valueType, e); + } + return listChildStream(getter, valueClass.asSubclass(DataObject.class), (ListSchemaNode) childSchema); + } + if (childSchema instanceof ChoiceSchemaNode) { + return choiceChildStream(getter); + } + if (childSchema instanceof AnyXmlSchemaNode) { + return qnameChildStream(STREAM_ANYXML, getter, childSchema); + } + if (childSchema instanceof LeafListSchemaNode) { + return qnameChildStream(((LeafListSchemaNode) childSchema).isUserOrdered() ? STREAM_ORDERED_LEAF_LIST + : STREAM_LEAF_LIST, getter, childSchema); + } + + LOG.debug("Ignoring {} due to unhandled schema {}", getter, childSchema); + return null; + } + + private static ChildStream choiceChildStream(final Method getter) { + // streamChoice(Foo.class, reg, stream, obj.getFoo()); + return new ChildStream( + ClassConstant.of(Sort.describe(getter.getReturnType()).asErasure()), + REG, + STREAM, + OBJ, + invokeMethod(getter), + STREAM_CHOICE); + } + + private ChildStream containerChildStream(final Method getter) { + final Class itemClass = getter.getReturnType().asSubclass(DataObject.class); + final DataObjectStreamer streamer = registry.getDataObjectSerializer(itemClass); + + // streamContainer(FooStreamer.INSTANCE, reg, stream, obj.getFoo()); + return new ChildStream(streamer, + streamerInstance(streamer), + REG, + STREAM, + OBJ, + invokeMethod(getter), + STREAM_CONTAINER); + } + + private ChildStream listChildStream(final Method getter, final Class itemClass, + final ListSchemaNode childSchema) { + final DataObjectStreamer streamer = registry.getDataObjectSerializer(itemClass); + final StackManipulation method; + if (childSchema.getKeyDefinition().isEmpty()) { + method = STREAM_LIST; + } else { + method = childSchema.isUserOrdered() ? STREAM_ORDERED_MAP : STREAM_MAP; + } + + // (Foo.class, FooStreamer.INSTACE, reg, stream, obj.getFoo()); + return new ChildStream(streamer, + ClassConstant.of(Sort.describe(itemClass).asErasure()), + streamerInstance(streamer), + REG, + STREAM, + OBJ, + invokeMethod(getter), + method); + } + + private static ChildStream qnameChildStream(final StackManipulation method, final Method getter, + final DataSchemaNode schema) { + // (stream, "foo", obj.getFoo()); + return new ChildStream( + STREAM, + new TextConstant(schema.getQName().getLocalName()), + OBJ, + invokeMethod(getter), + method); + } + + private static StackManipulation streamerInstance(final DataObjectStreamer streamer) { + try { + return getField(streamer.getClass().getDeclaredField(INSTANCE_FIELD)); + } catch (NoSuchFieldException e) { + throw new IllegalStateException(e); + } + } + + private static StackManipulation classUnknownSizeMethod(final StackManipulation method, final Class type) { + // (Foo.class, UNKNOWN_SIZE); + return new StackManipulation.Compound( + ClassConstant.of(Sort.describe(type).asErasure()), + UNKNOWN_SIZE, + method); + } + + private static ImmutableMap collectAllProperties(final GeneratedType type) { + final Map props = new HashMap<>(); + collectAllProperties(type, props); + return ImmutableMap.copyOf(props); + } + + private static void collectAllProperties(final GeneratedType type, final Map hashMap) { + for (final MethodSignature definition : type.getMethodDefinitions()) { + hashMap.put(definition.getName(), definition.getReturnType()); + } + for (final Type parent : type.getImplements()) { + if (parent instanceof GeneratedType) { + collectAllProperties((GeneratedType) parent, hashMap); + } + } + } + + private static final class SerializeImplementation implements Implementation { + private final List children; + private final StackManipulation startEvent; + private final Class bindingInterface; + + SerializeImplementation(final Class bindingInterface, final StackManipulation startEvent, + final List children) { + this.bindingInterface = requireNonNull(bindingInterface); + this.startEvent = requireNonNull(startEvent); + this.children = requireNonNull(children); + } + + @Override + public InstrumentedType prepare(final InstrumentedType instrumentedType) { + return instrumentedType + // private static final This INSTANCE = new This(); + .withField(new FieldDescription.Token(INSTANCE_FIELD, PUB_CONST, instrumentedType.asGenericType())) + .withInitializer(InitializeInstanceField.INSTANCE); + } + + @Override + public ByteCodeAppender appender(final Target implementationTarget) { + final List manipulations = new ArrayList<>(children.size() + 6); + + // stream.(...); + manipulations.add(STREAM); + manipulations.add(startEvent); + + // ... emit children ... + manipulations.addAll(children); + + if (Augmentable.class.isAssignableFrom(bindingInterface)) { + // streamAugmentations(reg, stream, obj); + manipulations.add(STREAM_AUGMENTATIONS); + } + + // stream.endNode(); + manipulations.add(STREAM); + manipulations.add(END_NODE); + // return; + manipulations.add(MethodReturn.VOID); + + return new ByteCodeAppender.Simple(manipulations); + } + } + + private static final class ChildStream extends StackManipulation.Compound { + private final @Nullable Class dependency; + + ChildStream(final StackManipulation... stackManipulation) { + super(stackManipulation); + dependency = null; + } + + ChildStream(final DataObjectStreamer streamer, final StackManipulation... stackManipulation) { + super(stackManipulation); + dependency = streamer.getClass(); + } + + @Nullable Class getDependency() { + return dependency; + } + } + + private enum InitializeInstanceField implements ByteCodeAppender { + INSTANCE; + + @Override + public Size apply(final MethodVisitor methodVisitor, final Context implementationContext, + final MethodDescription instrumentedMethod) { + final TypeDescription instrumentedType = implementationContext.getInstrumentedType(); + StackManipulation.Size operandStackSize = new StackManipulation.Compound( + TypeCreation.of(instrumentedType), + Duplication.SINGLE, + MethodInvocation.invoke(instrumentedType.getDeclaredMethods() + .filter(ElementMatchers.isDefaultConstructor()).getOnly().asDefined()), + putField(instrumentedType, INSTANCE_FIELD)) + .apply(methodVisitor, implementationContext); + return new Size(operandStackSize.getMaximalSize(), instrumentedMethod.getStackSize()); + } + } +} diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/OpaqueNodeCodecContext.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/OpaqueNodeCodecContext.java index dbad1573d5..313e300ee8 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/OpaqueNodeCodecContext.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/OpaqueNodeCodecContext.java @@ -12,22 +12,19 @@ import static com.google.common.base.Verify.verify; import static java.util.Objects.requireNonNull; import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import javassist.CannotCompileException; -import javassist.CtClass; -import javassist.Modifier; -import javassist.NotFoundException; import javax.xml.transform.dom.DOMSource; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType.Builder; +import net.bytebuddy.jar.asm.Opcodes; import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.mdsal.binding.dom.codec.api.BindingOpaqueObjectCodecTreeNode; import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader; -import org.opendaylight.mdsal.binding.dom.codec.loader.StaticClassPool; +import org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader.GeneratorResult; import org.opendaylight.yangtools.concepts.Codec; import org.opendaylight.yangtools.yang.binding.OpaqueData; import org.opendaylight.yangtools.yang.binding.OpaqueObject; @@ -55,9 +52,11 @@ abstract class OpaqueNodeCodecContext> extends ValueNo } } - private static final CtClass SUPERCLASS = StaticClassPool.findClass(CodecOpaqueObject.class); private static final MethodType CONSTRUCTOR_TYPE = MethodType.methodType(OpaqueObject.class, OpaqueData.class); + @SuppressWarnings("rawtypes") + private static final Builder TEMPLATE = new ByteBuddy().subclass(CodecOpaqueObject.class) + .modifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC); private final Codec valueCodec = new Codec() { @Override @@ -132,17 +131,11 @@ abstract class OpaqueNodeCodecContext> extends ValueNo } private static MethodHandle createImpl(final CodecClassLoader rootLoader, final Class bindingClass) { - final Class proxyClass; - try { - proxyClass = rootLoader.generateSubclass(SUPERCLASS, bindingClass, "codecImpl", - (pool, binding, generated) -> { - generated.addInterface(binding); - generated.setModifiers(Modifier.PUBLIC | Modifier.FINAL); - return ImmutableList.of(); - }); - } catch (CannotCompileException | IOException | NotFoundException e) { - throw new LinkageError("Failed to instantiate prototype for " + bindingClass, e); - } + final Class proxyClass = rootLoader.generateClass(bindingClass, "codecImpl", + (loader, fqcn, bindingInterface) -> GeneratorResult.of(TEMPLATE + .name(fqcn) + .implement(bindingInterface) + .make())); Constructor ctor; try { diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/CodecClassLoader.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/CodecClassLoader.java index c8b2a60140..0a0c9480d7 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/CodecClassLoader.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/CodecClassLoader.java @@ -7,25 +7,26 @@ */ package org.opendaylight.mdsal.binding.dom.codec.loader; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Verify.verify; -import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; import com.google.common.annotations.Beta; import com.google.common.base.Strings; import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.io.File; import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collection; import java.util.HashSet; -import java.util.List; +import java.util.Map.Entry; import java.util.Set; -import javassist.CannotCompileException; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.LoaderClassPath; -import javassist.NotFoundException; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType.Unloaded; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import org.eclipse.jdt.annotation.NonNull; -import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,32 +45,19 @@ import org.slf4j.LoggerFactory; *

In single-classloader environments, obviously, the root loader can load all binding classes, and hence no leaf * loader is created. * - *

- * Each {@link CodecClassLoader} has a {@link ClassPool} attached to it and can perform operations on it. Leaf loaders - * specify the root loader's ClassPool as their parent, but are configured to lookup classes first in themselves. - * * @author Robert Varga */ @Beta public abstract class CodecClassLoader extends ClassLoader { - /** - * A customizer allowing a generated class to be modified before it is loader. - */ - @FunctionalInterface - public interface Customizer { + public interface ClassGenerator { /** - * Customize a generated class before it is instantiated in the loader. + * Generate a class. * - * @param loader CodecClassLoader which will hold the class. It can be used to lookup/instantiate other classes - * @param bindingClass Binding class for which the customized class is being generated - * @param generated The class being generated - * @return A set of generated classes the generated class depends on - * @throws CannotCompileException if the customizer cannot perform partial compilation - * @throws NotFoundException if the customizer cannot find a required class - * @throws IOException if the customizer cannot perform partial loading + * @param fqcn Generated class Fully-qualified class name + * @param bindingInterface Binding interface for which the class is being generated + * @return A result. */ - @NonNull List> customize(@NonNull CodecClassLoader loader, @NonNull CtClass bindingClass, - @NonNull CtClass generated) throws CannotCompileException, NotFoundException, IOException; + GeneratorResult generateClass(CodecClassLoader loader, String fqcn, Class bindingInterface); /** * Run the specified loader in a customized environment. The environment customizations must be cleaned up by @@ -78,75 +66,83 @@ public abstract class CodecClassLoader extends ClassLoader { * @param loader Class loader to execute * @return Class returned by the loader */ - default Class customizeLoading(final @NonNull Supplier> loader) { + default Class customizeLoading(final @NonNull Supplier> loader) { return loader.get(); } } + public static final class GeneratorResult { + private final @NonNull ImmutableSet> dependecies; + private final @NonNull Unloaded result; + + GeneratorResult(final Unloaded result, final ImmutableSet> dependecies) { + this.result = requireNonNull(result); + this.dependecies = requireNonNull(dependecies); + } + + public static @NonNull GeneratorResult of(final Unloaded result) { + return new GeneratorResult<>(result, ImmutableSet.of()); + } + + public static @NonNull GeneratorResult of(final Unloaded result, + final Collection> dependencies) { + return dependencies.isEmpty() ? of(result) : new GeneratorResult<>(result, + ImmutableSet.copyOf(dependencies)); + } + + @NonNull Unloaded getResult() { + return result; + } + + @NonNull ImmutableSet> getDependencies() { + return dependecies; + } + } + + private static final ClassLoadingStrategy STRATEGY = (classLoader, types) -> { + verify(types.size() == 1, "Unexpected multiple types", types); + final Entry entry = types.entrySet().iterator().next(); + return ImmutableMap.of(entry.getKey(), classLoader.loadClass(entry.getKey().getName(), entry.getValue())); + }; + static { verify(ClassLoader.registerAsParallelCapable()); } private static final Logger LOG = LoggerFactory.getLogger(CodecClassLoader.class); + private static final File BYTECODE_DIRECTORY; - private final ClassPool classPool; - - private CodecClassLoader(final ClassLoader parentLoader, final ClassPool parentPool) { - super(parentLoader); - this.classPool = new ClassPool(parentPool); - this.classPool.childFirstLookup = true; - this.classPool.appendClassPath(new LoaderClassPath(this)); - } - - CodecClassLoader() { - this(StaticClassPool.LOADER, StaticClassPool.POOL); + static { + final String dir = System.getProperty("org.opendaylight.mdsal.binding.dom.codec.loader.bytecodeDumpDirectory"); + BYTECODE_DIRECTORY = Strings.isNullOrEmpty(dir) ? null : new File(dir); } - CodecClassLoader(final CodecClassLoader parent) { - this(parent, parent.classPool); + CodecClassLoader(final ClassLoader parentLoader) { + super(parentLoader); } /** - * Turn a Class instance into a CtClass for referencing it in code generation. This method supports both - * generated- and non-generated classes. + * Instantiate a new CodecClassLoader, which serves as the root of generated code loading. * - * @param clazz Class to be looked up. - * @return A CtClass instance - * @throws NotFoundException if the class cannot be found - * @throws NullPointerException if {@code clazz} is null + * @return A new CodecClassLoader. */ - public final @NonNull CtClass findClass(final @NonNull Class clazz) throws NotFoundException { - return BindingReflections.isBindingClass(clazz) ? findClassLoader(clazz).getLocalFrozen(clazz.getName()) - : StaticClassPool.findClass(clazz); + public static @NonNull CodecClassLoader create() { + return AccessController.doPrivileged((PrivilegedAction)() -> new RootCodecClassLoader()); } /** - * Create a new class by subclassing specified class and running a customizer on it. The name of the target class - * is formed through concatenation of the name of a {@code bindingInterface} and specified {@code suffix} + * The name of the target class is formed through concatenation of the name of a {@code bindingInterface} and + * specified {@code suffix}. * - * @param superClass Superclass from which to derive * @param bindingInterface Binding compile-time-generated interface * @param suffix Suffix to use - * @param customizer Customizer to use to process the class + * @param generator Code generator to run * @return A generated class object - * @throws CannotCompileException if the resulting generated class cannot be compiled or customized - * @throws NotFoundException if the binding interface cannot be found or the generated class cannot be customized - * @throws IOException if the generated class cannot be turned into bytecode or the generator fails with IOException * @throws NullPointerException if any argument is null */ - public final Class generateSubclass(final CtClass superClass, final Class bindingInterface, - final String suffix, final Customizer customizer) throws CannotCompileException, IOException, - NotFoundException { - return findClassLoader(requireNonNull(bindingInterface)) - .doGenerateSubclass(superClass, bindingInterface, suffix, customizer); - } - - final @NonNull CtClass getLocalFrozen(final String name) throws NotFoundException { - synchronized (getClassLoadingLock(name)) { - final CtClass result = classPool.get(name); - result.freeze(); - return result; - } + public final Class generateClass(final Class bindingInterface, + final String suffix, final ClassGenerator generator) { + return findClassLoader(requireNonNull(bindingInterface)).doGenerateClass(bindingInterface, suffix, generator); } /** @@ -168,53 +164,37 @@ public abstract class CodecClassLoader extends ClassLoader { */ abstract @NonNull CodecClassLoader findClassLoader(@NonNull Class bindingClass); - private Class doGenerateSubclass(final CtClass superClass, final Class bindingInterface, final String suffix, - final Customizer customizer) throws CannotCompileException, IOException, NotFoundException { - checkArgument(!superClass.isInterface(), "%s must not be an interface", superClass); - checkArgument(bindingInterface.isInterface(), "%s is not an interface", bindingInterface); - checkArgument(!Strings.isNullOrEmpty(suffix)); + private Class doGenerateClass(final Class bindingInterface, final String suffix, + final ClassGenerator generator) { + final String fqcn = bindingInterface.getName() + "$$$" + suffix; - final String bindingName = bindingInterface.getName(); - final String fqn = bindingName + "$$$" + suffix; - synchronized (getClassLoadingLock(fqn)) { + synchronized (getClassLoadingLock(fqcn)) { // Attempt to find a loaded class - final Class loaded = findLoadedClass(fqn); - if (loaded != null) { - return loaded; + final Class existing = findLoadedClass(fqcn); + if (existing != null) { + return (Class) existing; } - // Get the interface - final CtClass bindingCt = getLocalFrozen(bindingName); - try { - final byte[] byteCode; - final CtClass generated = verifyNotNull(classPool.makeClass(fqn, superClass)); - try { - final List> deps = customizer.customize(this, bindingCt, generated); - final String ctName = generated.getName(); - verify(fqn.equals(ctName), "Target class is %s returned result is %s", fqn, ctName); - processDependencies(deps); - - byteCode = generated.toBytecode(); - } finally { - // Always detach the result, as we will never use it again - generated.detach(); - } - - return customizer.customizeLoading(() -> { - final Class newClass = defineClass(fqn, byteCode, 0, byteCode.length); - resolveClass(newClass); - return newClass; - }); - } finally { - // Binding interfaces are used only a few times, hence it does not make sense to cache them in the class - // pool. - // TODO: this hinders caching, hence we should re-think this - bindingCt.detach(); - } + final GeneratorResult result = generator.generateClass(this, fqcn, bindingInterface); + final Unloaded unloaded = result.getResult(); + verify(fqcn.equals(unloaded.getTypeDescription().getName()), "Unexpected class in %s", unloaded); + verify(unloaded.getAuxiliaryTypes().isEmpty(), "Auxiliary types present in %s", unloaded); + dumpBytecode(unloaded); + + processDependencies(result.getDependencies()); + return generator.customizeLoading(() -> (Class) unloaded.load(this, STRATEGY).getLoaded()); } } - private void processDependencies(final List> deps) { + final Class loadClass(final String fqcn, final byte[] byteCode) { + synchronized (getClassLoadingLock(fqcn)) { + final Class existing = findLoadedClass(fqcn); + verify(existing == null, "Attempted to load existing %s", existing); + return defineClass(fqcn, byteCode, 0, byteCode.length); + } + } + + private void processDependencies(final Collection> deps) { final Set depLoaders = new HashSet<>(); for (Class dep : deps) { final ClassLoader depLoader = dep.getClassLoader(); @@ -238,4 +218,14 @@ public abstract class CodecClassLoader extends ClassLoader { appendLoaders(depLoaders); } } + + private static void dumpBytecode(final Unloaded unloaded) { + if (BYTECODE_DIRECTORY != null) { + try { + unloaded.saveIn(BYTECODE_DIRECTORY); + } catch (IOException | IllegalArgumentException e) { + LOG.info("Failed to save {}", unloaded.getTypeDescription().getName(), e); + } + } + } } diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/LeafCodecClassLoader.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/LeafCodecClassLoader.java index eebeb6c818..ef2eed4052 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/LeafCodecClassLoader.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/LeafCodecClassLoader.java @@ -48,10 +48,13 @@ final class LeafCodecClassLoader extends CodecClassLoader { } catch (ClassNotFoundException e) { LOG.trace("Class {} not found in target, looking through dependencies", name); for (LeafCodecClassLoader loader : dependencies) { - final Class loaded = loader.findLoadedClass(name); - if (loaded != null) { - LOG.trace("Class {} found in dependency {}", name, loader); - return loaded; + // Careful: a loading operation may be underway, make sure that process has completed + synchronized (loader.getClassLoadingLock(name)) { + final Class loaded = loader.findLoadedClass(name); + if (loaded != null) { + LOG.trace("Class {} found in dependency {}", name, loader); + return loaded; + } } } diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/RootCodecClassLoader.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/RootCodecClassLoader.java index 1b2b8023d9..5465e83cac 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/RootCodecClassLoader.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/RootCodecClassLoader.java @@ -8,6 +8,7 @@ package org.opendaylight.mdsal.binding.dom.codec.loader; import static com.google.common.base.Verify.verify; +import static com.google.common.base.Verify.verifyNotNull; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; @@ -15,12 +16,14 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Set; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.opendaylight.yangtools.yang.binding.DataContainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // A root codec classloader, binding only whatever is available StaticClassPool final class RootCodecClassLoader extends CodecClassLoader { private static final Logger LOG = LoggerFactory.getLogger(RootCodecClassLoader.class); + private static final ClassLoader LOADER = verifyNotNull(RootCodecClassLoader.class.getClassLoader()); static { verify(registerAsParallelCapable()); @@ -33,7 +36,7 @@ final class RootCodecClassLoader extends CodecClassLoader { private volatile ImmutableMap loaders = ImmutableMap.of(); RootCodecClassLoader() { - super(); + super(LOADER); } @Override @@ -55,7 +58,7 @@ final class RootCodecClassLoader extends CodecClassLoader { // ourselves) or we need to create a new Leaf. final CodecClassLoader found; if (!isOurClass(bindingClass)) { - StaticClassPool.verifyStaticLinkage(target); + verifyStaticLinkage(target); found = AccessController.doPrivileged( (PrivilegedAction)() -> new LeafCodecClassLoader(this, target)); } else { @@ -99,4 +102,17 @@ final class RootCodecClassLoader extends CodecClassLoader { } return bindingClass.equals(ourClass); } + + // Sanity check: target has to resolve yang-binding contents to the same class, otherwise we are in a pickle + private static void verifyStaticLinkage(final ClassLoader candidate) { + final Class targetClazz; + try { + targetClazz = candidate.loadClass(DataContainer.class.getName()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("ClassLoader " + candidate + " cannot load " + DataContainer.class, e); + } + verify(DataContainer.class.equals(targetClazz), + "Class mismatch on DataContainer. Ours is from %s, target %s has %s from %s", + DataContainer.class.getClassLoader(), candidate, targetClazz, targetClazz.getClassLoader()); + } } diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/StaticClassPool.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/StaticClassPool.java deleted file mode 100644 index a2b7ac6fe0..0000000000 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/StaticClassPool.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.loader; - -import static com.google.common.base.Verify.verify; -import static com.google.common.base.Verify.verifyNotNull; - -import com.google.common.annotations.Beta; -import java.security.AccessController; -import java.security.PrivilegedAction; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.LoaderClassPath; -import javassist.NotFoundException; -import org.eclipse.jdt.annotation.NonNull; -import org.opendaylight.yangtools.yang.binding.DataContainer; - -/** - * Static class pool, bound to the class loader of binding-dom-codec. It can be used to acquire CtClass instances that - * reside within the binding-dom-codec artifact or any of its direct mandatory dependencies. It can also instantiate - * {@link CodecClassLoader} instances for use with code generation. - * - * @author Robert Varga - */ -@Beta -public final class StaticClassPool { - static final ClassLoader LOADER = verifyNotNull(StaticClassPool.class.getClassLoader()); - static final ClassPool POOL; - - static { - final ClassPool pool = new ClassPool(); - pool.appendClassPath(new LoaderClassPath(LOADER)); - POOL = pool; - } - - private StaticClassPool() { - // Utility class - } - - /** - * Instantiate a new CodecClassLoader. - * - * @return A new CodecClassLoader. - */ - public static @NonNull CodecClassLoader createLoader() { - return AccessController.doPrivileged((PrivilegedAction)() -> new RootCodecClassLoader()); - } - - /** - * Resolve a binding-dom-codec class to its {@link CtClass} counterpart. - * - * @param clazz Class to resolve - * @return A CtClass instance - * @throws IllegalStateException if the class cannot be resolved - * @throws NullPointerException if {@code clazz} is null - */ - public static synchronized @NonNull CtClass findClass(final @NonNull Class clazz) { - final CtClass ret; - try { - ret = POOL.get(clazz.getName()); - } catch (NotFoundException e) { - throw new IllegalStateException("Failed to find " + clazz, e); - } - ret.freeze(); - return ret; - } - - // Sanity check: target has to resolve yang-binding contents to the same class, otherwise we are in a pickle - static void verifyStaticLinkage(final ClassLoader candidate) { - final Class targetClazz; - try { - targetClazz = candidate.loadClass(DataContainer.class.getName()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("ClassLoader " + candidate + " cannot load " + DataContainer.class, e); - } - verify(DataContainer.class.equals(targetClazz), - "Class mismatch on DataContainer. Ours is from %s, target %s has %s from %s", - DataContainer.class.getClassLoader(), candidate, targetClazz, targetClazz.getClassLoader()); - } -} diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/package-info.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/package-info.java index 4b0a42d36c..3747006bbc 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/package-info.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/loader/package-info.java @@ -6,16 +6,10 @@ * and is available at http://www.eclipse.org/legal/epl-v10.html */ /** - * {@link java.lang.ClassLoader} support for Binding/DOM codec translation code generators. This package provides two - * core classes: - *

    - *
  • {@link org.opendaylight.mdsal.binding.dom.codec.loader.StaticClassPool}, which is allows lookup of classes within - * Binding/DOM codec for the purpose of referencing them within code generators.
  • - *
  • {@link org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader}, which allows lookup of - * compile-time-generated Binding classes for the purpose of referencing them within code generators and which - * serves as the ClassLoader holding runtime-generated codecs. - *
  • - *
+ * {@link java.lang.ClassLoader} support for Binding/DOM codec translation code generators. This package provides one + * core class, {@link org.opendaylight.mdsal.binding.dom.codec.loader.CodecClassLoader}, which allows lookup of + * compile-time-generated Binding classes for the purpose of referencing them within code generators and which + * serves as the ClassLoader holding runtime-generated codecs. * *

* While the interfaces and classes in this package may be publicly accessible, they are an implementation detail and -- 2.36.6