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     private static final String IGNORE_VERSION = "ignore-version";
  58 
  59     private static final String DESCRIPTION = PluginsResourceBundle.getDescription(NAME);
  60     private static final String IGNORE_VERSION_WARNING = NAME + ".ignore.version.warn";
  61     private static final String VERSION_MISMATCH_WARNING = NAME + ".version.mismatch.warn";
  62 
  63     private static final String DEFAULT_TRACE_FILE = "default_jli_trace.txt";
  64 
  65     private static final String DIRECT_HOLDER = "java/lang/invoke/DirectMethodHandle$Holder";
  66     private static final String DMH_INVOKE_VIRTUAL = "invokeVirtual";
  67     private static final String DMH_INVOKE_STATIC = "invokeStatic";
  68     private static final String DMH_INVOKE_SPECIAL = "invokeSpecial";
  69     private static final String DMH_NEW_INVOKE_SPECIAL = "newInvokeSpecial";
  70     private static final String DMH_INVOKE_INTERFACE = "invokeInterface";
  71     private static final String DMH_INVOKE_STATIC_INIT = "invokeStaticInit";
  72 
  73     private static final String DELEGATING_HOLDER = "java/lang/invoke/DelegatingMethodHandle$Holder";
  74     private static final String BASIC_FORMS_HOLDER = "java/lang/invoke/LambdaForm$Holder";
  75     private static final String INVOKERS_HOLDER = "java/lang/invoke/Invokers$Holder";
  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     Map<String, Set<String>> dmhMethods = Map.of();
  85 
  86     String mainArgument;
  87 
  88     boolean ignoreVersion;
  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", "LILL_I", "L6_L");
 137     }
 138 
 139     /**
 140      * @return the list of default DirectMethodHandle methods to generate.
 141      */
 142     private static Map<String, Set<String>> defaultDMHMethods() {
 143         return Map.of(
 144             DMH_INVOKE_VIRTUAL, Set.of("L_L", "LL_L", "LLI_I", "L3_V"),
 145             DMH_INVOKE_SPECIAL, Set.of("LL_I", "LL_L", "LLF_L", "LLD_L", "L3_L",
 146                 "L4_L", "L5_L", "L6_L", "L7_L", "L8_L", "LLI_I", "LLI_L",
 147                 "LLIL_I", "LLII_I", "LLII_L", "L3I_L", "L3I_I", "LLILI_I",
 148                 "LLIIL_L", "LLIILL_L", "LLIILL_I", "LLIIL_I", "LLILIL_I",
 149                 "LLILILL_I", "LLILII_I", "LLI3_I", "LLI3L_I", "LLI3LL_I",
 150                 "LLI3_L", "LLI4_I"),
 151             DMH_INVOKE_STATIC, Set.of("LII_I", "LIL_I", "LILIL_I", "LILII_I",
 152                 "L_I", "L_L", "L_V", "LD_L", "LF_L", "LI_I", "LII_L", "LLI_L",
 153                 "LL_V", "LL_L", "L3_L", "L4_L", "L5_L", "L6_L", "L7_L",
 154                 "L8_L", "L9_L", "L10_L", "L10I_L", "L10II_L", "L10IIL_L",
 155                 "L11_L", "L12_L", "L13_L", "L14_L", "L14I_L", "L14II_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         ignoreVersion = Boolean.parseBoolean(config.get(IGNORE_VERSION));
 174     }
 175 
 176     public void initialize(ResourcePool in) {
 177         // Start with the default configuration
 178         speciesTypes = defaultSpecies().stream()
 179                 .map(type -> expandSignature(type))
 180                 .collect(Collectors.toSet());
 181 
 182         invokerTypes = defaultInvokers();
 183         validateMethodTypes(invokerTypes);
 184 
 185         dmhMethods = defaultDMHMethods();
 186         for (Set<String> dmhMethodTypes : dmhMethods.values()) {
 187             validateMethodTypes(dmhMethodTypes);
 188         }
 189 
 190         // Extend the default configuration with the contents in the supplied
 191         // input file - if none was supplied we look for the default file
 192         if (mainArgument == null || !mainArgument.startsWith("@")) {
 193             try (InputStream traceFile =
 194                     this.getClass().getResourceAsStream(DEFAULT_TRACE_FILE)) {
 195                 if (traceFile != null) {
 196                     readTraceConfig(
 197                         new BufferedReader(
 198                             new InputStreamReader(traceFile)).lines());
 199                 }
 200             } catch (Exception e) {
 201                 throw new PluginException("Couldn't read " + DEFAULT_TRACE_FILE, e);
 202             }
 203         } else {
 204             File file = new File(mainArgument.substring(1));
 205             if (file.exists()) {
 206                 readTraceConfig(fileLines(file));
 207             }
 208         }
 209     }
 210 
 211     private boolean checkVersion(Runtime.Version linkedVersion) {
 212         Runtime.Version baseVersion = Runtime.version();
 213         if (baseVersion.feature() != linkedVersion.feature() ||
 214                 baseVersion.interim() != linkedVersion.interim()) {
 215             return false;
 216         }
 217         return true;
 218     }
 219 
 220     private Runtime.Version getLinkedVersion(ResourcePool in) {
 221         ModuleDescriptor.Version version = in.moduleView()
 222                 .findModule("java.base")
 223                 .get()
 224                 .descriptor()
 225                 .version()
 226                 .orElseThrow(() -> new PluginException("No version defined in "
 227                         + "the java.base being linked"));
 228          return Runtime.Version.parse(version.toString());
 229     }
 230 
 231     private void readTraceConfig(Stream<String> lines) {
 232         // Use TreeSet/TreeMap to keep things sorted in a deterministic
 233         // order to avoid scrambling the layout on small changes and to
 234         // ease finding methods in the generated code
 235         speciesTypes = new TreeSet<>(speciesTypes);
 236         invokerTypes = new TreeSet<>(invokerTypes);
 237         TreeMap<String, Set<String>> newDMHMethods = new TreeMap<>();
 238         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 239             newDMHMethods.put(entry.getKey(), new TreeSet<>(entry.getValue()));
 240         }
 241         dmhMethods = newDMHMethods;
 242         lines.map(line -> line.split(" "))
 243              .forEach(parts -> {
 244                 switch (parts[0]) {
 245                     case "[SPECIES_RESOLVE]":
 246                         // Allow for new types of species data classes being resolved here
 247                         if (parts.length == 3 && parts[1].startsWith("java.lang.invoke.BoundMethodHandle$Species_")) {
 248                             String species = parts[1].substring("java.lang.invoke.BoundMethodHandle$Species_".length());
 249                             if (!"L".equals(species)) {
 250                                 speciesTypes.add(expandSignature(species));
 251                             }
 252                         }
 253                         break;
 254                     case "[LF_RESOLVE]":
 255                         String methodType = parts[3];
 256                         validateMethodType(methodType);
 257                         if (parts[1].contains("Invokers")) {
 258                             invokerTypes.add(methodType);
 259                         } else if (parts[1].contains("DirectMethodHandle")) {
 260                             String dmh = parts[2];
 261                             // ignore getObject etc for now (generated
 262                             // by default)
 263                             if (DMH_METHOD_TYPE_MAP.containsKey(dmh)) {
 264                                 addDMHMethodType(dmh, methodType);
 265                             }
 266                         }
 267                         break;
 268                     default: break; // ignore
 269                 }
 270             });
 271     }
 272 
 273     private void addDMHMethodType(String dmh, String methodType) {
 274         validateMethodType(methodType);
 275         Set<String> methodTypes = dmhMethods.get(dmh);
 276         if (methodTypes == null) {
 277             methodTypes = new TreeSet<>();
 278             dmhMethods.put(dmh, methodTypes);
 279         }
 280         methodTypes.add(methodType);
 281     }
 282 
 283     private Stream<String> fileLines(File file) {
 284         try {
 285             return Files.lines(file.toPath());
 286         } catch (IOException io) {
 287             throw new PluginException("Couldn't read file");
 288         }
 289     }
 290 
 291     private void validateMethodTypes(Set<String> dmhMethodTypes) {
 292         for (String type : dmhMethodTypes) {
 293             validateMethodType(type);
 294         }
 295     }
 296 
 297     private void validateMethodType(String type) {
 298         String[] typeParts = type.split("_");
 299         // check return type (second part)
 300         if (typeParts.length != 2 || typeParts[1].length() != 1
 301                 || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) {
 302             throw new PluginException(
 303                     "Method type signature must be of form [LJIFD]*_[LJIFDV]");
 304         }
 305         // expand and check arguments (first part)
 306         expandSignature(typeParts[0]);
 307     }
 308 
 309     private static void requireBasicType(char c) {
 310         if ("LIJFD".indexOf(c) < 0) {
 311             throw new PluginException(
 312                     "Character " + c + " must correspond to a basic field type: LIJFD");
 313         }
 314     }
 315 
 316     @Override
 317     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
 318         if (ignoreVersion) {
 319             System.out.println(
 320                     PluginsResourceBundle
 321                             .getMessage(IGNORE_VERSION_WARNING));
 322         } else if (!checkVersion(getLinkedVersion(in))) {
 323             // The linked images are not version compatible
 324             if (mainArgument != null) {
 325                 // Log a mismatch warning if an argument was specified
 326                 System.out.println(
 327                         PluginsResourceBundle
 328                                 .getMessage(VERSION_MISMATCH_WARNING,
 329                                             getLinkedVersion(in),
 330                                             Runtime.version()));
 331             }
 332             in.transformAndCopy(entry -> entry, out);
 333             return out.build();
 334         }
 335 
 336         initialize(in);
 337         // Copy all but DMH_ENTRY to out
 338         in.transformAndCopy(entry -> {
 339                 // filter out placeholder entries
 340                 if (entry.path().equals(DIRECT_METHOD_HOLDER_ENTRY) ||
 341                     entry.path().equals(DELEGATING_METHOD_HOLDER_ENTRY) ||
 342                     entry.path().equals(INVOKERS_HOLDER_ENTRY) ||
 343                     entry.path().equals(BASIC_FORMS_HOLDER_ENTRY)) {
 344                     return null;
 345                 } else {
 346                     return entry;
 347                 }
 348             }, out);
 349 
 350         // Generate BMH Species classes
 351         speciesTypes.forEach(types -> generateBMHClass(types, out));
 352 
 353         // Generate LambdaForm Holder classes
 354         generateHolderClasses(out);
 355 
 356         // Let it go
 357         speciesTypes = null;
 358         invokerTypes = null;
 359         dmhMethods = null;
 360 
 361         return out.build();
 362     }
 363 
 364     @SuppressWarnings("unchecked")
 365     private void generateBMHClass(String types, ResourcePoolBuilder out) {
 366         try {
 367             // Generate class
 368             Map.Entry<String, byte[]> result =
 369                     JLIA.generateConcreteBMHClassBytes(types);
 370             String className = result.getKey();
 371             byte[] bytes = result.getValue();
 372 
 373             // Add class to pool
 374             ResourcePoolEntry ndata = ResourcePoolEntry.create(
 375                     "/java.base/" + className + ".class",
 376                     bytes);
 377             out.add(ndata);
 378         } catch (Exception ex) {
 379             throw new PluginException(ex);
 380         }
 381     }
 382 
 383     private void generateHolderClasses(ResourcePoolBuilder out) {
 384         int count = 0;
 385         for (Set<String> entry : dmhMethods.values()) {
 386             count += entry.size();
 387         }
 388         MethodType[] directMethodTypes = new MethodType[count];
 389         int[] dmhTypes = new int[count];
 390         int index = 0;
 391         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 392             String dmhType = entry.getKey();
 393             for (String type : entry.getValue()) {
 394                 // The DMH type to actually ask for is retrieved by removing
 395                 // the first argument, which needs to be of Object.class
 396                 MethodType mt = asMethodType(type);
 397                 if (mt.parameterCount() < 1 ||
 398                     mt.parameterType(0) != Object.class) {
 399                     throw new PluginException(
 400                             "DMH type parameter must start with L");
 401                 }
 402                 directMethodTypes[index] = mt.dropParameterTypes(0, 1);
 403                 dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType);
 404                 index++;
 405             }
 406         }
 407         MethodType[] invokerMethodTypes = new MethodType[this.invokerTypes.size()];
 408         int i = 0;
 409         for (String invokerType : invokerTypes) {
 410             // The invoker type to ask for is retrieved by removing the first
 411             // and the last argument, which needs to be of Object.class
 412             MethodType mt = asMethodType(invokerType);
 413             final int lastParam = mt.parameterCount() - 1;
 414             if (mt.parameterCount() < 2 ||
 415                     mt.parameterType(0) != Object.class ||
 416                     mt.parameterType(lastParam) != Object.class) {
 417                 throw new PluginException(
 418                         "Invoker type parameter must start and end with L");
 419             }
 420             mt = mt.dropParameterTypes(lastParam, lastParam + 1);
 421             invokerMethodTypes[i] = mt.dropParameterTypes(0, 1);
 422             i++;
 423         }
 424         try {
 425             byte[] bytes = JLIA.generateDirectMethodHandleHolderClassBytes(
 426                     DIRECT_HOLDER, directMethodTypes, dmhTypes);
 427             ResourcePoolEntry ndata = ResourcePoolEntry
 428                     .create(DIRECT_METHOD_HOLDER_ENTRY, bytes);
 429             out.add(ndata);
 430 
 431             bytes = JLIA.generateDelegatingMethodHandleHolderClassBytes(
 432                     DELEGATING_HOLDER, directMethodTypes);
 433             ndata = ResourcePoolEntry.create(DELEGATING_METHOD_HOLDER_ENTRY, bytes);
 434             out.add(ndata);
 435 
 436             bytes = JLIA.generateInvokersHolderClassBytes(INVOKERS_HOLDER,
 437                     invokerMethodTypes);
 438             ndata = ResourcePoolEntry.create(INVOKERS_HOLDER_ENTRY, bytes);
 439             out.add(ndata);
 440 
 441             bytes = JLIA.generateBasicFormsClassBytes(BASIC_FORMS_HOLDER);
 442             ndata = ResourcePoolEntry.create(BASIC_FORMS_HOLDER_ENTRY, bytes);
 443             out.add(ndata);
 444         } catch (Exception ex) {
 445             throw new PluginException(ex);
 446         }
 447     }
 448     private static final String DIRECT_METHOD_HOLDER_ENTRY =
 449             "/java.base/" + DIRECT_HOLDER + ".class";
 450     private static final String DELEGATING_METHOD_HOLDER_ENTRY =
 451             "/java.base/" + DELEGATING_HOLDER + ".class";
 452     private static final String BASIC_FORMS_HOLDER_ENTRY =
 453             "/java.base/" + BASIC_FORMS_HOLDER + ".class";
 454     private static final String INVOKERS_HOLDER_ENTRY =
 455             "/java.base/" + INVOKERS_HOLDER + ".class";
 456 
 457     // Convert LL -> LL, L3 -> LLL
 458     public static String expandSignature(String signature) {
 459         StringBuilder sb = new StringBuilder();
 460         char last = 'X';
 461         int count = 0;
 462         for (int i = 0; i < signature.length(); i++) {
 463             char c = signature.charAt(i);
 464             if (c >= '0' && c <= '9') {
 465                 count *= 10;
 466                 count += (c - '0');
 467             } else {
 468                 requireBasicType(c);
 469                 for (int j = 1; j < count; j++) {
 470                     sb.append(last);
 471                 }
 472                 sb.append(c);
 473                 last = c;
 474                 count = 0;
 475             }
 476         }
 477 
 478         // ended with a number, e.g., "L2": append last char count - 1 times
 479         if (count > 1) {
 480             requireBasicType(last);
 481             for (int j = 1; j < count; j++) {
 482                 sb.append(last);
 483             }
 484         }
 485         return sb.toString();
 486     }
 487 
 488     private static MethodType asMethodType(String basicSignatureString) {
 489         String[] parts = basicSignatureString.split("_");
 490         assert(parts.length == 2);
 491         assert(parts[1].length() == 1);
 492         String parameters = expandSignature(parts[0]);
 493         Class<?> rtype = simpleType(parts[1].charAt(0));
 494         if (parameters.isEmpty()) {
 495             return MethodType.methodType(rtype);
 496         } else {
 497             Class<?>[] ptypes = new Class<?>[parameters.length()];
 498             for (int i = 0; i < ptypes.length; i++) {
 499                 ptypes[i] = simpleType(parameters.charAt(i));
 500             }
 501             return MethodType.methodType(rtype, ptypes);
 502         }
 503     }
 504 
 505     private static Class<?> simpleType(char c) {
 506         switch (c) {
 507             case 'F':
 508                 return float.class;
 509             case 'D':
 510                 return double.class;
 511             case 'I':
 512                 return int.class;
 513             case 'L':
 514                 return Object.class;
 515             case 'J':
 516                 return long.class;
 517             case 'V':
 518                 return void.class;
 519             case 'Z':
 520             case 'B':
 521             case 'S':
 522             case 'C':
 523                 throw new IllegalArgumentException("Not a valid primitive: " + c +
 524                         " (use I instead)");
 525             default:
 526                 throw new IllegalArgumentException("Not a primitive: " + c);
 527         }
 528     }
 529 }