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