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.major() != linkedVersion.major() ||
 214                 baseVersion.minor() != linkedVersion.minor()) {
 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 "[BMH_RESOLVE]":
 246                         speciesTypes.add(expandSignature(parts[1]));
 247                         break;
 248                     case "[LF_RESOLVE]":
 249                         String methodType = parts[3];
 250                         validateMethodType(methodType);
 251                         if (parts[1].contains("Invokers")) {
 252                             invokerTypes.add(methodType);
 253                         } else if (parts[1].contains("DirectMethodHandle")) {
 254                             String dmh = parts[2];
 255                             // ignore getObject etc for now (generated
 256                             // by default)
 257                             if (DMH_METHOD_TYPE_MAP.containsKey(dmh)) {
 258                                 addDMHMethodType(dmh, methodType);
 259                             }
 260                         }
 261                         break;
 262                     default: break; // ignore
 263                 }
 264             });
 265     }
 266 
 267     private void addDMHMethodType(String dmh, String methodType) {
 268         validateMethodType(methodType);
 269         Set<String> methodTypes = dmhMethods.get(dmh);
 270         if (methodTypes == null) {
 271             methodTypes = new TreeSet<>();
 272             dmhMethods.put(dmh, methodTypes);
 273         }
 274         methodTypes.add(methodType);
 275     }
 276 
 277     private Stream<String> fileLines(File file) {
 278         try {
 279             return Files.lines(file.toPath());
 280         } catch (IOException io) {
 281             throw new PluginException("Couldn't read file");
 282         }
 283     }
 284 
 285     private void validateMethodTypes(Set<String> dmhMethodTypes) {
 286         for (String type : dmhMethodTypes) {
 287             validateMethodType(type);
 288         }
 289     }
 290 
 291     private void validateMethodType(String type) {
 292         String[] typeParts = type.split("_");
 293         // check return type (second part)
 294         if (typeParts.length != 2 || typeParts[1].length() != 1
 295                 || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) {
 296             throw new PluginException(
 297                     "Method type signature must be of form [LJIFD]*_[LJIFDV]");
 298         }
 299         // expand and check arguments (first part)
 300         expandSignature(typeParts[0]);
 301     }
 302 
 303     private static void requireBasicType(char c) {
 304         if ("LIJFD".indexOf(c) < 0) {
 305             throw new PluginException(
 306                     "Character " + c + " must correspond to a basic field type: LIJFD");
 307         }
 308     }
 309 
 310     @Override
 311     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
 312         if (ignoreVersion) {
 313             System.out.println(
 314                     PluginsResourceBundle
 315                             .getMessage(IGNORE_VERSION_WARNING));
 316         } else if (!checkVersion(getLinkedVersion(in))) {
 317             // The linked images are not version compatible
 318             if (mainArgument != null) {
 319                 // Log a mismatch warning if an argument was specified
 320                 System.out.println(
 321                         PluginsResourceBundle
 322                                 .getMessage(VERSION_MISMATCH_WARNING,
 323                                             getLinkedVersion(in),
 324                                             Runtime.version()));
 325             }
 326             in.transformAndCopy(entry -> entry, out);
 327             return out.build();
 328         }
 329 
 330         initialize(in);
 331         // Copy all but DMH_ENTRY to out
 332         in.transformAndCopy(entry -> {
 333                 // filter out placeholder entries
 334                 if (entry.path().equals(DIRECT_METHOD_HOLDER_ENTRY) ||
 335                     entry.path().equals(DELEGATING_METHOD_HOLDER_ENTRY) ||
 336                     entry.path().equals(INVOKERS_HOLDER_ENTRY) ||
 337                     entry.path().equals(BASIC_FORMS_HOLDER_ENTRY)) {
 338                     return null;
 339                 } else {
 340                     return entry;
 341                 }
 342             }, out);
 343 
 344         // Generate BMH Species classes
 345         speciesTypes.forEach(types -> generateBMHClass(types, out));
 346 
 347         // Generate LambdaForm Holder classes
 348         generateHolderClasses(out);
 349 
 350         // Let it go
 351         speciesTypes = null;
 352         invokerTypes = null;
 353         dmhMethods = null;
 354 
 355         return out.build();
 356     }
 357 
 358     @SuppressWarnings("unchecked")
 359     private void generateBMHClass(String types, ResourcePoolBuilder out) {
 360         try {
 361             // Generate class
 362             Map.Entry<String, byte[]> result =
 363                     JLIA.generateConcreteBMHClassBytes(types);
 364             String className = result.getKey();
 365             byte[] bytes = result.getValue();
 366 
 367             // Add class to pool
 368             ResourcePoolEntry ndata = ResourcePoolEntry.create(
 369                     "/java.base/" + className + ".class",
 370                     bytes);
 371             out.add(ndata);
 372         } catch (Exception ex) {
 373             throw new PluginException(ex);
 374         }
 375     }
 376 
 377     private void generateHolderClasses(ResourcePoolBuilder out) {
 378         int count = 0;
 379         for (Set<String> entry : dmhMethods.values()) {
 380             count += entry.size();
 381         }
 382         MethodType[] directMethodTypes = new MethodType[count];
 383         int[] dmhTypes = new int[count];
 384         int index = 0;
 385         for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) {
 386             String dmhType = entry.getKey();
 387             for (String type : entry.getValue()) {
 388                 // The DMH type to actually ask for is retrieved by removing
 389                 // the first argument, which needs to be of Object.class
 390                 MethodType mt = asMethodType(type);
 391                 if (mt.parameterCount() < 1 ||
 392                     mt.parameterType(0) != Object.class) {
 393                     throw new PluginException(
 394                             "DMH type parameter must start with L");
 395                 }
 396                 directMethodTypes[index] = mt.dropParameterTypes(0, 1);
 397                 dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType);
 398                 index++;
 399             }
 400         }
 401         MethodType[] invokerMethodTypes = new MethodType[this.invokerTypes.size()];
 402         int i = 0;
 403         for (String invokerType : invokerTypes) {
 404             // The invoker type to ask for is retrieved by removing the first
 405             // and the last argument, which needs to be of Object.class
 406             MethodType mt = asMethodType(invokerType);
 407             final int lastParam = mt.parameterCount() - 1;
 408             if (mt.parameterCount() < 2 ||
 409                     mt.parameterType(0) != Object.class ||
 410                     mt.parameterType(lastParam) != Object.class) {
 411                 throw new PluginException(
 412                         "Invoker type parameter must start and end with L");
 413             }
 414             mt = mt.dropParameterTypes(lastParam, lastParam + 1);
 415             invokerMethodTypes[i] = mt.dropParameterTypes(0, 1);
 416             i++;
 417         }
 418         try {
 419             byte[] bytes = JLIA.generateDirectMethodHandleHolderClassBytes(
 420                     DIRECT_HOLDER, directMethodTypes, dmhTypes);
 421             ResourcePoolEntry ndata = ResourcePoolEntry
 422                     .create(DIRECT_METHOD_HOLDER_ENTRY, bytes);
 423             out.add(ndata);
 424 
 425             bytes = JLIA.generateDelegatingMethodHandleHolderClassBytes(
 426                     DELEGATING_HOLDER, directMethodTypes);
 427             ndata = ResourcePoolEntry.create(DELEGATING_METHOD_HOLDER_ENTRY, bytes);
 428             out.add(ndata);
 429 
 430             bytes = JLIA.generateInvokersHolderClassBytes(INVOKERS_HOLDER,
 431                     invokerMethodTypes);
 432             ndata = ResourcePoolEntry.create(INVOKERS_HOLDER_ENTRY, bytes);
 433             out.add(ndata);
 434 
 435             bytes = JLIA.generateBasicFormsClassBytes(BASIC_FORMS_HOLDER);
 436             ndata = ResourcePoolEntry.create(BASIC_FORMS_HOLDER_ENTRY, bytes);
 437             out.add(ndata);
 438         } catch (Exception ex) {
 439             throw new PluginException(ex);
 440         }
 441     }
 442     private static final String DIRECT_METHOD_HOLDER_ENTRY =
 443             "/java.base/" + DIRECT_HOLDER + ".class";
 444     private static final String DELEGATING_METHOD_HOLDER_ENTRY =
 445             "/java.base/" + DELEGATING_HOLDER + ".class";
 446     private static final String BASIC_FORMS_HOLDER_ENTRY =
 447             "/java.base/" + BASIC_FORMS_HOLDER + ".class";
 448     private static final String INVOKERS_HOLDER_ENTRY =
 449             "/java.base/" + INVOKERS_HOLDER + ".class";
 450 
 451     // Convert LL -> LL, L3 -> LLL
 452     private static String expandSignature(String signature) {
 453         StringBuilder sb = new StringBuilder();
 454         char last = 'X';
 455         int count = 0;
 456         for (int i = 0; i < signature.length(); i++) {
 457             char c = signature.charAt(i);
 458             if (c >= '0' && c <= '9') {
 459                 count *= 10;
 460                 count += (c - '0');
 461             } else {
 462                 requireBasicType(c);
 463                 for (int j = 1; j < count; j++) {
 464                     sb.append(last);
 465                 }
 466                 sb.append(c);
 467                 last = c;
 468                 count = 0;
 469             }
 470         }
 471 
 472         // ended with a number, e.g., "L2": append last char count - 1 times
 473         if (count > 1) {
 474             requireBasicType(last);
 475             for (int j = 1; j < count; j++) {
 476                 sb.append(last);
 477             }
 478         }
 479         return sb.toString();
 480     }
 481 
 482     private static MethodType asMethodType(String basicSignatureString) {
 483         String[] parts = basicSignatureString.split("_");
 484         assert(parts.length == 2);
 485         assert(parts[1].length() == 1);
 486         String parameters = expandSignature(parts[0]);
 487         Class<?> rtype = simpleType(parts[1].charAt(0));
 488         if (parameters.isEmpty()) {
 489             return MethodType.methodType(rtype);
 490         } else {
 491             Class<?>[] ptypes = new Class<?>[parameters.length()];
 492             for (int i = 0; i < ptypes.length; i++) {
 493                 ptypes[i] = simpleType(parameters.charAt(i));
 494             }
 495             return MethodType.methodType(rtype, ptypes);
 496         }
 497     }
 498 
 499     private static Class<?> simpleType(char c) {
 500         switch (c) {
 501             case 'F':
 502                 return float.class;
 503             case 'D':
 504                 return double.class;
 505             case 'I':
 506                 return int.class;
 507             case 'L':
 508                 return Object.class;
 509             case 'J':
 510                 return long.class;
 511             case 'V':
 512                 return void.class;
 513             case 'Z':
 514             case 'B':
 515             case 'S':
 516             case 'C':
 517                 throw new IllegalArgumentException("Not a valid primitive: " + c +
 518                         " (use I instead)");
 519             default:
 520                 throw new IllegalArgumentException("Not a primitive: " + c);
 521         }
 522     }
 523 }