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             DMH_NEW_INVOKE_SPECIAL, Set.of("L_L", "LL_L")
 152         );
 153     }
 154 
 155     // Map from DirectMethodHandle method type to internal ID
 156     private static final Map<String, Integer> DMH_METHOD_TYPE_MAP =
 157             Map.of(
 158                 DMH_INVOKE_VIRTUAL,     0,
 159                 DMH_INVOKE_STATIC,      1,
 160                 DMH_INVOKE_SPECIAL,     2,
 161                 DMH_NEW_INVOKE_SPECIAL, 3,
 162                 DMH_INVOKE_INTERFACE,   4,
 163                 DMH_INVOKE_STATIC_INIT, 5
 164             );
 165 
 166     @Override
 167     public void configure(Map<String, String> config) {
 168         mainArgument = config.get(NAME);
 169     }
 170 
 171     public void initialize(ResourcePool in) {
 172         // Start with the default configuration
 173         speciesTypes = defaultSpecies().stream()
 174                 .map(type -> expandSignature(type))
 175                 .collect(Collectors.toSet());
 176 
 177         invokerTypes = defaultInvokers();
 178         validateMethodTypes(invokerTypes);
 179 
 180         dmhMethods = defaultDMHMethods();
 181         for (Set<String> dmhMethodTypes : dmhMethods.values()) {
 182             validateMethodTypes(dmhMethodTypes);
 183         }
 184 
 185         // Extend the default configuration with the contents in the supplied
 186         // input file - if none was supplied we look for the default file
 187         if (mainArgument == null || !mainArgument.startsWith("@")) {
 188             try (InputStream traceFile =
 189                     this.getClass().getResourceAsStream(DEFAULT_TRACE_FILE)) {
 190                 if (traceFile != null) {
 191                     readTraceConfig(
 192                         new BufferedReader(
 193                             new InputStreamReader(traceFile)).lines());
 194                 }
 195             } catch (Exception e) {
 196                 throw new PluginException("Couldn't read " + DEFAULT_TRACE_FILE, e);
 197             }
 198         } else {
 199             File file = new File(mainArgument.substring(1));
 200             if (file.exists()) {
 201                 readTraceConfig(fileLines(file));
 202             }
 203         }
 204     }
 205 
 206     private void readTraceConfig(Stream<String> lines) {
 207         // Use TreeSet/TreeMap to keep things sorted in a deterministic
 208         // order to avoid scrambling the layout on small changes and to
 209         // ease finding methods in the generated code
 210         speciesTypes = new TreeSet<>(speciesTypes);
 211         invokerTypes = new TreeSet<>(invokerTypes);
 212         TreeMap<String, Set<String>> newDMHMethods = new TreeMap<>();
 213         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 214             newDMHMethods.put(entry.getKey(), new TreeSet<>(entry.getValue()));
 215         }
 216         dmhMethods = newDMHMethods;
 217         lines.map(line -> line.split(" "))
 218              .forEach(parts -> {
 219                 switch (parts[0]) {
 220                     case "[SPECIES_RESOLVE]":
 221                         // Allow for new types of species data classes being resolved here
 222                         if (parts.length == 3 && parts[1].startsWith("java.lang.invoke.BoundMethodHandle$Species_")) {
 223                             String species = parts[1].substring("java.lang.invoke.BoundMethodHandle$Species_".length());
 224                             if (!"L".equals(species)) {
 225                                 speciesTypes.add(expandSignature(species));
 226                             }
 227                         }
 228                         break;
 229                     case "[LF_RESOLVE]":
 230                         String methodType = parts[3];
 231                         validateMethodType(methodType);
 232                         if (parts[1].contains("Invokers")) {
 233                             invokerTypes.add(methodType);
 234                         } else if (parts[1].contains("DirectMethodHandle")) {
 235                             String dmh = parts[2];
 236                             // ignore getObject etc for now (generated
 237                             // by default)
 238                             if (DMH_METHOD_TYPE_MAP.containsKey(dmh)) {
 239                                 addDMHMethodType(dmh, methodType);
 240                             }
 241                         }
 242                         break;
 243                     default: break; // ignore
 244                 }
 245             });
 246     }
 247 
 248     private void addDMHMethodType(String dmh, String methodType) {
 249         validateMethodType(methodType);
 250         Set<String> methodTypes = dmhMethods.get(dmh);
 251         if (methodTypes == null) {
 252             methodTypes = new TreeSet<>();
 253             dmhMethods.put(dmh, methodTypes);
 254         }
 255         methodTypes.add(methodType);
 256     }
 257 
 258     private Stream<String> fileLines(File file) {
 259         try {
 260             return Files.lines(file.toPath());
 261         } catch (IOException io) {
 262             throw new PluginException("Couldn't read file");
 263         }
 264     }
 265 
 266     private void validateMethodTypes(Set<String> dmhMethodTypes) {
 267         for (String type : dmhMethodTypes) {
 268             validateMethodType(type);
 269         }
 270     }
 271 
 272     private void validateMethodType(String type) {
 273         String[] typeParts = type.split("_");
 274         // check return type (second part)
 275         if (typeParts.length != 2 || typeParts[1].length() != 1
 276                 || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) {
 277             throw new PluginException(
 278                     "Method type signature must be of form [LJIFD]*_[LJIFDV]");
 279         }
 280         // expand and check arguments (first part)
 281         expandSignature(typeParts[0]);
 282     }
 283 
 284     private static void requireBasicType(char c) {
 285         if ("LIJFD".indexOf(c) < 0) {
 286             throw new PluginException(
 287                     "Character " + c + " must correspond to a basic field type: LIJFD");
 288         }
 289     }
 290 
 291     @Override
 292     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
 293         initialize(in);
 294         // Copy all but DMH_ENTRY to out
 295         in.transformAndCopy(entry -> {
 296                 // filter out placeholder entries
 297                 if (entry.path().equals(DIRECT_METHOD_HOLDER_ENTRY) ||
 298                     entry.path().equals(DELEGATING_METHOD_HOLDER_ENTRY) ||
 299                     entry.path().equals(INVOKERS_HOLDER_ENTRY) ||
 300                     entry.path().equals(BASIC_FORMS_HOLDER_ENTRY)) {
 301                     return null;
 302                 } else {
 303                     return entry;
 304                 }
 305             }, out);
 306 
 307         // Generate BMH Species classes
 308         speciesTypes.forEach(types -> generateBMHClass(types, out));
 309 
 310         // Generate LambdaForm Holder classes
 311         generateHolderClasses(out);
 312 
 313         // Let it go
 314         speciesTypes = null;
 315         invokerTypes = null;
 316         dmhMethods = null;
 317 
 318         return out.build();
 319     }
 320 
 321     @SuppressWarnings("unchecked")
 322     private void generateBMHClass(String types, ResourcePoolBuilder out) {
 323         try {
 324             // Generate class
 325             Map.Entry<String, byte[]> result =
 326                     JLIA.generateConcreteBMHClassBytes(types);
 327             String className = result.getKey();
 328             byte[] bytes = result.getValue();
 329 
 330             // Add class to pool
 331             ResourcePoolEntry ndata = ResourcePoolEntry.create(
 332                     "/java.base/" + className + ".class",
 333                     bytes);
 334             out.add(ndata);
 335         } catch (Exception ex) {
 336             throw new PluginException(ex);
 337         }
 338     }
 339 
 340     private void generateHolderClasses(ResourcePoolBuilder out) {
 341         int count = 0;
 342         for (Set<String> entry : dmhMethods.values()) {
 343             count += entry.size();
 344         }
 345         MethodType[] directMethodTypes = new MethodType[count];
 346         int[] dmhTypes = new int[count];
 347         int index = 0;
 348         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 349             String dmhType = entry.getKey();
 350             for (String type : entry.getValue()) {
 351                 // The DMH type to actually ask for is retrieved by removing
 352                 // the first argument, which needs to be of Object.class
 353                 MethodType mt = asMethodType(type);
 354                 if (mt.parameterCount() < 1 ||
 355                     mt.parameterType(0) != Object.class) {
 356                     throw new PluginException(
 357                             "DMH type parameter must start with L");
 358                 }
 359                 directMethodTypes[index] = mt.dropParameterTypes(0, 1);
 360                 dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType);
 361                 index++;
 362             }
 363         }
 364         MethodType[] invokerMethodTypes = new MethodType[this.invokerTypes.size()];
 365         int i = 0;
 366         for (String invokerType : invokerTypes) {
 367             // The invoker type to ask for is retrieved by removing the first
 368             // and the last argument, which needs to be of Object.class
 369             MethodType mt = asMethodType(invokerType);
 370             final int lastParam = mt.parameterCount() - 1;
 371             if (mt.parameterCount() < 2 ||
 372                     mt.parameterType(0) != Object.class ||
 373                     mt.parameterType(lastParam) != Object.class) {
 374                 throw new PluginException(
 375                         "Invoker type parameter must start and end with L");
 376             }
 377             mt = mt.dropParameterTypes(lastParam, lastParam + 1);
 378             invokerMethodTypes[i] = mt.dropParameterTypes(0, 1);
 379             i++;
 380         }
 381         try {
 382             byte[] bytes = JLIA.generateDirectMethodHandleHolderClassBytes(
 383                     DIRECT_HOLDER, directMethodTypes, dmhTypes);
 384             ResourcePoolEntry ndata = ResourcePoolEntry
 385                     .create(DIRECT_METHOD_HOLDER_ENTRY, bytes);
 386             out.add(ndata);
 387 
 388             bytes = JLIA.generateDelegatingMethodHandleHolderClassBytes(
 389                     DELEGATING_HOLDER, directMethodTypes);
 390             ndata = ResourcePoolEntry.create(DELEGATING_METHOD_HOLDER_ENTRY, bytes);
 391             out.add(ndata);
 392 
 393             bytes = JLIA.generateInvokersHolderClassBytes(INVOKERS_HOLDER,
 394                     invokerMethodTypes);
 395             ndata = ResourcePoolEntry.create(INVOKERS_HOLDER_ENTRY, bytes);
 396             out.add(ndata);
 397 
 398             bytes = JLIA.generateBasicFormsClassBytes(BASIC_FORMS_HOLDER);
 399             ndata = ResourcePoolEntry.create(BASIC_FORMS_HOLDER_ENTRY, bytes);
 400             out.add(ndata);
 401         } catch (Exception ex) {
 402             throw new PluginException(ex);
 403         }
 404     }
 405     private static final String DIRECT_METHOD_HOLDER_ENTRY =
 406             "/java.base/" + DIRECT_HOLDER + ".class";
 407     private static final String DELEGATING_METHOD_HOLDER_ENTRY =
 408             "/java.base/" + DELEGATING_HOLDER + ".class";
 409     private static final String BASIC_FORMS_HOLDER_ENTRY =
 410             "/java.base/" + BASIC_FORMS_HOLDER + ".class";
 411     private static final String INVOKERS_HOLDER_ENTRY =
 412             "/java.base/" + INVOKERS_HOLDER + ".class";
 413 
 414     // Convert LL -> LL, L3 -> LLL
 415     public static String expandSignature(String signature) {
 416         StringBuilder sb = new StringBuilder();
 417         char last = 'X';
 418         int count = 0;
 419         for (int i = 0; i < signature.length(); i++) {
 420             char c = signature.charAt(i);
 421             if (c >= '0' && c <= '9') {
 422                 count *= 10;
 423                 count += (c - '0');
 424             } else {
 425                 requireBasicType(c);
 426                 for (int j = 1; j < count; j++) {
 427                     sb.append(last);
 428                 }
 429                 sb.append(c);
 430                 last = c;
 431                 count = 0;
 432             }
 433         }
 434 
 435         // ended with a number, e.g., "L2": append last char count - 1 times
 436         if (count > 1) {
 437             requireBasicType(last);
 438             for (int j = 1; j < count; j++) {
 439                 sb.append(last);
 440             }
 441         }
 442         return sb.toString();
 443     }
 444 
 445     private static MethodType asMethodType(String basicSignatureString) {
 446         String[] parts = basicSignatureString.split("_");
 447         assert(parts.length == 2);
 448         assert(parts[1].length() == 1);
 449         String parameters = expandSignature(parts[0]);
 450         Class<?> rtype = simpleType(parts[1].charAt(0));
 451         if (parameters.isEmpty()) {
 452             return MethodType.methodType(rtype);
 453         } else {
 454             Class<?>[] ptypes = new Class<?>[parameters.length()];
 455             for (int i = 0; i < ptypes.length; i++) {
 456                 ptypes[i] = simpleType(parameters.charAt(i));
 457             }
 458             return MethodType.methodType(rtype, ptypes);
 459         }
 460     }
 461 
 462     private static Class<?> simpleType(char c) {
 463         switch (c) {
 464             case 'F':
 465                 return float.class;
 466             case 'D':
 467                 return double.class;
 468             case 'I':
 469                 return int.class;
 470             case 'L':
 471                 return Object.class;
 472             case 'J':
 473                 return long.class;
 474             case 'V':
 475                 return void.class;
 476             case 'Z':
 477             case 'B':
 478             case 'S':
 479             case 'C':
 480                 throw new IllegalArgumentException("Not a valid primitive: " + c +
 481                         " (use I instead)");
 482             default:
 483                 throw new IllegalArgumentException("Not a primitive: " + c);
 484         }
 485     }
 486 }