blob: f5cef382a59a536d2e77880fb6a68f8ad923684b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.drill.exec.compile;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import org.apache.drill.exec.compile.ClassTransformer.ClassSet;
import org.apache.drill.exec.compile.bytecode.ValueHolderReplacementVisitor;
import org.apache.drill.exec.compile.sig.SignatureHolder;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.Remapper;
import org.objectweb.asm.commons.RemappingClassAdapter;
import org.objectweb.asm.commons.RemappingMethodAdapter;
import org.objectweb.asm.commons.SimpleRemapper;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.apache.drill.shaded.guava.com.google.common.collect.Sets;
/**
* Serves two purposes. Renames all inner classes references to the outer class to the new name. Also adds all the
* methods and fields of the class to merge to the class that is being visited.
*/
@SuppressWarnings("unused")
class MergeAdapter extends ClassVisitor {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MergeAdapter.class);
private final ClassNode classToMerge;
private final ClassSet set;
private final Set<String> mergingNames = Sets.newHashSet();
private final boolean hasInit;
private String name;
// when more mature, consider AssertionUtil.IsAssertionsEnabled()
private static final boolean verifyBytecode = true;
private MergeAdapter(ClassSet set, ClassVisitor cv, ClassNode cn) {
super(CompilationConfig.ASM_API_VERSION, cv);
this.classToMerge = cn;
this.set = set;
boolean hasInit = false;
for (Object o : classToMerge.methods) {
String name = ((MethodNode)o).name;
if (name.equals("<init>")) {
continue;
}
if (name.equals(SignatureHolder.DRILL_INIT_METHOD)) {
hasInit = true;
}
mergingNames.add(name);
}
this.hasInit = hasInit;
}
@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
// logger.debug(String.format("[Inner Class] Name: %s, outerName: %s, innerName: %s, templateName: %s, newName: %s.",
// name, outerName, innerName, templateName, newName));
if (name.startsWith(set.precompiled.slash)) {
// outerName = outerName.replace(precompiled.slash, generated.slash);
name = name.replace(set.precompiled.slash, set.generated.slash);
int i = name.lastIndexOf('$');
outerName = name.substring(0, i);
super.visitInnerClass(name, outerName, innerName, access);
} else {
super.visitInnerClass(name, outerName, innerName, access);
}
}
// visit the class
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
// use the access and names of the impl class.
this.name = name;
if (name.contains("$")) {
super.visit(version, access, name, signature, superName, interfaces);
} else {
super.visit(version, access ^ Modifier.ABSTRACT | Modifier.FINAL, name, signature, superName, interfaces);
}
// this.cname = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// finalize all methods.
// skip all abstract methods as they should have implementations.
if ((access & Modifier.ABSTRACT) != 0 || mergingNames.contains(name)) {
// logger.debug("Skipping copy of '{}()' since it is abstract or listed elsewhere.", arg1);
return null;
}
if (signature != null) {
signature = signature.replace(set.precompiled.slash, set.generated.slash);
}
// if ((access & Modifier.PUBLIC) == 0) {
// access = access ^ Modifier.PUBLIC ^ Modifier.PROTECTED | Modifier.PRIVATE;
// }
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (!name.equals("<init>")) {
access = access | Modifier.FINAL;
} else {
if (hasInit) {
return new DrillInitMethodVisitor(this.name, mv);
}
}
return mv;
}
@Override
public void visitEnd() {
// add all the fields of the class we're going to merge.
for (Iterator<?> it = classToMerge.fields.iterator(); it.hasNext();) {
// Special handling for nested classes. Drill uses non-static nested
// "inner" classes in some templates. Prior versions of Drill would
// create the generated nested classes as static, then this line
// would copy the "this$0" field to convert the static nested class
// into a non-static inner class. However, that approach is not
// compatible with plain-old Java compilation. Now, Drill generates
// the nested classes as non-static inner classes. As a result, we
// do not want to copy the hidden fields; we'll end up with two if
// we do.
FieldNode field = (FieldNode) it.next();
if (! field.name.startsWith("this$")) {
field.accept(this);
}
}
// add all the methods that we to include.
for (Iterator<?> it = classToMerge.methods.iterator(); it.hasNext();) {
MethodNode mn = (MethodNode) it.next();
if (mn.name.equals("<init>")) {
continue;
}
String[] exceptions = new String[mn.exceptions.size()];
mn.exceptions.toArray(exceptions);
MethodVisitor mv = cv.visitMethod(mn.access | Modifier.FINAL, mn.name, mn.desc, mn.signature, exceptions);
if (verifyBytecode) {
mv = new CheckMethodVisitorFsm(api, mv);
}
mn.instructions.resetLabels();
// mn.accept(new RemappingMethodAdapter(mn.access, mn.desc, mv, new
// SimpleRemapper("org.apache.drill.exec.compile.ExampleTemplate", "Bunky")));
ClassSet top = set;
while (top.parent != null) {
top = top.parent;
}
mn.accept(new RemappingMethodAdapter(mn.access, mn.desc, mv,
new SimpleRemapper(top.precompiled.slash, top.generated.slash)));
}
super.visitEnd();
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
return super.visitField(access, name, desc, signature, value);
}
public static class MergedClassResult {
public final byte[] bytes;
public final Collection<String> innerClasses;
public MergedClassResult(byte[] bytes, Collection<String> innerClasses) {
this.bytes = bytes;
this.innerClasses = innerClasses;
}
}
public static MergedClassResult getMergedClass(final ClassSet set, final byte[] precompiledClass,
ClassNode generatedClass, final boolean scalarReplace) {
if (verifyBytecode) {
if (!AsmUtil.isClassBytesOk(logger, "precompiledClass", precompiledClass)) {
throw new IllegalStateException("Problem found in precompiledClass");
}
if ((generatedClass != null) && !AsmUtil.isClassOk(logger, "generatedClass", generatedClass)) {
throw new IllegalStateException("Problem found in generatedClass");
}
}
/*
* Setup adapters for merging, remapping class names and class writing. This is done in
* reverse order of how they will be evaluated.
*/
final RemapClasses re = new RemapClasses(set);
try {
if (scalarReplace && generatedClass != null) {
if (logger.isDebugEnabled()) {
AsmUtil.logClass(logger, "generated " + set.generated.dot, generatedClass);
}
final ClassNode generatedMerged = new ClassNode();
ClassVisitor mergeGenerator = generatedMerged;
if (verifyBytecode) {
mergeGenerator = new DrillCheckClassAdapter(CompilationConfig.ASM_API_VERSION,
new CheckClassVisitorFsm(CompilationConfig.ASM_API_VERSION, generatedMerged), true);
}
/*
* Even though we're effectively transforming-creating a new class in mergeGenerator,
* there's no way to pass in ClassWriter.COMPUTE_MAXS, which would save us from having
* to figure out stack size increases on our own. That gets handled by the
* InstructionModifier (from inside ValueHolderReplacement > ScalarReplacementNode).
*/
generatedClass.accept(new ValueHolderReplacementVisitor(mergeGenerator, verifyBytecode));
if (verifyBytecode) {
if (!AsmUtil.isClassOk(logger, "generatedMerged", generatedMerged)) {
throw new IllegalStateException("Problem found with generatedMerged");
}
}
generatedClass = generatedMerged;
}
final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor writerVisitor = writer;
if (verifyBytecode) {
writerVisitor = new DrillCheckClassAdapter(CompilationConfig.ASM_API_VERSION,
new CheckClassVisitorFsm(CompilationConfig.ASM_API_VERSION, writerVisitor), true);
}
ClassVisitor remappingAdapter = new RemappingClassAdapter(writerVisitor, re);
if (verifyBytecode) {
remappingAdapter = new DrillCheckClassAdapter(CompilationConfig.ASM_API_VERSION,
new CheckClassVisitorFsm(CompilationConfig.ASM_API_VERSION, remappingAdapter), true);
}
ClassVisitor visitor = remappingAdapter;
if (generatedClass != null) {
visitor = new MergeAdapter(set, remappingAdapter, generatedClass);
}
ClassReader tReader = new ClassReader(precompiledClass);
tReader.accept(visitor, ClassReader.SKIP_FRAMES);
byte[] outputClass = writer.toByteArray();
if (logger.isDebugEnabled()) {
AsmUtil.logClassFromBytes(logger, "merged " + set.generated.dot, outputClass);
}
// enable when you want all the generated merged class files to also be written to disk.
// try {
// File destDir = new File( "/tmp/scratch/drill-generated-classes" );
// destDir.mkdirs();
// Files.write(outputClass, new File(destDir, String.format("%s-output.class", set.generated.dot)));
// } catch (IOException e) {
// // Ignore;
// }
return new MergedClassResult(outputClass, re.getInnerClasses());
} catch (Error | RuntimeException e) {
logger.error("Failure while merging classes.", e);
AsmUtil.logClass(logger, "generatedClass", generatedClass);
throw e;
}
}
private static class RemapClasses extends Remapper {
final Set<String> innerClasses = Sets.newHashSet();
ClassSet top;
ClassSet current;
public RemapClasses(final ClassSet set) {
current = set;
ClassSet top = set;
while (top.parent != null) {
top = top.parent;
}
this.top = top;
}
@Override
public String map(final String typeName) {
// remap the names of all classes that start with the old class name.
if (typeName.startsWith(top.precompiled.slash)) {
// write down all the sub classes.
if (typeName.startsWith(current.precompiled.slash + "$")) {
innerClasses.add(typeName);
}
return typeName.replace(top.precompiled.slash, top.generated.slash);
}
return typeName;
}
public Set<String> getInnerClasses() {
return innerClasses;
}
}
}