Last active
November 4, 2019 18:40
-
-
Save JarvisCraft/53b6e5e4eb91419b6e852cf63e1c8a2f to your computer and use it in GitHub Desktop.
[OpenJDK] invokedynamic-based String-switch + base for future indy-based switch implementations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff -r 5573a7098439 src/java.base/share/classes/java/lang/invoke/SwitchTableFactory.java | |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 | |
+++ b/src/java.base/share/classes/java/lang/invoke/SwitchTableFactory.java Mon Nov 04 18:07:43 2019 +0300 | |
@@ -0,0 +1,580 @@ | |
+/* | |
+ * Copyright (c) 2008, 2019, Oracle and/or its affiliates. All rights reserved. | |
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
+ * | |
+ * This code is free software; you can redistribute it and/or modify it | |
+ * under the terms of the GNU General Public License version 2 only, as | |
+ * published by the Free Software Foundation. Oracle designates this | |
+ * particular file as subject to the "Classpath" exception as provided | |
+ * by Oracle in the LICENSE file that accompanied this code. | |
+ * | |
+ * This code is distributed in the hope that it will be useful, but WITHOUT | |
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
+ * version 2 for more details (a copy is included in the LICENSE file that | |
+ * accompanied this code). | |
+ * | |
+ * You should have received a copy of the GNU General Public License version | |
+ * 2 along with this work; if not, write to the Free Software Foundation, | |
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
+ * | |
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
+ * or visit www.oracle.com if you need additional information or have any | |
+ * questions. | |
+ */ | |
+ | |
+package java.lang.invoke; | |
+ | |
+import jdk.internal.misc.Unsafe; | |
+import jdk.internal.org.objectweb.asm.ClassWriter; | |
+import jdk.internal.org.objectweb.asm.Label; | |
+import jdk.internal.org.objectweb.asm.MethodVisitor; | |
+ | |
+import java.lang.invoke.MethodHandles.Lookup; | |
+import java.util.*; | |
+ | |
+import static jdk.internal.org.objectweb.asm.Opcodes.*; | |
+ | |
+/** | |
+ * <p>Methods used to facilitate effective switching by values whose relation to ints is not bijective.</p> | |
+ * | |
+ * @author Petr Portnov | |
+ * @since 14 | |
+ */ | |
+public class SwitchTableFactory { | |
+ | |
+ /** | |
+ * Radix used for integers with {@code alt}-variants of methods which rely on String dictionaries | |
+ * | |
+ * @implNote this value should never be changed once this class becomes part of the standard | |
+ * as it may lead to backwards-compatibility issues | |
+ */ | |
+ public static final int BRANCH_DICTIONARY_INTEGER_RADIX = 16; | |
+ | |
+ /** | |
+ * Instance of {@link Unsafe} used for internal purposes | |
+ */ | |
+ private static final Unsafe UNSAFE = Unsafe.getUnsafe(); | |
+ /** | |
+ * Instance of {@link Lookup} given full access | |
+ */ | |
+ private static final Lookup LOOKUP = Lookup.IMPL_LOOKUP; | |
+ | |
+ /** | |
+ * Version of generated class files | |
+ */ | |
+ private static final int CLASS_FILE_VERSION = V1_8; | |
+ /** | |
+ * Name used for generated methods for which the call-sites get created | |
+ */ | |
+ private static final String GENERATED_METHOD_NAME = "getId"; | |
+ | |
+ /** | |
+ * Method handle referencing {@link #constantId(int)} used for fast implementation of empty switches | |
+ */ | |
+ private static final MethodHandle CONSTANT_ID_METHOD_HANDLE; | |
+ | |
+ static { | |
+ // initialize `CONSTANT_ID_METHOD_HANDLE` | |
+ try { | |
+ CONSTANT_ID_METHOD_HANDLE = LOOKUP.findStatic( | |
+ SwitchTableFactory.class, "constantId", MethodType.methodType(int.class, int.class, String.class) | |
+ ); | |
+ } catch (final NoSuchMethodException | IllegalAccessException e) { | |
+ // this should never happen but the exception is still a checked one | |
+ // this may be rewritten once JEP 303 makes its way into production | |
+ throw new InternalError("Unable to create a method handle for own method `static int constantId(int)`"); | |
+ } | |
+ } | |
+ | |
+ private static String getClassName(final Class<?> hostClass, final String name) { | |
+ return hostClass.getName().replace('.', '/') + "$$SwitchTable$$" + name; | |
+ } | |
+ | |
+ private static void pushInt(final MethodVisitor method, final int value) { | |
+ switch (value) { | |
+ case -1: { | |
+ method.visitInsn(ICONST_M1); | |
+ return; | |
+ } | |
+ case 0: { | |
+ method.visitInsn(ICONST_0); | |
+ return; | |
+ } | |
+ case 1: { | |
+ method.visitInsn(ICONST_1); | |
+ return; | |
+ } | |
+ case 2: { | |
+ method.visitInsn(ICONST_2); | |
+ return; | |
+ } | |
+ case 3: { | |
+ method.visitInsn(ICONST_3); | |
+ return; | |
+ } | |
+ case 4: { | |
+ method.visitInsn(ICONST_4); | |
+ return; | |
+ } | |
+ case 5: { | |
+ method.visitInsn(ICONST_5); | |
+ return; | |
+ } | |
+ default: { | |
+ if (Byte.MAX_VALUE <= value && value <= Byte.MAX_VALUE) method.visitIntInsn(BIPUSH, value); | |
+ else if (Short.MAX_VALUE <= value && value <= Short.MAX_VALUE) method.visitIntInsn(SIPUSH, value); | |
+ else method.visitLdcInsn(value); | |
+ } | |
+ } | |
+ } | |
+ | |
+ private static int constantId(final int id, final String unused) { | |
+ return id; | |
+ } | |
+ | |
+ private static CallSite constantIdCallSite(final int defaultBranchId) { | |
+ return new ConstantCallSite(CONSTANT_ID_METHOD_HANDLE.rebind().bindArgumentI(0, defaultBranchId)); | |
+ } | |
+ | |
+ /** | |
+ * <p>Facilitates the creation of optimized {@link String}-switch table, | |
+ * whose values can later be used in {@code int}-switch. | |
+ * Typically used as <em>bootstrap method</em> for {@code invokedynamic} call sites, | |
+ * to support the <em>string switch</em> feature of the Java Programming Language.</p> | |
+ * <p>This method is an alternative to {@link #altCreateStringSwitchTable(Lookup, String, MethodType, Object...)} | |
+ * which is simpler but allows limited (max method argument slot size -specific) amount of values used.</p> | |
+ * | |
+ * <p>Effective signature is the following:</p> | |
+ * <pre> | |
+ * CallSite createStringSwitchTable(Lookup lookup, | |
+ * String name, | |
+ * MethodType type, | |
+ * int defaultBranchId, | |
+ * int labelCount, // 0 is allowed though this is useless | |
+ * // BEGIN repeated part | |
+ * int branchId, | |
+ * int count, | |
+ * String... labels, // `count` times | |
+ * // END repeated part | |
+ * ) | |
+ * </pre> | |
+ * | |
+ * @param lookup Lookup context with access privileges. | |
+ * @param name The name of the method to implement. | |
+ * @param type The expected signature of the {@link CallSite}. Normally it is {@code (String)I} | |
+ * @param bootstrapArguments special bootstrap arguments describing the dictionary used | |
+ * | |
+ * @return a CallSite whose target can be used to find the ID | |
+ * of the {@link String} passed to it as a dynamic parameter. | |
+ * | |
+ * @throws NullPointerException if {@code lookup} or {@code name} or {@code type} or {@code dictionary} | |
+ * or any of {@code dictionary}'s elements is {@code null} | |
+ * @throws SwitchTableFactoryException if any parameter is illegal | |
+ */ | |
+ public static CallSite createStringSwitchTable(final Lookup lookup, | |
+ final String name, | |
+ final MethodType type, | |
+ final Object... bootstrapArguments) | |
+ throws SwitchTableFactoryException { | |
+ // Check JVM-passed parameters not to have undefined behaviour if (somehow) incorrect values get passed | |
+ Objects.requireNonNull(lookup, "Lookup is null"); | |
+ Objects.requireNonNull(name, "Name is null"); | |
+ Objects.requireNonNull(type, "Type is null"); | |
+ | |
+ // check bootstrap arguments array general nullability | |
+ Objects.requireNonNull(bootstrapArguments, "Bootstrap arguments array is null"); | |
+ | |
+ // Validate type details | |
+ if (type.parameterCount() != 1 | |
+ || type.returnType() != int.class | |
+ || type.parameterType(0) != String.class) throw new SwitchTableFactoryException( | |
+ "Parameter type should be `(String)int`" | |
+ ); | |
+ | |
+ // Note: cast to primitives will also perform null-check | |
+ | |
+ final int defaultId = (int) bootstrapArguments[0]; | |
+ | |
+ final int labelCount = (int) bootstrapArguments[1]; | |
+ if (labelCount < 0) throw new SwitchTableFactoryException("Branch count cannot be negative"); | |
+ if (labelCount == 0 /* somehow */) return constantIdCallSite(defaultId); | |
+ | |
+ final StringSwitchNode[] nodes = new StringSwitchNode[labelCount]; | |
+ { // add nodes according to the dictionary | |
+ int readArgumentIndex = 1, nodeIndex = 0; | |
+ while (nodeIndex < labelCount) { | |
+ final int branchId = (int) bootstrapArguments[++readArgumentIndex]; | |
+ final int count = (int) bootstrapArguments[++readArgumentIndex]; | |
+ if (count < 0) throw new SwitchTableFactoryException( | |
+ "Count of branch by ID " + branchId + " cannot be negative" | |
+ ); | |
+ for (int i = 0; i < count; i++) nodes[nodeIndex++] = new StringSwitchNode( | |
+ branchId, | |
+ (String) Objects.requireNonNull(bootstrapArguments[++readArgumentIndex], "Branch label is null") | |
+ ); | |
+ } | |
+ } | |
+ | |
+ return generateStringSwitchTable(lookup, name, type, defaultId, nodes); | |
+ } | |
+ | |
+ /** | |
+ * <p>Facilitates the creation of optimized {@link String}-switch table, | |
+ * whose values can later be used in {@code int}-switch. | |
+ * Typically used as <em>bootstrap method</em> for {@code invokedynamic} call sites, | |
+ * to support the <em>string switch</em> feature of the Java Programming Language.</p> | |
+ * <p>This method is an alternative to {@link #createStringSwitchTable(Lookup, String, MethodType, Object...)} | |
+ * allowing practically unlimited amount of values used.</p> | |
+ * | |
+ * <p>Effective signature is the following:</p> | |
+ * <pre> | |
+ * CallSite altCreateStringSwitchTable(Lookup lookup, | |
+ * String name, | |
+ * MethodType type, | |
+ * int defaultBranchId, | |
+ * int labelCount, // 0 is allowed though this is useless | |
+ * int dictionarySize, // 0 is allowed though this is useless | |
+ * String... dictionaryPages, // `dictionarySize` times | |
+ * ) | |
+ * </pre> | |
+ * <p>Dictionary pages have the following format (all elements go not separated):</p> | |
+ * <dl> | |
+ * <dt>ID{@code \0}</dt> | |
+ * <dd>ID returned for the following <i>size</i> values</dd> | |
+ * | |
+ * <dt>size</dt> | |
+ * <dd>Amount of value-labels described in the block</dd> | |
+ * | |
+ * <dt>value_length (N times, each followed by its <i>value</i>)</dt> | |
+ * <dd>Length of the following <i>value</i></dd> | |
+ * | |
+ * <dt>value</dt> | |
+ * <dd>Value-label of length <i>value_length</i></dd> | |
+ * </dl> | |
+ * <p><em>Important</em>: all numeric values have the radix {@value #BRANCH_DICTIONARY_INTEGER_RADIX} | |
+ * available to the externals tools via constant {@link #BRANCH_DICTIONARY_INTEGER_RADIX}.</p> | |
+ * | |
+ * <p>For example {@code "0\0002\000\3\000Lol4\000ABCD1\0001\000\5\000Hello"} | |
+ * stands for 2 labels, {@code "lol"} and {@code "ABCD"}, to which ID {@code 0} corresponds | |
+ * and 1 label, {@code "Hello"}, to which ID {@code 1} corresponds.</p> | |
+ * | |
+ * @param lookup Lookup context with access privileges. | |
+ * @param name The name of the method to implement. | |
+ * @param type The expected signature of the {@link CallSite}. Normally it is {@code (String)I} | |
+ * @param bootstrapArguments special bootstrap arguments describing the dictionary used | |
+ * | |
+ * @return a CallSite whose target can be used to find the ID | |
+ * of the {@link String} passed to it as a dynamic parameter. | |
+ * | |
+ * @throws NullPointerException if {@code lookup} or {@code name} or {@code type} or {@code dictionary} | |
+ * or any of {@code dictionary}'s elements is {@code null} | |
+ * @throws SwitchTableFactoryException if any parameter is illegal | |
+ */ | |
+ public static CallSite altCreateStringSwitchTable(final Lookup lookup, | |
+ final String name, | |
+ final MethodType type, | |
+ final Object... bootstrapArguments) | |
+ throws SwitchTableFactoryException { | |
+ // Check JVM-passed parameters not to have undefined behaviour if (somehow) incorrect values get passed | |
+ Objects.requireNonNull(lookup, "Lookup is null"); | |
+ Objects.requireNonNull(name, "Name is null"); | |
+ Objects.requireNonNull(type, "Type is null"); | |
+ | |
+ // check bootstrap arguments array general nullability | |
+ Objects.requireNonNull(bootstrapArguments, "Bootstrap arguments array is null"); | |
+ | |
+ // Validate type details | |
+ if (type.parameterCount() != 1 | |
+ || type.returnType() != int.class | |
+ || type.parameterType(0) != String.class) throw new SwitchTableFactoryException( | |
+ "Parameter type should be `(String)int`" | |
+ ); | |
+ | |
+ // Note: cast to primitives will also perform null-check | |
+ | |
+ final int defaultId = (int) bootstrapArguments[0]; | |
+ | |
+ final int labelCount = (int) bootstrapArguments[1]; | |
+ if (labelCount < 0) throw new SwitchTableFactoryException("Label count cannot be negative"); | |
+ if (labelCount == 0 /* somehow */) return constantIdCallSite(defaultId); | |
+ | |
+ final int dictionarySize = (int) bootstrapArguments[2]; | |
+ if (dictionarySize < 0) throw new SwitchTableFactoryException("Dictionary count cannot be negative"); | |
+ // the following just checks for an absurd case which may in rare cases help the user | |
+ if (dictionarySize > labelCount) return constantIdCallSite(defaultId); | |
+ | |
+ final StringSwitchNode[] nodes = new StringSwitchNode[labelCount]; | |
+ | |
+ { | |
+ int nodeId = -1; | |
+ for (int dictionaryPageIndex = 0; dictionaryPageIndex < dictionarySize; dictionaryPageIndex++) { | |
+ final String page = (String) Objects.requireNonNull( | |
+ bootstrapArguments[dictionaryPageIndex + 3 /* amount of previously read arguments */], | |
+ "Dictionary page is null" | |
+ ); | |
+ | |
+ int startIndex = 0, delimiterIndex; | |
+ while ((delimiterIndex = page.indexOf('\0', startIndex)) != -1) { | |
+ final int id = Integer.parseInt(page, startIndex, delimiterIndex, BRANCH_DICTIONARY_INTEGER_RADIX); | |
+ | |
+ startIndex = delimiterIndex + 1; | |
+ delimiterIndex = page.indexOf('\0', startIndex); | |
+ if (delimiterIndex == -1) throw new SwitchTableFactoryException( | |
+ "Missing amount of labels for branch " + id + " in dictionary page #" + dictionaryPageIndex | |
+ ); | |
+ | |
+ final int count = Integer.parseInt( | |
+ page, startIndex, delimiterIndex, BRANCH_DICTIONARY_INTEGER_RADIX | |
+ ); | |
+ | |
+ startIndex = delimiterIndex + 1; | |
+ | |
+ for (int elementIndex = 0; elementIndex < count; elementIndex++) { | |
+ delimiterIndex = page.indexOf('\0', startIndex); | |
+ final int elementLength = Integer.parseInt( | |
+ page, startIndex, delimiterIndex, BRANCH_DICTIONARY_INTEGER_RADIX | |
+ ); | |
+ | |
+ nodes[++nodeId] = new StringSwitchNode(id, page.substring( | |
+ delimiterIndex + 1, startIndex = delimiterIndex + 1 + elementLength | |
+ )); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ return generateStringSwitchTable(lookup, name, type, defaultId, nodes); | |
+ } | |
+ | |
+ // Note: this method does not perform any validations | |
+ private static CallSite generateStringSwitchTable(final Lookup lookup, | |
+ /* var */ String name, | |
+ final MethodType type, | |
+ final int defaultId, | |
+ final StringSwitchNode[] nodes) | |
+ throws SwitchTableFactoryException { | |
+ /* | |
+ * Here the black magic begins with class generation. | |
+ * | |
+ * Lasciate ogni speranza, voi ch'entrate | |
+ */ | |
+ | |
+ /* ********************************************************************************************************** */ | |
+ | |
+ // Frames and Maximums are computes manually as everything here is predictable | |
+ final ClassWriter clazz = new ClassWriter(0); | |
+ // get the caller class (needed for generating an informative class-name and | |
+ final Class<?> hostClass = lookup.lookupClass(); | |
+ name = getClassName(hostClass, name); | |
+ // initiate class file generation using `java.lang.Object` as its super-class | |
+ clazz.visit( | |
+ CLASS_FILE_VERSION, ACC_SUPER | ACC_PUBLIC | ACC_FINAL | ACC_SYNTHETIC, | |
+ name, null, "java/lang/Object", null | |
+ ); | |
+ | |
+ // start the visit of the class | |
+ | |
+ // Note: there is no need for the constructor to be present in the generated class as it can't be instantiated | |
+ | |
+ { | |
+ // start visiting the method being generated | |
+ final MethodVisitor method = clazz.visitMethod( | |
+ ACC_PUBLIC | ACC_STATIC | ACC_FINAL, GENERATED_METHOD_NAME, "(Ljava/lang/String;)I", null, null | |
+ ); | |
+ // modify method's bytecode | |
+ method.visitCode(); | |
+ | |
+ // label corresponding to no match found for the `String` | |
+ final Label defaultLabel = new Label(); | |
+ if (nodes.length == 1) { | |
+ final StringSwitchNode node = nodes[0]; | |
+ // push the `String` checked | |
+ method.visitVarInsn(ALOAD, 0); | |
+ // push the `String` to compare the value with | |
+ method.visitLdcInsn(node.string); | |
+ // invoke `equals(Object)` method | |
+ method.visitMethodInsn( | |
+ INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false | |
+ ); | |
+ // jump to non-equality label if `equals(Object)` returns false | |
+ method.visitJumpInsn(IFEQ, defaultLabel); | |
+ // return ID of the case when the strings are equal | |
+ pushInt(method, node.id); | |
+ method.visitInsn(IRETURN); | |
+ } else { | |
+ // push the String onto the stack | |
+ method.visitVarInsn(ALOAD, 0); | |
+ method.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "hashCode", "()I", false); | |
+ | |
+ int nonDefaultBranchCount = 0; | |
+ final NavigableMap<Integer, List<StringSwitchNode>> branches = new TreeMap<>(); | |
+ // put the nodes into the map | |
+ for (final StringSwitchNode node : nodes) branches | |
+ .computeIfAbsent(node.stringHashCode, ArrayList::new).add(node); | |
+ | |
+ final int branchCount = branches.size(); | |
+ final Label[] labels = new Label[branchCount]; | |
+ { | |
+ final int[] hashCodes = new int[branchCount]; | |
+ | |
+ { // fill labels and hashCodes | |
+ int i = -1; | |
+ for (final Integer hashCode : branches.keySet()) { | |
+ labels[++i] = new Label(); | |
+ hashCodes[i] = hashCode; | |
+ } | |
+ } | |
+ | |
+ | |
+ // Lookup is used because the chance of ints being close is extremely small | |
+ method.visitLookupSwitchInsn(defaultLabel, hashCodes, labels); | |
+ } | |
+ | |
+ // now add all switch branches | |
+ { | |
+ int labelId = -1; | |
+ // traverse through all groups of nodes with equal hash-codes | |
+ for (final List<StringSwitchNode> nodeGroup : branches.values()) { | |
+ // amount of nodes in current group | |
+ final int nodeGroupSize = nodeGroup.size(); | |
+ | |
+ // visit the label of the current switch-branch | |
+ method.visitLabel(labels[++labelId]); | |
+ method.visitFrame(F_SAME, 0, null, 0, null); | |
+ | |
+ StringSwitchNode node; | |
+ // if there is only one node then simply generate typical check for it | |
+ if (nodeGroupSize == 1) node = nodeGroup.get(0); | |
+ // otherwise generate a chain of if-else statements to reach the needed string, | |
+ // finally using typical check for the last one | |
+ else { | |
+ // index of the last node | |
+ final int lastNodeIndex = nodeGroupSize - 1; | |
+ // traverse through all nodes except for the last one | |
+ for (int i = 0; i < lastNodeIndex; i++) { | |
+ // label of the next node to be visited | |
+ final Label nextLabel = new Label(); | |
+ // get the current node | |
+ node = nodeGroup.get(i); | |
+ // push the `String` checked | |
+ method.visitVarInsn(ALOAD, 0); | |
+ // push the `String` to compare the value with | |
+ method.visitLdcInsn(node.string); | |
+ // invoke `equals(Object)` method | |
+ method.visitMethodInsn( | |
+ INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false | |
+ ); | |
+ // jump to next check label if `equals(Object)` returns false | |
+ method.visitJumpInsn(IFEQ, nextLabel); | |
+ // return ID of the case when the strings are equal | |
+ pushInt(method, node.id); | |
+ method.visitInsn(IRETURN); | |
+ | |
+ // start the visit of the next label | |
+ method.visitLabel(nextLabel); | |
+ } | |
+ | |
+ node = nodeGroup.get(lastNodeIndex); | |
+ } | |
+ // push the `String` checked | |
+ method.visitVarInsn(ALOAD, 0); | |
+ // push the `String` to compare the value with | |
+ method.visitLdcInsn(node.string); | |
+ // invoke `equals(Object)` method | |
+ method.visitMethodInsn( | |
+ INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false | |
+ ); | |
+ method.visitJumpInsn(IFEQ, defaultLabel); | |
+ pushInt(method, node.id); | |
+ method.visitInsn(IRETURN); | |
+ } | |
+ } | |
+ } | |
+ // and finally handle default label (the case in which no match was found for the `String`) | |
+ method.visitLabel(defaultLabel); | |
+ method.visitFrame(F_SAME, 0, null, 0, null); | |
+ // returning default ID | |
+ pushInt(method, defaultId); | |
+ method.visitInsn(IRETURN); | |
+ | |
+ method.visitMaxs(2 /* worst: two compared Strings */, 1 /* no local variables except fot the parameter */); | |
+ | |
+ // end method modification | |
+ method.visitEnd(); | |
+ } | |
+ | |
+ // end class modification | |
+ clazz.visitEnd(); | |
+ | |
+ /* ********************************************************************************************************** */ | |
+ | |
+ /* | |
+ * Now produce the result of all this black magic | |
+ */ | |
+ | |
+ final MethodHandle methodHandle; | |
+ { // class loading and method handle lookup | |
+ // Define the created class and make sure that it was initialized | |
+ final Class<?> generatedClass = UNSAFE.defineAnonymousClass(hostClass, clazz.toByteArray(), null); | |
+ UNSAFE.ensureClassInitialized(generatedClass); | |
+ | |
+ // Find the method handle referencing the method | |
+ try { | |
+ methodHandle = LOOKUP.findStatic(generatedClass, GENERATED_METHOD_NAME, type); | |
+ } catch (final NoSuchMethodException | IllegalAccessException e) { | |
+ throw new SwitchTableFactoryException( | |
+ "Could not find the generated method in the generated class (somehow)", e | |
+ ); | |
+ } | |
+ } | |
+ | |
+ // And finally return a constant call site of the method handle | |
+ return new ConstantCallSite(methodHandle); | |
+ } | |
+ | |
+ // no need for getters as the class is for internal use only | |
+ private static final class StringSwitchNode implements Comparable<StringSwitchNode> { | |
+ private final int id; | |
+ private final /* effectively non-null*/ String string; | |
+ private final int stringHashCode; | |
+ private final int hashCode; | |
+ | |
+ private StringSwitchNode(/* effectively non-null*/ final int id, final String string) { | |
+ this.id = id; | |
+ this.string = string; | |
+ stringHashCode = string.hashCode(); | |
+ hashCode = string.hashCode() * 31 + id; | |
+ } | |
+ | |
+ @Override | |
+ public boolean equals(final Object other) { | |
+ if (this == other) return true; | |
+ if (other == null || other.getClass() != StringSwitchNode.class) return false; | |
+ final StringSwitchNode otherNode = (StringSwitchNode) other; | |
+ return hashCode == otherNode.hashCode // fast check (also contract guarantee) | |
+ && id == otherNode.id && string.equals(otherNode.string); | |
+ } | |
+ | |
+ @Override | |
+ public int hashCode() { | |
+ return hashCode; | |
+ } | |
+ | |
+ @Override | |
+ public int compareTo(final StringSwitchNode other) { | |
+ return Integer.compare(this.stringHashCode, other.stringHashCode); | |
+ } | |
+ | |
+ /* This one is here for debug purposes */ | |
+ @Override | |
+ public String toString() { | |
+ return "StringSwitchNode{" + | |
+ "id=" + id + | |
+ ", string='" + string + '\'' + | |
+ ", stringHashCode=" + stringHashCode + | |
+ ", hashCode=" + hashCode + | |
+ '}'; | |
+ } | |
+ } | |
+} | |
diff -r 5573a7098439 src/java.base/share/classes/java/lang/invoke/SwitchTableFactoryException.java | |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 | |
+++ b/src/java.base/share/classes/java/lang/invoke/SwitchTableFactoryException.java Mon Nov 04 18:07:43 2019 +0300 | |
@@ -0,0 +1,37 @@ | |
+package java.lang.invoke; | |
+ | |
+/** | |
+ * An exception thrown whenever {@link SwitchTableFactory} is unable to do its work | |
+ * (in most cases, due to incorrect parameters passed). | |
+ * | |
+ * @author Petr Portnov | |
+ * | |
+ * @since 14 | |
+ */ | |
+public class SwitchTableFactoryException extends Exception { | |
+ | |
+ /** | |
+ * Serial version unique ID (as required by {@link java.io.Serializable} specification) | |
+ */ | |
+ @java.io.Serial | |
+ private static final long serialVersionUID = 292L + 10L; | |
+ | |
+ /** | |
+ * Constructs a new exception with a message describing the problem. | |
+ * | |
+ * @param message message describing the problem | |
+ */ | |
+ public SwitchTableFactoryException(final String message) { | |
+ super(message); | |
+ } | |
+ | |
+ /** | |
+ * Constructs a new exception with a message describing the problem and a cause of this exception. | |
+ * | |
+ * @param message message describing the problem | |
+ * @param cause cause of this exception | |
+ */ | |
+ public SwitchTableFactoryException(final String message, final Throwable cause) { | |
+ super(message, cause); | |
+ } | |
+} | |
diff -r 5573a7098439 test/jdk/java/lang/invoke/SwitchTableFactory/BasicTest.java | |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 | |
+++ b/test/jdk/java/lang/invoke/SwitchTableFactory/BasicTest.java Mon Nov 04 18:07:43 2019 +0300 | |
@@ -0,0 +1,356 @@ | |
+/* | |
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. | |
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
+ * | |
+ * This code is free software; you can redistribute it and/or modify it | |
+ * under the terms of the GNU General Public License version 2 only, as | |
+ * published by the Free Software Foundation. | |
+ * | |
+ * This code is distributed in the hope that it will be useful, but WITHOUT | |
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
+ * version 2 for more details (a copy is included in the LICENSE file that | |
+ * accompanied this code). | |
+ * | |
+ * You should have received a copy of the GNU General Public License version | |
+ * 2 along with this work; if not, write to the Free Software Foundation, | |
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
+ * | |
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
+ * or visit www.oracle.com if you need additional information or have any | |
+ * questions. | |
+ */ | |
+ | |
+import java.lang.invoke.MethodHandle; | |
+import java.lang.invoke.MethodHandles; | |
+import java.lang.invoke.MethodType; | |
+import java.lang.invoke.SwitchTableFactory; | |
+import java.lang.invoke.SwitchTableFactoryException; | |
+ | |
+/** | |
+ * This test simply tests logical behaviour of {@link SwitchTableFactory}. | |
+ */ | |
+public class BasicTest { | |
+ | |
+ public static void main(final String[] args) { | |
+ testPreconditionsOfCreateStringSwitchTableTest(); | |
+ testCreateStringSwitchTableTest(); | |
+ testPreconditionsOfAltCreateStringSwitchTableTest(); | |
+ testAltCreateStringSwitchTableTest(); | |
+ testCornerCase0Branches(); | |
+ testCornerCase1Branch(); | |
+ } | |
+ | |
+ static void assertSame(final int expected, final int actual) { | |
+ if (expected != actual) throw new AssertionError("Expected " + expected + " but found " + actual); | |
+ } | |
+ | |
+ static void assertThrows(final Class<? extends Throwable> exceptionType, | |
+ final TestRunnable task) { | |
+ try { | |
+ task.run(); | |
+ throw new AssertionError("Exception of type " + exceptionType + " was not thrown"); | |
+ } catch (final Throwable x) { | |
+ if (!exceptionType.isAssignableFrom(x.getClass())) throw new AssertionError( | |
+ "Thwown exception " + x + " is not of type " + exceptionType | |
+ ); | |
+ } | |
+ } | |
+ | |
+ static void testPreconditionsOfCreateStringSwitchTableTest() { | |
+ // invokedynamic universal parameters | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ null, | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 1, | |
+ 1, 1, "a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ null, | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 1, | |
+ 1, 1, "a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ null, | |
+ -1, 1, | |
+ 1, 1, "a" | |
+ )); | |
+ // bootstrap arguments array | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ (Object[]) null | |
+ )); | |
+ // bootstrap arguments | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ null, 1, | |
+ 1, 1, "a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, null, | |
+ 1, 1, "a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 1, | |
+ null, 1, "a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 1, | |
+ 1, null, "a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 1, | |
+ 1, 1, null | |
+ )); | |
+ // type validation | |
+ assertThrows(SwitchTableFactoryException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(Object.class, String.class), | |
+ -1, 1, | |
+ 1, 1, "a" | |
+ )); | |
+ assertThrows(SwitchTableFactoryException.class, () -> SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, Object.class), | |
+ -1, 1, | |
+ 1, 1, "a" | |
+ )); | |
+ } | |
+ | |
+ static void testCreateStringSwitchTableTest() { | |
+ final MethodHandle methodHandle; | |
+ try { | |
+ methodHandle = SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "somename", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 5, | |
+ 0, 2, "Hello", "World", | |
+ 1, 3, "One", "Two", "Three" | |
+ ).getTarget(); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while bootstrapping", e); | |
+ } | |
+ | |
+ try { | |
+ assertSame(0, (int) methodHandle.invokeExact("Hello")); | |
+ assertSame(0, (int) methodHandle.invokeExact("World")); | |
+ assertSame(1, (int) methodHandle.invokeExact("One")); | |
+ assertSame(1, (int) methodHandle.invokeExact("Two")); | |
+ assertSame(1, (int) methodHandle.invokeExact("Three")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Something other")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Nope")); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while testing", e); | |
+ } | |
+ } | |
+ | |
+ static void testPreconditionsOfAltCreateStringSwitchTableTest() { | |
+ // invokedynamic universal parameters | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ null, | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -2, 1, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ null, | |
+ MethodType.methodType(int.class, String.class), | |
+ -2, 1, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ null, | |
+ -2, 1, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ // bootstrap arguments array | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ (Object[]) null | |
+ )); | |
+ // bootstrap arguments | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ null, 1, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -2, null, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -2, 1, | |
+ null, "1\0001\0001\000a" | |
+ )); | |
+ assertThrows(NullPointerException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, String.class), | |
+ -2, 1, | |
+ 1, null | |
+ )); | |
+ // type validation | |
+ assertThrows(SwitchTableFactoryException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(Object.class, String.class), | |
+ -2, 1, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ assertThrows(SwitchTableFactoryException.class, () -> SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "foo", | |
+ MethodType.methodType(int.class, Object.class), | |
+ -2, 1, | |
+ 1, "1\0001\0001\000a" | |
+ )); | |
+ } | |
+ | |
+ static void testAltCreateStringSwitchTableTest() { | |
+ final MethodHandle methodHandle; | |
+ try { | |
+ methodHandle = SwitchTableFactory.altCreateStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "othername", | |
+ MethodType.methodType(int.class, String.class), | |
+ -10, 5, 2, | |
+ "12\0002\000" | |
+ + "3\000Lol" | |
+ + "3\000Lul", | |
+ "34\0003\000" | |
+ + "4\000abcd" | |
+ + "3\000omg" | |
+ + "5\000qwert" | |
+ ).getTarget(); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while bootstrapping", e); | |
+ } | |
+ | |
+ try { | |
+ assertSame(-10, (int) methodHandle.invokeExact("wait")); | |
+ assertSame(-10, (int) methodHandle.invokeExact("is")); | |
+ assertSame(0x12, (int) methodHandle.invokeExact("Lol")); | |
+ assertSame(0x12, (int) methodHandle.invokeExact("Lul")); | |
+ assertSame(0x34, (int) methodHandle.invokeExact("abcd")); | |
+ assertSame(0x34, (int) methodHandle.invokeExact("omg")); | |
+ assertSame(0x34, (int) methodHandle.invokeExact("qwert")); | |
+ assertSame(-10, (int) methodHandle.invokeExact("this")); | |
+ assertSame(-10, (int) methodHandle.invokeExact("even legal")); | |
+ assertSame(-10, (int) methodHandle.invokeExact("?")); | |
+ assertSame(-10, (int) methodHandle.invokeExact("")); | |
+ assertSame(-10, (int) methodHandle.invokeExact("mb")); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while testing", e); | |
+ } | |
+ } | |
+ | |
+ // Note: there is no need to test both factories here as they differ only from point of passing arguments | |
+ static void testCornerCase0Branches() { | |
+ { // 0 branches | |
+ final MethodHandle methodHandle; | |
+ try { | |
+ methodHandle = SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "somename", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 0 | |
+ ).getTarget(); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while bootstrapping", e); | |
+ } | |
+ | |
+ try { | |
+ assertSame(-1, (int) methodHandle.invokeExact("Hello")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("World")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("One")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Two")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Three")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Something other")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Nope")); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while testing", e); | |
+ } | |
+ } | |
+ } | |
+ | |
+ // Note: there is no need to test both factories here as they differ only from point of passing arguments | |
+ // Note: this case is tested because it is logical to treat it specially | |
+ static void testCornerCase1Branch() { | |
+ { // 0 branches | |
+ final MethodHandle methodHandle; | |
+ try { | |
+ methodHandle = SwitchTableFactory.createStringSwitchTable( | |
+ MethodHandles.lookup(), | |
+ "somename", | |
+ MethodType.methodType(int.class, String.class), | |
+ -1, 1, | |
+ 100, 1, "foo" | |
+ ).getTarget(); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while bootstrapping", e); | |
+ } | |
+ | |
+ try { | |
+ assertSame(-1, (int) methodHandle.invokeExact("Hello")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("World")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("One")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Two")); | |
+ assertSame(100, (int) methodHandle.invokeExact("foo")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Three")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Something other")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Nope")); | |
+ assertSame(100, (int) methodHandle.invokeExact("foo")); | |
+ assertSame(-1, (int) methodHandle.invokeExact("Nope")); | |
+ assertSame(100, (int) methodHandle.invokeExact("foo")); | |
+ } catch (final Throwable e) { | |
+ throw new AssertionError("An exception was thrown while testing", e); | |
+ } | |
+ } | |
+ } | |
+ | |
+ @FunctionalInterface | |
+ interface TestRunnable { | |
+ void run() throws Throwable; | |
+ } | |
+} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment