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