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 */
25package jdk.tools.jlink.internal.plugins;
26
27import java.io.BufferedReader;
28import java.io.File;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.InputStreamReader;
32import java.lang.invoke.MethodType;
33import java.lang.module.ModuleDescriptor;
34import java.nio.file.Files;
35import java.util.EnumSet;
36import java.util.Map;
37import java.util.Optional;
38import java.util.Set;
39import java.util.TreeMap;
40import java.util.TreeSet;
41import java.util.stream.Collectors;
42import java.util.stream.Stream;
43import jdk.internal.misc.SharedSecrets;
44import jdk.internal.misc.JavaLangInvokeAccess;
45import jdk.tools.jlink.plugin.ResourcePoolEntry;
46import jdk.tools.jlink.plugin.PluginException;
47import jdk.tools.jlink.plugin.ResourcePool;
48import jdk.tools.jlink.plugin.ResourcePoolBuilder;
49import jdk.tools.jlink.plugin.Plugin;
50
51/**
52 * Plugin to generate java.lang.invoke classes.
53 */
54public 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}
524