/* * 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.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Map; import org.apache.drill.common.config.DrillConfig; import org.apache.drill.common.util.DrillStringUtils; import org.apache.drill.exec.compile.ClassTransformer.ClassNames; import org.apache.drill.exec.exception.ClassTransformationException; import org.apache.drill.exec.expr.CodeGenerator; import org.apache.drill.exec.server.options.OptionSet; import org.codehaus.commons.compiler.CompileException; /** * Implements the "plain Java" method of code generation and * compilation. Given a {@link CodeGenerator}, obtains the generated * source code, compiles it with the selected compiler, loads the * byte-codes into a class loader and provides the resulting * class. Compared with the {@link ClassTransformer} mechanism, * this one requires the code generator to have generated a complete * Java class that is capable of direct compilation and loading. * This means the generated class must be a subclass of the template * so that the JVM can use normal Java inheritance to associate the * template and generated methods. * <p> * Here is how to use the plain Java technique to debug * generated code: * <ul> * <li>Set the config option <tt>drill.exec.compile.code_dir</tt> * to the location where you want to save the generated source * code.</li> * <li>Where you generate code (using a {@link CodeGenerator}), * set the "plain Java" options:<pre> * CodeGenerator<Foo> cg = ... * cg.plainJavaCapable(true); // Class supports plain Java * cg.preferPlainJava(true); // Actually generate plain Java * cg.saveCodeForDebugging(true); // Save code for debugging * ...</pre> * Note that <tt>saveCodeForDebugging</tt> automatically sets the PJ * option if the generator is capable. Call <tt>preferPlainJava</tt> * only if you want to try PJ for this particular generated class * without saving the generated code.</li> * <li>In your favorite IDE, add to the code lookup path the * code directory saved earlier. In Eclipse, for example, you do * this in the debug configuration you will use to debug Drill.</li> * <li>Set a breakpoint in template used for the generated code.</li> * <li>Run Drill. The IDE will stop at your breakpoint.</li> * <li>Step into the generated code. Examine class field and * local variables. Have fun!</li> * </ul> * <p> * Most generated classes have been upgraded to support Plain Java * compilation. Once this work is complete, the calls to * <tt>plainJavaCapable<tt> can be removed as all generated classes * will be capable. * <p> * The setting to prefer plain Java is ignored for any remaining generated * classes not marked as plain Java capable. */ public class ClassBuilder { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ClassBuilder.class); public static final String CODE_DIR_OPTION = CodeCompiler.COMPILE_BASE + ".code_dir"; private final DrillConfig config; private final OptionSet options; private final File codeDir; public ClassBuilder(DrillConfig config, OptionSet optionManager) { this.config = config; options = optionManager; // Code can be saved per-class to enable debugging. // Just request the code generator to persist code, // point your debugger to the directory set below, and you // can step into the code for debugging. Code is not saved // be default because doing so is expensive and unnecessary. codeDir = new File(config.getString(CODE_DIR_OPTION)); } /** * Given a code generator which has already generated plain Java * code, compile the code, create a class loader, and return the * resulting Java class. * * @param cg a plain Java capable code generator that has generated * plain Java code * @return the class that the code generator defines * @throws ClassTransformationException */ public Class<?> getImplementationClass(CodeGenerator<?> cg) throws ClassTransformationException { try { return compileClass(cg); } catch (CompileException | ClassNotFoundException|IOException e) { throw new ClassTransformationException(e); } } /** * Performs the actual work of compiling the code and loading the class. * * @param cg the code generator that has built the class(es) to be generated. * @return the class, after code generation and (if needed) compilation. * @throws IOException if an error occurs when optionally writing code to disk. * @throws CompileException if the generated code has compile issues. * @throws ClassNotFoundException if the generated code references unknown classes. * @throws ClassTransformationException generic "something is wrong" error from * Drill class compilation code. */ @SuppressWarnings("resource") private Class<?> compileClass(CodeGenerator<?> cg) throws IOException, CompileException, ClassNotFoundException, ClassTransformationException { final long t1 = System.nanoTime(); // Get the plain Java code. String code = cg.getGeneratedCode(); // Get the class names (dotted, file path, etc.) String className = cg.getMaterializedClassName(); ClassTransformer.ClassNames name = new ClassTransformer.ClassNames(className); // A key advantage of this method is that the code can be // saved and debugged, if needed. if (cg.isCodeToBeSaved()) { saveCode(code, name); } // Compile the code and load it into a class loader. CachedClassLoader classLoader = new CachedClassLoader(); ClassCompilerSelector compilerSelector = new ClassCompilerSelector(classLoader, config, options); Map<String,byte[]> results = compilerSelector.compile(name, code); classLoader.addClasses(results); long totalBytecodeSize = 0; for (byte[] clazz : results.values()) { totalBytecodeSize += clazz.length; } logger.debug("Compiled {}: bytecode size = {}, time = {} ms.", cg.getClassName(), DrillStringUtils.readable(totalBytecodeSize), (System.nanoTime() - t1 + 500_000) / 1_000_000); // Get the class from the class loader. try { return classLoader.findClass(className); } catch (ClassNotFoundException e) { // This should never occur. throw new IllegalStateException("Code load failed", e); } } /** * Save code to a predefined location for debugging. To use the code * for debugging, make sure the save location is on your IDE's source * code search path. Code is saved in usual Java format with each * package as a directory. The provided code directory becomes a * source directory, as in Maven's "src/main/java". * * @param code the source code * @param name the class name */ private void saveCode(String code, ClassNames name) { String pathName = name.slash + ".java"; File codeFile = new File(codeDir, pathName); codeFile.getParentFile().mkdirs(); try (final FileWriter writer = new FileWriter(codeFile)) { writer.write(code); } catch (IOException e) { System.err.println("Could not save: " + codeFile.getAbsolutePath()); } } }