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 
  73     private static final String INVOKERS_HOLDER_NAME = "java.lang.invoke.Invokers$Holder";
  74     private static final String INVOKERS_HOLDER_INTERNAL_NAME = INVOKERS_HOLDER_NAME.replace('.', '/');
  75 
  76     private static final JavaLangInvokeAccess JLIA
  77             = SharedSecrets.getJavaLangInvokeAccess();
  78 
  79     Set<String> speciesTypes = Set.of();
  80 
  81     Set<String> invokerTypes = Set.of();
  82 
  83     Set<String> callSiteTypes = Set.of();
  84 
  85     Map<String, Set<String>> dmhMethods = Map.of();
  86 
  87     String mainArgument;
  88 
  89     public GenerateJLIClassesPlugin() {
  90     }
  91 
  92     @Override
  93     public String getName() {
  94         return NAME;
  95     }
  96 
  97     @Override
  98     public String getDescription() {
  99         return DESCRIPTION;
 100     }
 101 
 102     @Override
 103     public Set<State> getState() {
 104         return EnumSet.of(State.AUTO_ENABLED, State.FUNCTIONAL);
 105     }
 106 
 107     @Override
 108     public boolean hasArguments() {
 109         return true;
 110     }
 111 
 112     @Override
 113     public String getArgumentsDescription() {
 114        return PluginsResourceBundle.getArgument(NAME);
 115     }
 116 
 117     /**
 118      * @return the default Species forms to generate.
 119      *
 120      * This list was derived from running a small startup benchmark.
 121      * A better long-term solution is to define and run a set of quick
 122      * generators and extracting this list as a step in the build process.
 123      */
 124     public static Set<String> defaultSpecies() {
 125         return Set.of("LL", "L3", "L4", "L5", "L6", "L7", "L7I",
 126                 "L7II", "L7IIL", "L8", "L9", "L10", "L10I", "L10II", "L10IIL",
 127                 "L11", "L12", "L13", "LI", "D", "L3I", "LIL", "LLI", "LLIL",
 128                 "LILL", "I", "LLILL");
 129     }
 130 
 131     /**
 132      * @return the default invoker forms to generate.
 133      */
 134     private static Set<String> defaultInvokers() {
 135         return Set.of("LL_L", "LL_I", "LLLL_L", "LLLL_I", "LLIL_L", "LLIL_I", "L6_L");
 136     }
 137 
 138     /**
 139      * @return the list of default DirectMethodHandle methods to generate.
 140      */
 141     private static Map<String, Set<String>> defaultDMHMethods() {
 142         return Map.of(
 143             DMH_INVOKE_VIRTUAL, Set.of("L_L", "LL_L", "LLI_I", "L3_V"),
 144             DMH_INVOKE_SPECIAL, Set.of("LL_I", "LL_L", "LLF_L", "LLD_L", "L3_L",
 145                 "L4_L", "L5_L", "L6_L", "L7_L", "L8_L", "LLI_I", "LLI_L",
 146                 "LLIL_I", "LLII_I", "LLII_L", "L3I_L", "L3I_I", "LLILI_I",
 147                 "LLIIL_L", "LLIILL_L", "LLIILL_I", "LLIIL_I", "LLILIL_I",
 148                 "LLILILL_I", "LLILII_I", "LLI3_I", "LLI3L_I", "LLI3LL_I",
 149                 "LLI3_L", "LLI4_I"),
 150             DMH_INVOKE_STATIC, Set.of("LII_I", "LIL_I", "LILIL_I", "LILII_I",
 151                 "L_I", "L_L", "L_V", "LD_L", "LF_L", "LI_I", "LII_L", "LLI_L",
 152                 "LL_V", "LL_L", "L3_L", "L4_L", "L5_L", "L6_L", "L7_L",
 153                 "L8_L", "L9_L", "L10_L", "L10I_L", "L10II_L", "L10IIL_L",
 154                 "L11_L", "L12_L", "L13_L", "L14_L", "L14I_L", "L14II_L"),
 155             DMH_NEW_INVOKE_SPECIAL, Set.of("L_L", "LL_L")
 156         );
 157     }
 158 
 159     // Map from DirectMethodHandle method type to internal ID
 160     private static final Map<String, Integer> DMH_METHOD_TYPE_MAP =
 161             Map.of(
 162                 DMH_INVOKE_VIRTUAL,     0,
 163                 DMH_INVOKE_STATIC,      1,
 164                 DMH_INVOKE_SPECIAL,     2,
 165                 DMH_NEW_INVOKE_SPECIAL, 3,
 166                 DMH_INVOKE_INTERFACE,   4,
 167                 DMH_INVOKE_STATIC_INIT, 5
 168             );
 169 
 170     @Override
 171     public void configure(Map<String, String> config) {
 172         mainArgument = config.get(NAME);
 173     }
 174 
 175     public void initialize(ResourcePool in) {
 176         // Start with the default configuration
 177         speciesTypes = defaultSpecies().stream()
 178                 .map(type -> expandSignature(type))
 179                 .collect(Collectors.toSet());
 180 
 181         invokerTypes = defaultInvokers();
 182         validateMethodTypes(invokerTypes);
 183 
 184         dmhMethods = defaultDMHMethods();
 185         for (Set<String> dmhMethodTypes : dmhMethods.values()) {
 186             validateMethodTypes(dmhMethodTypes);
 187         }
 188 
 189         // Extend the default configuration with the contents in the supplied
 190         // input file - if none was supplied we look for the default file
 191         if (mainArgument == null || !mainArgument.startsWith("@")) {
 192             try (InputStream traceFile =
 193                     this.getClass().getResourceAsStream(DEFAULT_TRACE_FILE)) {
 194                 if (traceFile != null) {
 195                     readTraceConfig(
 196                         new BufferedReader(
 197                             new InputStreamReader(traceFile)).lines());
 198                 }
 199             } catch (Exception e) {
 200                 throw new PluginException("Couldn't read " + DEFAULT_TRACE_FILE, e);
 201             }
 202         } else {
 203             File file = new File(mainArgument.substring(1));
 204             if (file.exists()) {
 205                 readTraceConfig(fileLines(file));
 206             }
 207         }
 208     }
 209 
 210     private void readTraceConfig(Stream<String> lines) {
 211         // Use TreeSet/TreeMap to keep things sorted in a deterministic
 212         // order to avoid scrambling the layout on small changes and to
 213         // ease finding methods in the generated code
 214         speciesTypes = new TreeSet<>(speciesTypes);
 215         invokerTypes = new TreeSet<>(invokerTypes);
 216         callSiteTypes = new TreeSet<>(callSiteTypes);
 217 
 218         TreeMap<String, Set<String>> newDMHMethods = new TreeMap<>();
 219         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 220             newDMHMethods.put(entry.getKey(), new TreeSet<>(entry.getValue()));
 221         }
 222         dmhMethods = newDMHMethods;
 223         lines.map(line -> line.split(" "))
 224              .forEach(parts -> {
 225                 switch (parts[0]) {
 226                     case "[SPECIES_RESOLVE]":
 227                         // Allow for new types of species data classes being resolved here
 228                         if (parts.length == 3 && parts[1].startsWith("java.lang.invoke.BoundMethodHandle$Species_")) {
 229                             String species = parts[1].substring("java.lang.invoke.BoundMethodHandle$Species_".length());
 230                             if (!"L".equals(species)) {
 231                                 speciesTypes.add(expandSignature(species));
 232                             }
 233                         }
 234                         break;
 235                     case "[LF_RESOLVE]":
 236                         String methodType = parts[3];
 237                         validateMethodType(methodType);
 238                         if (parts[1].equals(INVOKERS_HOLDER_NAME)) {
 239                             if ("linkToTargetMethod".equals(parts[2]) ||
 240                                     "linkToCallSite".equals(parts[2])) {
 241                                 callSiteTypes.add(methodType);
 242                             } else {
 243                                 invokerTypes.add(methodType);
 244                             }
 245                         } else if (parts[1].contains("DirectMethodHandle")) {
 246                             String dmh = parts[2];
 247                             // ignore getObject etc for now (generated
 248                             // by default)
 249                             if (DMH_METHOD_TYPE_MAP.containsKey(dmh)) {
 250                                 addDMHMethodType(dmh, methodType);
 251                             }
 252                         }
 253                         break;
 254                     default: break; // ignore
 255                 }
 256             });
 257     }
 258 
 259     private void addDMHMethodType(String dmh, String methodType) {
 260         validateMethodType(methodType);
 261         Set<String> methodTypes = dmhMethods.get(dmh);
 262         if (methodTypes == null) {
 263             methodTypes = new TreeSet<>();
 264             dmhMethods.put(dmh, methodTypes);
 265         }
 266         methodTypes.add(methodType);
 267     }
 268 
 269     private Stream<String> fileLines(File file) {
 270         try {
 271             return Files.lines(file.toPath());
 272         } catch (IOException io) {
 273             throw new PluginException("Couldn't read file");
 274         }
 275     }
 276 
 277     private void validateMethodTypes(Set<String> dmhMethodTypes) {
 278         for (String type : dmhMethodTypes) {
 279             validateMethodType(type);
 280         }
 281     }
 282 
 283     private void validateMethodType(String type) {
 284         String[] typeParts = type.split("_");
 285         // check return type (second part)
 286         if (typeParts.length != 2 || typeParts[1].length() != 1
 287                 || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) {
 288             throw new PluginException(
 289                     "Method type signature must be of form [LJIFD]*_[LJIFDV]");
 290         }
 291         // expand and check arguments (first part)
 292         expandSignature(typeParts[0]);
 293     }
 294 
 295     private static void requireBasicType(char c) {
 296         if ("LIJFD".indexOf(c) < 0) {
 297             throw new PluginException(
 298                     "Character " + c + " must correspond to a basic field type: LIJFD");
 299         }
 300     }
 301 
 302     @Override
 303     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
 304         initialize(in);
 305         // Copy all but DMH_ENTRY to out
 306         in.transformAndCopy(entry -> {
 307                 // filter out placeholder entries
 308                 String path = entry.path();
 309                 if (path.equals(DIRECT_METHOD_HOLDER_ENTRY) ||
 310                     path.equals(DELEGATING_METHOD_HOLDER_ENTRY) ||
 311                     path.equals(INVOKERS_HOLDER_ENTRY) ||
 312                     path.equals(BASIC_FORMS_HOLDER_ENTRY)) {
 313                     return null;
 314                 } else {
 315                     return entry;
 316                 }
 317             }, out);
 318 
 319         // Generate BMH Species classes
 320         speciesTypes.forEach(types -> generateBMHClass(types, out));
 321 
 322         // Generate LambdaForm Holder classes
 323         generateHolderClasses(out);
 324 
 325         // Let it go
 326         speciesTypes = null;
 327         invokerTypes = null;
 328         dmhMethods = null;
 329 
 330         return out.build();
 331     }
 332 
 333     @SuppressWarnings("unchecked")
 334     private void generateBMHClass(String types, ResourcePoolBuilder out) {
 335         try {
 336             // Generate class
 337             Map.Entry<String, byte[]> result =
 338                     JLIA.generateConcreteBMHClassBytes(types);
 339             String className = result.getKey();
 340             byte[] bytes = result.getValue();
 341 
 342             // Add class to pool
 343             ResourcePoolEntry ndata = ResourcePoolEntry.create(
 344                     "/java.base/" + className + ".class",
 345                     bytes);
 346             out.add(ndata);
 347         } catch (Exception ex) {
 348             throw new PluginException(ex);
 349         }
 350     }
 351 
 352     private void generateHolderClasses(ResourcePoolBuilder out) {
 353         int count = 0;
 354         for (Set<String> entry : dmhMethods.values()) {
 355             count += entry.size();
 356         }
 357         MethodType[] directMethodTypes = new MethodType[count];
 358         int[] dmhTypes = new int[count];
 359         int index = 0;
 360         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 361             String dmhType = entry.getKey();
 362             for (String type : entry.getValue()) {
 363                 // The DMH type to actually ask for is retrieved by removing
 364                 // the first argument, which needs to be of Object.class
 365                 MethodType mt = asMethodType(type);
 366                 if (mt.parameterCount() < 1 ||
 367                     mt.parameterType(0) != Object.class) {
 368                     throw new PluginException(
 369                             "DMH type parameter must start with L");
 370                 }
 371                 directMethodTypes[index] = mt.dropParameterTypes(0, 1);
 372                 dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType);
 373                 index++;
 374             }
 375         }
 376 
 377         // The invoker type to ask for is retrieved by removing the first
 378         // and the last argument, which needs to be of Object.class
 379         MethodType[] invokerMethodTypes = new MethodType[this.invokerTypes.size()];
 380         int i = 0;
 381         for (String invokerType : invokerTypes) {
 382             MethodType mt = asMethodType(invokerType);
 383             final int lastParam = mt.parameterCount() - 1;
 384             if (mt.parameterCount() < 2 ||
 385                     mt.parameterType(0) != Object.class ||
 386                     mt.parameterType(lastParam) != Object.class) {
 387                 throw new PluginException(
 388                         "Invoker type parameter must start and end with Object: " + invokerType);
 389             }
 390             mt = mt.dropParameterTypes(lastParam, lastParam + 1);
 391             invokerMethodTypes[i] = mt.dropParameterTypes(0, 1);
 392             i++;
 393         }
 394 
 395         // The callSite type to ask for is retrieved by removing the last
 396         // argument, which needs to be of Object.class
 397         MethodType[] callSiteMethodTypes = new MethodType[this.callSiteTypes.size()];
 398         i = 0;
 399         for (String callSiteType : callSiteTypes) {
 400             MethodType mt = asMethodType(callSiteType);
 401             final int lastParam = mt.parameterCount() - 1;
 402             if (mt.parameterCount() < 1 ||
 403                     mt.parameterType(lastParam) != Object.class) {
 404                 throw new PluginException(
 405                         "CallSite type parameter must end with Object: " + callSiteType);
 406             }
 407             callSiteMethodTypes[i] = mt.dropParameterTypes(lastParam, lastParam + 1);
 408             i++;
 409         }
 410         try {
 411             byte[] bytes = JLIA.generateDirectMethodHandleHolderClassBytes(
 412                     DIRECT_HOLDER, directMethodTypes, dmhTypes);
 413             ResourcePoolEntry ndata = ResourcePoolEntry
 414                     .create(DIRECT_METHOD_HOLDER_ENTRY, bytes);
 415             out.add(ndata);
 416 
 417             bytes = JLIA.generateDelegatingMethodHandleHolderClassBytes(
 418                     DELEGATING_HOLDER, directMethodTypes);
 419             ndata = ResourcePoolEntry.create(DELEGATING_METHOD_HOLDER_ENTRY, bytes);
 420             out.add(ndata);
 421 
 422             bytes = JLIA.generateInvokersHolderClassBytes(INVOKERS_HOLDER_INTERNAL_NAME,
 423                     invokerMethodTypes, callSiteMethodTypes);
 424             ndata = ResourcePoolEntry.create(INVOKERS_HOLDER_ENTRY, bytes);
 425             out.add(ndata);
 426 
 427             bytes = JLIA.generateBasicFormsClassBytes(BASIC_FORMS_HOLDER);
 428             ndata = ResourcePoolEntry.create(BASIC_FORMS_HOLDER_ENTRY, bytes);
 429             out.add(ndata);
 430         } catch (Exception ex) {
 431             throw new PluginException(ex);
 432         }
 433     }
 434     private static final String DIRECT_METHOD_HOLDER_ENTRY =
 435             "/java.base/" + DIRECT_HOLDER + ".class";
 436     private static final String DELEGATING_METHOD_HOLDER_ENTRY =
 437             "/java.base/" + DELEGATING_HOLDER + ".class";
 438     private static final String BASIC_FORMS_HOLDER_ENTRY =
 439             "/java.base/" + BASIC_FORMS_HOLDER + ".class";
 440     private static final String INVOKERS_HOLDER_ENTRY =
 441             "/java.base/" + INVOKERS_HOLDER_INTERNAL_NAME + ".class";
 442 
 443     // Convert LL -> LL, L3 -> LLL
 444     public static String expandSignature(String signature) {
 445         StringBuilder sb = new StringBuilder();
 446         char last = 'X';
 447         int count = 0;
 448         for (int i = 0; i < signature.length(); i++) {
 449             char c = signature.charAt(i);
 450             if (c >= '0' && c <= '9') {
 451                 count *= 10;
 452                 count += (c - '0');
 453             } else {
 454                 requireBasicType(c);
 455                 for (int j = 1; j < count; j++) {
 456                     sb.append(last);
 457                 }
 458                 sb.append(c);
 459                 last = c;
 460                 count = 0;
 461             }
 462         }
 463 
 464         // ended with a number, e.g., "L2": append last char count - 1 times
 465         if (count > 1) {
 466             requireBasicType(last);
 467             for (int j = 1; j < count; j++) {
 468                 sb.append(last);
 469             }
 470         }
 471         return sb.toString();
 472     }
 473 
 474     private static MethodType asMethodType(String basicSignatureString) {
 475         String[] parts = basicSignatureString.split("_");
 476         assert(parts.length == 2);
 477         assert(parts[1].length() == 1);
 478         String parameters = expandSignature(parts[0]);
 479         Class<?> rtype = simpleType(parts[1].charAt(0));
 480         if (parameters.isEmpty()) {
 481             return MethodType.methodType(rtype);
 482         } else {
 483             Class<?>[] ptypes = new Class<?>[parameters.length()];
 484             for (int i = 0; i < ptypes.length; i++) {
 485                 ptypes[i] = simpleType(parameters.charAt(i));
 486             }
 487             return MethodType.methodType(rtype, ptypes);
 488         }
 489     }
 490 
 491     private static Class<?> simpleType(char c) {
 492         switch (c) {
 493             case 'F':
 494                 return float.class;
 495             case 'D':
 496                 return double.class;
 497             case 'I':
 498                 return int.class;
 499             case 'L':
 500                 return Object.class;
 501             case 'J':
 502                 return long.class;
 503             case 'V':
 504                 return void.class;
 505             case 'Z':
 506             case 'B':
 507             case 'S':
 508             case 'C':
 509                 throw new IllegalArgumentException("Not a valid primitive: " + c +
 510                         " (use I instead)");
 511             default:
 512                 throw new IllegalArgumentException("Not a primitive: " + c);
 513         }
 514     }
 515 }