1 /*
   2  * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.tools.jlink.internal.plugins;
  26 
  27 import java.io.BufferedReader;
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.InputStreamReader;
  32 import java.lang.invoke.MethodType;
  33 import java.lang.module.ModuleDescriptor;
  34 import java.nio.file.Files;
  35 import java.util.EnumSet;
  36 import java.util.Map;
  37 import java.util.Optional;
  38 import java.util.Set;
  39 import java.util.TreeMap;
  40 import java.util.TreeSet;
  41 import java.util.stream.Collectors;
  42 import java.util.stream.Stream;
  43 import jdk.internal.misc.SharedSecrets;
  44 import jdk.internal.misc.JavaLangInvokeAccess;
  45 import jdk.tools.jlink.plugin.ResourcePoolEntry;
  46 import jdk.tools.jlink.plugin.PluginException;
  47 import jdk.tools.jlink.plugin.ResourcePool;
  48 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
  49 import jdk.tools.jlink.plugin.Plugin;
  50 
  51 /**
  52  * Plugin to generate java.lang.invoke classes.
  53  */
  54 public final class GenerateJLIClassesPlugin implements Plugin {
  55 
  56     private static final String NAME = "generate-jli-classes";
  57 
  58     private static final String DESCRIPTION = PluginsResourceBundle.getDescription(NAME);
  59 
  60     private static final String DEFAULT_TRACE_FILE = "default_jli_trace.txt";
  61 
  62     private static final String DIRECT_HOLDER = "java/lang/invoke/DirectMethodHandle$Holder";
  63     private static final String DMH_INVOKE_VIRTUAL = "invokeVirtual";
  64     private static final String DMH_INVOKE_STATIC = "invokeStatic";
  65     private static final String DMH_INVOKE_SPECIAL = "invokeSpecial";
  66     private static final String DMH_NEW_INVOKE_SPECIAL = "newInvokeSpecial";
  67     private static final String DMH_INVOKE_INTERFACE = "invokeInterface";
  68     private static final String DMH_INVOKE_STATIC_INIT = "invokeStaticInit";
  69 
  70     private static final String DELEGATING_HOLDER = "java/lang/invoke/DelegatingMethodHandle$Holder";
  71     private static final String BASIC_FORMS_HOLDER = "java/lang/invoke/LambdaForm$Holder";
  72     private static final String INVOKERS_HOLDER = "java/lang/invoke/Invokers$Holder";
  73 
  74     private static final JavaLangInvokeAccess JLIA
  75             = SharedSecrets.getJavaLangInvokeAccess();
  76 
  77     Set<String> speciesTypes = Set.of();
  78 
  79     Set<String> invokerTypes = Set.of();
  80 
  81     Map<String, Set<String>> dmhMethods = Map.of();
  82 
  83     String mainArgument;
  84 
  85     public GenerateJLIClassesPlugin() {
  86     }
  87 
  88     @Override
  89     public String getName() {
  90         return NAME;
  91     }
  92 
  93     @Override
  94     public String getDescription() {
  95         return DESCRIPTION;
  96     }
  97 
  98     @Override
  99     public Set<State> getState() {
 100         return EnumSet.of(State.AUTO_ENABLED, State.FUNCTIONAL);
 101     }
 102 
 103     @Override
 104     public boolean hasArguments() {
 105         return true;
 106     }
 107 
 108     @Override
 109     public String getArgumentsDescription() {
 110        return PluginsResourceBundle.getArgument(NAME);
 111     }
 112 
 113     /**
 114      * @return the default Species forms to generate.
 115      *
 116      * This list was derived from running a small startup benchmark.
 117      * A better long-term solution is to define and run a set of quick
 118      * generators and extracting this list as a step in the build process.
 119      */
 120     public static Set<String> defaultSpecies() {
 121         return Set.of("LL", "L3", "L4", "L5", "L6", "L7", "L7I",
 122                 "L7II", "L7IIL", "L8", "L9", "L10", "L10I", "L10II", "L10IIL",
 123                 "L11", "L12", "L13", "LI", "D", "L3I", "LIL", "LLI", "LLIL",
 124                 "LILL", "I", "LLILL");
 125     }
 126 
 127     /**
 128      * @return the default invoker forms to generate.
 129      */
 130     private static Set<String> defaultInvokers() {
 131         return Set.of("LL_L", "LL_I", "LILL_I", "L6_L");
 132     }
 133 
 134     /**
 135      * @return the list of default DirectMethodHandle methods to generate.
 136      */
 137     private static Map<String, Set<String>> defaultDMHMethods() {
 138         return Map.of(
 139             DMH_INVOKE_VIRTUAL, Set.of("L_L", "LL_L", "LLI_I", "L3_V"),
 140             DMH_INVOKE_SPECIAL, Set.of("LL_I", "LL_L", "LLF_L", "LLD_L", "L3_L",
 141                 "L4_L", "L5_L", "L6_L", "L7_L", "L8_L", "LLI_I", "LLI_L",
 142                 "LLIL_I", "LLII_I", "LLII_L", "L3I_L", "L3I_I", "LLILI_I",
 143                 "LLIIL_L", "LLIILL_L", "LLIILL_I", "LLIIL_I", "LLILIL_I",
 144                 "LLILILL_I", "LLILII_I", "LLI3_I", "LLI3L_I", "LLI3LL_I",
 145                 "LLI3_L", "LLI4_I"),
 146             DMH_INVOKE_STATIC, Set.of("LII_I", "LIL_I", "LILIL_I", "LILII_I",
 147                 "L_I", "L_L", "L_V", "LD_L", "LF_L", "LI_I", "LII_L", "LLI_L",
 148                 "LL_V", "LL_L", "L3_L", "L4_L", "L5_L", "L6_L", "L7_L",
 149                 "L8_L", "L9_L", "L10_L", "L10I_L", "L10II_L", "L10IIL_L",
 150                 "L11_L", "L12_L", "L13_L", "L14_L", "L14I_L", "L14II_L")
 151         );
 152     }
 153 
 154     // Map from DirectMethodHandle method type to internal ID
 155     private static final Map<String, Integer> DMH_METHOD_TYPE_MAP =
 156             Map.of(
 157                 DMH_INVOKE_VIRTUAL,     0,
 158                 DMH_INVOKE_STATIC,      1,
 159                 DMH_INVOKE_SPECIAL,     2,
 160                 DMH_NEW_INVOKE_SPECIAL, 3,
 161                 DMH_INVOKE_INTERFACE,   4,
 162                 DMH_INVOKE_STATIC_INIT, 5
 163             );
 164 
 165     @Override
 166     public void configure(Map<String, String> config) {
 167         mainArgument = config.get(NAME);
 168     }
 169 
 170     public void initialize(ResourcePool in) {
 171         // Start with the default configuration
 172         speciesTypes = defaultSpecies().stream()
 173                 .map(type -> expandSignature(type))
 174                 .collect(Collectors.toSet());
 175 
 176         invokerTypes = defaultInvokers();
 177         validateMethodTypes(invokerTypes);
 178 
 179         dmhMethods = defaultDMHMethods();
 180         for (Set<String> dmhMethodTypes : dmhMethods.values()) {
 181             validateMethodTypes(dmhMethodTypes);
 182         }
 183 
 184         // Extend the default configuration with the contents in the supplied
 185         // input file - if none was supplied we look for the default file
 186         if (mainArgument == null || !mainArgument.startsWith("@")) {
 187             try (InputStream traceFile =
 188                     this.getClass().getResourceAsStream(DEFAULT_TRACE_FILE)) {
 189                 if (traceFile != null) {
 190                     readTraceConfig(
 191                         new BufferedReader(
 192                             new InputStreamReader(traceFile)).lines());
 193                 }
 194             } catch (Exception e) {
 195                 throw new PluginException("Couldn't read " + DEFAULT_TRACE_FILE, e);
 196             }
 197         } else {
 198             File file = new File(mainArgument.substring(1));
 199             if (file.exists()) {
 200                 readTraceConfig(fileLines(file));
 201             }
 202         }
 203     }
 204 
 205     private void readTraceConfig(Stream<String> lines) {
 206         // Use TreeSet/TreeMap to keep things sorted in a deterministic
 207         // order to avoid scrambling the layout on small changes and to
 208         // ease finding methods in the generated code
 209         speciesTypes = new TreeSet<>(speciesTypes);
 210         invokerTypes = new TreeSet<>(invokerTypes);
 211         TreeMap<String, Set<String>> newDMHMethods = new TreeMap<>();
 212         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 213             newDMHMethods.put(entry.getKey(), new TreeSet<>(entry.getValue()));
 214         }
 215         dmhMethods = newDMHMethods;
 216         lines.map(line -> line.split(" "))
 217              .forEach(parts -> {
 218                 switch (parts[0]) {
 219                     case "[SPECIES_RESOLVE]":
 220                         // Allow for new types of species data classes being resolved here
 221                         if (parts.length == 3 && parts[1].startsWith("java.lang.invoke.BoundMethodHandle$Species_")) {
 222                             String species = parts[1].substring("java.lang.invoke.BoundMethodHandle$Species_".length());
 223                             if (!"L".equals(species)) {
 224                                 speciesTypes.add(expandSignature(species));
 225                             }
 226                         }
 227                         break;
 228                     case "[LF_RESOLVE]":
 229                         String methodType = parts[3];
 230                         validateMethodType(methodType);
 231                         if (parts[1].contains("Invokers")) {
 232                             invokerTypes.add(methodType);
 233                         } else if (parts[1].contains("DirectMethodHandle")) {
 234                             String dmh = parts[2];
 235                             // ignore getObject etc for now (generated
 236                             // by default)
 237                             if (DMH_METHOD_TYPE_MAP.containsKey(dmh)) {
 238                                 addDMHMethodType(dmh, methodType);
 239                             }
 240                         }
 241                         break;
 242                     default: break; // ignore
 243                 }
 244             });
 245     }
 246 
 247     private void addDMHMethodType(String dmh, String methodType) {
 248         validateMethodType(methodType);
 249         Set<String> methodTypes = dmhMethods.get(dmh);
 250         if (methodTypes == null) {
 251             methodTypes = new TreeSet<>();
 252             dmhMethods.put(dmh, methodTypes);
 253         }
 254         methodTypes.add(methodType);
 255     }
 256 
 257     private Stream<String> fileLines(File file) {
 258         try {
 259             return Files.lines(file.toPath());
 260         } catch (IOException io) {
 261             throw new PluginException("Couldn't read file");
 262         }
 263     }
 264 
 265     private void validateMethodTypes(Set<String> dmhMethodTypes) {
 266         for (String type : dmhMethodTypes) {
 267             validateMethodType(type);
 268         }
 269     }
 270 
 271     private void validateMethodType(String type) {
 272         String[] typeParts = type.split("_");
 273         // check return type (second part)
 274         if (typeParts.length != 2 || typeParts[1].length() != 1
 275                 || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) {
 276             throw new PluginException(
 277                     "Method type signature must be of form [LJIFD]*_[LJIFDV]");
 278         }
 279         // expand and check arguments (first part)
 280         expandSignature(typeParts[0]);
 281     }
 282 
 283     private static void requireBasicType(char c) {
 284         if ("LIJFD".indexOf(c) < 0) {
 285             throw new PluginException(
 286                     "Character " + c + " must correspond to a basic field type: LIJFD");
 287         }
 288     }
 289 
 290     @Override
 291     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
 292         initialize(in);
 293         // Copy all but DMH_ENTRY to out
 294         in.transformAndCopy(entry -> {
 295                 // filter out placeholder entries
 296                 if (entry.path().equals(DIRECT_METHOD_HOLDER_ENTRY) ||
 297                     entry.path().equals(DELEGATING_METHOD_HOLDER_ENTRY) ||
 298                     entry.path().equals(INVOKERS_HOLDER_ENTRY) ||
 299                     entry.path().equals(BASIC_FORMS_HOLDER_ENTRY)) {
 300                     return null;
 301                 } else {
 302                     return entry;
 303                 }
 304             }, out);
 305 
 306         // Generate BMH Species classes
 307         speciesTypes.forEach(types -> generateBMHClass(types, out));
 308 
 309         // Generate LambdaForm Holder classes
 310         generateHolderClasses(out);
 311 
 312         // Let it go
 313         speciesTypes = null;
 314         invokerTypes = null;
 315         dmhMethods = null;
 316 
 317         return out.build();
 318     }
 319 
 320     @SuppressWarnings("unchecked")
 321     private void generateBMHClass(String types, ResourcePoolBuilder out) {
 322         try {
 323             // Generate class
 324             Map.Entry<String, byte[]> result =
 325                     JLIA.generateConcreteBMHClassBytes(types);
 326             String className = result.getKey();
 327             byte[] bytes = result.getValue();
 328 
 329             // Add class to pool
 330             ResourcePoolEntry ndata = ResourcePoolEntry.create(
 331                     "/java.base/" + className + ".class",
 332                     bytes);
 333             out.add(ndata);
 334         } catch (Exception ex) {
 335             throw new PluginException(ex);
 336         }
 337     }
 338 
 339     private void generateHolderClasses(ResourcePoolBuilder out) {
 340         int count = 0;
 341         for (Set<String> entry : dmhMethods.values()) {
 342             count += entry.size();
 343         }
 344         MethodType[] directMethodTypes = new MethodType[count];
 345         int[] dmhTypes = new int[count];
 346         int index = 0;
 347         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 348             String dmhType = entry.getKey();
 349             for (String type : entry.getValue()) {
 350                 // The DMH type to actually ask for is retrieved by removing
 351                 // the first argument, which needs to be of Object.class
 352                 MethodType mt = asMethodType(type);
 353                 if (mt.parameterCount() < 1 ||
 354                     mt.parameterType(0) != Object.class) {
 355                     throw new PluginException(
 356                             "DMH type parameter must start with L");
 357                 }
 358                 directMethodTypes[index] = mt.dropParameterTypes(0, 1);
 359                 dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType);
 360                 index++;
 361             }
 362         }
 363         MethodType[] invokerMethodTypes = new MethodType[this.invokerTypes.size()];
 364         int i = 0;
 365         for (String invokerType : invokerTypes) {
 366             // The invoker type to ask for is retrieved by removing the first
 367             // and the last argument, which needs to be of Object.class
 368             MethodType mt = asMethodType(invokerType);
 369             final int lastParam = mt.parameterCount() - 1;
 370             if (mt.parameterCount() < 2 ||
 371                     mt.parameterType(0) != Object.class ||
 372                     mt.parameterType(lastParam) != Object.class) {
 373                 throw new PluginException(
 374                         "Invoker type parameter must start and end with L");
 375             }
 376             mt = mt.dropParameterTypes(lastParam, lastParam + 1);
 377             invokerMethodTypes[i] = mt.dropParameterTypes(0, 1);
 378             i++;
 379         }
 380         try {
 381             byte[] bytes = JLIA.generateDirectMethodHandleHolderClassBytes(
 382                     DIRECT_HOLDER, directMethodTypes, dmhTypes);
 383             ResourcePoolEntry ndata = ResourcePoolEntry
 384                     .create(DIRECT_METHOD_HOLDER_ENTRY, bytes);
 385             out.add(ndata);
 386 
 387             bytes = JLIA.generateDelegatingMethodHandleHolderClassBytes(
 388                     DELEGATING_HOLDER, directMethodTypes);
 389             ndata = ResourcePoolEntry.create(DELEGATING_METHOD_HOLDER_ENTRY, bytes);
 390             out.add(ndata);
 391 
 392             bytes = JLIA.generateInvokersHolderClassBytes(INVOKERS_HOLDER,
 393                     invokerMethodTypes);
 394             ndata = ResourcePoolEntry.create(INVOKERS_HOLDER_ENTRY, bytes);
 395             out.add(ndata);
 396 
 397             bytes = JLIA.generateBasicFormsClassBytes(BASIC_FORMS_HOLDER);
 398             ndata = ResourcePoolEntry.create(BASIC_FORMS_HOLDER_ENTRY, bytes);
 399             out.add(ndata);
 400         } catch (Exception ex) {
 401             throw new PluginException(ex);
 402         }
 403     }
 404     private static final String DIRECT_METHOD_HOLDER_ENTRY =
 405             "/java.base/" + DIRECT_HOLDER + ".class";
 406     private static final String DELEGATING_METHOD_HOLDER_ENTRY =
 407             "/java.base/" + DELEGATING_HOLDER + ".class";
 408     private static final String BASIC_FORMS_HOLDER_ENTRY =
 409             "/java.base/" + BASIC_FORMS_HOLDER + ".class";
 410     private static final String INVOKERS_HOLDER_ENTRY =
 411             "/java.base/" + INVOKERS_HOLDER + ".class";
 412 
 413     // Convert LL -> LL, L3 -> LLL
 414     public static String expandSignature(String signature) {
 415         StringBuilder sb = new StringBuilder();
 416         char last = 'X';
 417         int count = 0;
 418         for (int i = 0; i < signature.length(); i++) {
 419             char c = signature.charAt(i);
 420             if (c >= '0' && c <= '9') {
 421                 count *= 10;
 422                 count += (c - '0');
 423             } else {
 424                 requireBasicType(c);
 425                 for (int j = 1; j < count; j++) {
 426                     sb.append(last);
 427                 }
 428                 sb.append(c);
 429                 last = c;
 430                 count = 0;
 431             }
 432         }
 433 
 434         // ended with a number, e.g., "L2": append last char count - 1 times
 435         if (count > 1) {
 436             requireBasicType(last);
 437             for (int j = 1; j < count; j++) {
 438                 sb.append(last);
 439             }
 440         }
 441         return sb.toString();
 442     }
 443 
 444     private static MethodType asMethodType(String basicSignatureString) {
 445         String[] parts = basicSignatureString.split("_");
 446         assert(parts.length == 2);
 447         assert(parts[1].length() == 1);
 448         String parameters = expandSignature(parts[0]);
 449         Class<?> rtype = simpleType(parts[1].charAt(0));
 450         if (parameters.isEmpty()) {
 451             return MethodType.methodType(rtype);
 452         } else {
 453             Class<?>[] ptypes = new Class<?>[parameters.length()];
 454             for (int i = 0; i < ptypes.length; i++) {
 455                 ptypes[i] = simpleType(parameters.charAt(i));
 456             }
 457             return MethodType.methodType(rtype, ptypes);
 458         }
 459     }
 460 
 461     private static Class<?> simpleType(char c) {
 462         switch (c) {
 463             case 'F':
 464                 return float.class;
 465             case 'D':
 466                 return double.class;
 467             case 'I':
 468                 return int.class;
 469             case 'L':
 470                 return Object.class;
 471             case 'J':
 472                 return long.class;
 473             case 'V':
 474                 return void.class;
 475             case 'Z':
 476             case 'B':
 477             case 'S':
 478             case 'C':
 479                 throw new IllegalArgumentException("Not a valid primitive: " + c +
 480                         " (use I instead)");
 481             default:
 482                 throw new IllegalArgumentException("Not a primitive: " + c);
 483         }
 484     }
 485 }