1904 lines
80 KiB
Java
1904 lines
80 KiB
Java
/*
|
|
* Copyright (c) 2015, 2020, 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.access.JavaLangAccess;
|
|
import jdk.internal.access.SharedSecrets;
|
|
import jdk.internal.misc.Unsafe;
|
|
import jdk.internal.misc.VM;
|
|
import jdk.internal.org.objectweb.asm.ClassWriter;
|
|
import jdk.internal.org.objectweb.asm.Label;
|
|
import jdk.internal.org.objectweb.asm.MethodVisitor;
|
|
import jdk.internal.org.objectweb.asm.Opcodes;
|
|
import jdk.internal.vm.annotation.Stable;
|
|
import sun.invoke.util.Wrapper;
|
|
|
|
import java.lang.invoke.MethodHandles.Lookup;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
import java.util.function.Function;
|
|
|
|
import static java.lang.invoke.MethodHandles.lookup;
|
|
import static java.lang.invoke.MethodType.methodType;
|
|
import static java.lang.invoke.MethodHandles.Lookup.ClassOption.*;
|
|
import static jdk.internal.org.objectweb.asm.Opcodes.*;
|
|
|
|
/**
|
|
* <p>Methods to facilitate the creation of String concatenation methods, that
|
|
* can be used to efficiently concatenate a known number of arguments of known
|
|
* types, possibly after type adaptation and partial evaluation of arguments.
|
|
* These methods are typically used as <em>bootstrap methods</em> for {@code
|
|
* invokedynamic} call sites, to support the <em>string concatenation</em>
|
|
* feature of the Java Programming Language.
|
|
*
|
|
* <p>Indirect access to the behavior specified by the provided {@code
|
|
* MethodHandle} proceeds in order through two phases:
|
|
*
|
|
* <ol>
|
|
* <li><em>Linkage</em> occurs when the methods in this class are invoked.
|
|
* They take as arguments a method type describing the concatenated arguments
|
|
* count and types, and optionally the String <em>recipe</em>, plus the
|
|
* constants that participate in the String concatenation. The details on
|
|
* accepted recipe shapes are described further below. Linkage may involve
|
|
* dynamically loading a new class that implements the expected concatenation
|
|
* behavior. The {@code CallSite} holds the {@code MethodHandle} pointing to the
|
|
* exact concatenation method. The concatenation methods may be shared among
|
|
* different {@code CallSite}s, e.g. if linkage methods produce them as pure
|
|
* functions.</li>
|
|
*
|
|
* <li><em>Invocation</em> occurs when a generated concatenation method is
|
|
* invoked with the exact dynamic arguments. This may occur many times for a
|
|
* single concatenation method. The method referenced by the behavior {@code
|
|
* MethodHandle} is invoked with the static arguments and any additional dynamic
|
|
* arguments provided on invocation, as if by {@link MethodHandle#invoke(Object...)}.</li>
|
|
* </ol>
|
|
*
|
|
* <p> This class provides two forms of linkage methods: a simple version
|
|
* ({@link #makeConcat(java.lang.invoke.MethodHandles.Lookup, String,
|
|
* MethodType)}) using only the dynamic arguments, and an advanced version
|
|
* ({@link #makeConcatWithConstants(java.lang.invoke.MethodHandles.Lookup,
|
|
* String, MethodType, String, Object...)} using the advanced forms of capturing
|
|
* the constant arguments. The advanced strategy can produce marginally better
|
|
* invocation bytecode, at the expense of exploding the number of shapes of
|
|
* string concatenation methods present at runtime, because those shapes would
|
|
* include constant static arguments as well.
|
|
*
|
|
* @author Aleksey Shipilev
|
|
* @author Remi Forax
|
|
* @author Peter Levart
|
|
*
|
|
* @apiNote
|
|
* <p>There is a JVM limit (classfile structural constraint): no method
|
|
* can call with more than 255 slots. This limits the number of static and
|
|
* dynamic arguments one can pass to bootstrap method. Since there are potential
|
|
* concatenation strategies that use {@code MethodHandle} combinators, we need
|
|
* to reserve a few empty slots on the parameter lists to capture the
|
|
* temporal results. This is why bootstrap methods in this factory do not accept
|
|
* more than 200 argument slots. Users requiring more than 200 argument slots in
|
|
* concatenation are expected to split the large concatenation in smaller
|
|
* expressions.
|
|
*
|
|
* @since 9
|
|
*/
|
|
public final class StringConcatFactory {
|
|
|
|
/**
|
|
* Tag used to demarcate an ordinary argument.
|
|
*/
|
|
private static final char TAG_ARG = '\u0001';
|
|
|
|
/**
|
|
* Tag used to demarcate a constant.
|
|
*/
|
|
private static final char TAG_CONST = '\u0002';
|
|
|
|
/**
|
|
* Maximum number of argument slots in String Concat call.
|
|
*
|
|
* While the maximum number of argument slots that indy call can handle is 253,
|
|
* we do not use all those slots, to let the strategies with MethodHandle
|
|
* combinators to use some arguments.
|
|
*/
|
|
private static final int MAX_INDY_CONCAT_ARG_SLOTS = 200;
|
|
|
|
/**
|
|
* Concatenation strategy to use. See {@link Strategy} for possible options.
|
|
* This option is controllable with -Djava.lang.invoke.stringConcat JDK option.
|
|
*
|
|
* Defaults to MH_INLINE_SIZED_EXACT if not set.
|
|
*/
|
|
private static final Strategy STRATEGY;
|
|
|
|
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
|
|
|
|
private enum Strategy {
|
|
/**
|
|
* Bytecode generator, calling into {@link java.lang.StringBuilder}.
|
|
*/
|
|
BC_SB,
|
|
|
|
/**
|
|
* Bytecode generator, calling into {@link java.lang.StringBuilder};
|
|
* but trying to estimate the required storage.
|
|
*/
|
|
BC_SB_SIZED,
|
|
|
|
/**
|
|
* Bytecode generator, calling into {@link java.lang.StringBuilder};
|
|
* but computing the required storage exactly.
|
|
*/
|
|
BC_SB_SIZED_EXACT,
|
|
|
|
/**
|
|
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
|
|
* This strategy also tries to estimate the required storage.
|
|
*/
|
|
MH_SB_SIZED,
|
|
|
|
/**
|
|
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
|
|
* This strategy also estimate the required storage exactly.
|
|
*/
|
|
MH_SB_SIZED_EXACT,
|
|
|
|
/**
|
|
* MethodHandle-based generator, that constructs its own byte[] array from
|
|
* the arguments. It computes the required storage exactly.
|
|
*/
|
|
MH_INLINE_SIZED_EXACT
|
|
}
|
|
|
|
/**
|
|
* Enables debugging: this may print debugging messages, perform additional (non-neutral for performance)
|
|
* checks, etc.
|
|
*/
|
|
private static final boolean DEBUG;
|
|
|
|
static {
|
|
final String strategy =
|
|
VM.getSavedProperty("java.lang.invoke.stringConcat");
|
|
STRATEGY = (strategy == null) ? null : Strategy.valueOf(strategy);
|
|
|
|
if (STRATEGY == null || STRATEGY == Strategy.MH_INLINE_SIZED_EXACT) {
|
|
// Force initialization of default strategy:
|
|
Unsafe.getUnsafe().ensureClassInitialized(MethodHandleInlineCopyStrategy.class);
|
|
}
|
|
|
|
DEBUG = Boolean.parseBoolean(
|
|
VM.getSavedProperty("java.lang.invoke.stringConcat.debug"));
|
|
}
|
|
|
|
/**
|
|
* Cache key is a composite of:
|
|
* - class name, that lets to disambiguate stubs, to avoid excess sharing
|
|
* - method type, describing the dynamic arguments for concatenation
|
|
* - concat recipe, describing the constants and concat shape
|
|
*/
|
|
private static final class Key {
|
|
final String className;
|
|
final MethodType mt;
|
|
final Recipe recipe;
|
|
|
|
public Key(String className, MethodType mt, Recipe recipe) {
|
|
this.className = className;
|
|
this.mt = mt;
|
|
this.recipe = recipe;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
|
|
Key key = (Key) o;
|
|
|
|
if (!className.equals(key.className)) return false;
|
|
if (!mt.equals(key.mt)) return false;
|
|
if (!recipe.equals(key.recipe)) return false;
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
int result = className.hashCode();
|
|
result = 31 * result + mt.hashCode();
|
|
result = 31 * result + recipe.hashCode();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the recipe string, and produces a traversable collection of
|
|
* {@link java.lang.invoke.StringConcatFactory.RecipeElement}-s for generator
|
|
* strategies. Notably, this class parses out the constants from the recipe
|
|
* and from other static arguments.
|
|
*/
|
|
private static final class Recipe {
|
|
private final List<RecipeElement> elements;
|
|
|
|
public Recipe(String src, Object[] constants) {
|
|
List<RecipeElement> el = new ArrayList<>();
|
|
|
|
int constC = 0;
|
|
int argC = 0;
|
|
|
|
StringBuilder acc = new StringBuilder();
|
|
|
|
for (int i = 0; i < src.length(); i++) {
|
|
char c = src.charAt(i);
|
|
|
|
if (c == TAG_CONST || c == TAG_ARG) {
|
|
// Detected a special tag, flush all accumulated characters
|
|
// as a constant first:
|
|
if (acc.length() > 0) {
|
|
el.add(new RecipeElement(acc.toString()));
|
|
acc.setLength(0);
|
|
}
|
|
if (c == TAG_CONST) {
|
|
Object cnst = constants[constC++];
|
|
el.add(new RecipeElement(cnst));
|
|
} else if (c == TAG_ARG) {
|
|
el.add(new RecipeElement(argC++));
|
|
}
|
|
} else {
|
|
// Not a special character, this is a constant embedded into
|
|
// the recipe itself.
|
|
acc.append(c);
|
|
}
|
|
}
|
|
|
|
// Flush the remaining characters as constant:
|
|
if (acc.length() > 0) {
|
|
el.add(new RecipeElement(acc.toString()));
|
|
}
|
|
|
|
elements = el;
|
|
}
|
|
|
|
public List<RecipeElement> getElements() {
|
|
return elements;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
|
|
Recipe recipe = (Recipe) o;
|
|
return elements.equals(recipe.elements);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Recipe{" +
|
|
"elements=" + elements +
|
|
'}';
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return elements.hashCode();
|
|
}
|
|
}
|
|
|
|
private static final class RecipeElement {
|
|
private final String value;
|
|
private final int argPos;
|
|
private final char tag;
|
|
|
|
public RecipeElement(Object cnst) {
|
|
this.value = String.valueOf(Objects.requireNonNull(cnst));
|
|
this.argPos = -1;
|
|
this.tag = TAG_CONST;
|
|
}
|
|
|
|
public RecipeElement(int arg) {
|
|
this.value = null;
|
|
this.argPos = arg;
|
|
this.tag = TAG_ARG;
|
|
}
|
|
|
|
public String getValue() {
|
|
assert (tag == TAG_CONST);
|
|
return value;
|
|
}
|
|
|
|
public int getArgPos() {
|
|
assert (tag == TAG_ARG);
|
|
return argPos;
|
|
}
|
|
|
|
public char getTag() {
|
|
return tag;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
|
|
RecipeElement that = (RecipeElement) o;
|
|
|
|
if (this.tag != that.tag) return false;
|
|
if (this.tag == TAG_CONST && (!value.equals(that.value))) return false;
|
|
if (this.tag == TAG_ARG && (argPos != that.argPos)) return false;
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "RecipeElement{" +
|
|
"value='" + value + '\'' +
|
|
", argPos=" + argPos +
|
|
", tag=" + tag +
|
|
'}';
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return (int)tag;
|
|
}
|
|
}
|
|
|
|
// StringConcatFactory bootstrap methods are startup sensitive, and may be
|
|
// special cased in java.lang.invokeBootstrapMethodInvoker to ensure
|
|
// methods are invoked with exact type information to avoid generating
|
|
// code for runtime checks. Take care any changes or additions here are
|
|
// reflected there as appropriate.
|
|
|
|
/**
|
|
* Facilitates the creation of optimized String concatenation methods, that
|
|
* can be used to efficiently concatenate a known number of arguments of
|
|
* known types, possibly after type adaptation and partial evaluation of
|
|
* arguments. Typically used as a <em>bootstrap method</em> for {@code
|
|
* invokedynamic} call sites, to support the <em>string concatenation</em>
|
|
* feature of the Java Programming Language.
|
|
*
|
|
* <p>When the target of the {@code CallSite} returned from this method is
|
|
* invoked, it returns the result of String concatenation, taking all
|
|
* function arguments passed to the linkage method as inputs for
|
|
* concatenation. The target signature is given by {@code concatType}.
|
|
* For a target accepting:
|
|
* <ul>
|
|
* <li>zero inputs, concatenation results in an empty string;</li>
|
|
* <li>one input, concatenation results in the single
|
|
* input converted as per JLS 5.1.11 "String Conversion"; otherwise</li>
|
|
* <li>two or more inputs, the inputs are concatenated as per
|
|
* requirements stated in JLS 15.18.1 "String Concatenation Operator +".
|
|
* The inputs are converted as per JLS 5.1.11 "String Conversion",
|
|
* and combined from left to right.</li>
|
|
* </ul>
|
|
*
|
|
* <p>Assume the linkage arguments are as follows:
|
|
*
|
|
* <ul>
|
|
* <li>{@code concatType}, describing the {@code CallSite} signature</li>
|
|
* </ul>
|
|
*
|
|
* <p>Then the following linkage invariants must hold:
|
|
*
|
|
* <ul>
|
|
* <li>The number of parameter slots in {@code concatType} is
|
|
* less than or equal to 200</li>
|
|
* <li>The return type in {@code concatType} is assignable from {@link java.lang.String}</li>
|
|
* </ul>
|
|
*
|
|
* @param lookup Represents a lookup context with the accessibility
|
|
* privileges of the caller. Specifically, the lookup
|
|
* context must have
|
|
* {@linkplain MethodHandles.Lookup#hasFullPrivilegeAccess()
|
|
* full privilege access}.
|
|
* When used with {@code invokedynamic}, this is stacked
|
|
* automatically by the VM.
|
|
* @param name The name of the method to implement. This name is
|
|
* arbitrary, and has no meaning for this linkage method.
|
|
* When used with {@code invokedynamic}, this is provided by
|
|
* the {@code NameAndType} of the {@code InvokeDynamic}
|
|
* structure and is stacked automatically by the VM.
|
|
* @param concatType The expected signature of the {@code CallSite}. The
|
|
* parameter types represent the types of concatenation
|
|
* arguments; the return type is always assignable from {@link
|
|
* java.lang.String}. When used with {@code invokedynamic},
|
|
* this is provided by the {@code NameAndType} of the {@code
|
|
* InvokeDynamic} structure and is stacked automatically by
|
|
* the VM.
|
|
* @return a CallSite whose target can be used to perform String
|
|
* concatenation, with dynamic concatenation arguments described by the given
|
|
* {@code concatType}.
|
|
* @throws StringConcatException If any of the linkage invariants described
|
|
* here are violated, or the lookup context
|
|
* does not have private access privileges.
|
|
* @throws NullPointerException If any of the incoming arguments is null.
|
|
* This will never happen when a bootstrap method
|
|
* is called with invokedynamic.
|
|
*
|
|
* @jls 5.1.11 String Conversion
|
|
* @jls 15.18.1 String Concatenation Operator +
|
|
*/
|
|
public static CallSite makeConcat(MethodHandles.Lookup lookup,
|
|
String name,
|
|
MethodType concatType) throws StringConcatException {
|
|
if (DEBUG) {
|
|
System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType);
|
|
}
|
|
|
|
return doStringConcat(lookup, name, concatType, true, null);
|
|
}
|
|
|
|
/**
|
|
* Facilitates the creation of optimized String concatenation methods, that
|
|
* can be used to efficiently concatenate a known number of arguments of
|
|
* known types, possibly after type adaptation and partial evaluation of
|
|
* arguments. Typically used as a <em>bootstrap method</em> for {@code
|
|
* invokedynamic} call sites, to support the <em>string concatenation</em>
|
|
* feature of the Java Programming Language.
|
|
*
|
|
* <p>When the target of the {@code CallSite} returned from this method is
|
|
* invoked, it returns the result of String concatenation, taking all
|
|
* function arguments and constants passed to the linkage method as inputs for
|
|
* concatenation. The target signature is given by {@code concatType}, and
|
|
* does not include constants.
|
|
* For a target accepting:
|
|
* <ul>
|
|
* <li>zero inputs, concatenation results in an empty string;</li>
|
|
* <li>one input, concatenation results in the single
|
|
* input converted as per JLS 5.1.11 "String Conversion"; otherwise</li>
|
|
* <li>two or more inputs, the inputs are concatenated as per
|
|
* requirements stated in JLS 15.18.1 "String Concatenation Operator +".
|
|
* The inputs are converted as per JLS 5.1.11 "String Conversion",
|
|
* and combined from left to right.</li>
|
|
* </ul>
|
|
*
|
|
* <p>The concatenation <em>recipe</em> is a String description for the way to
|
|
* construct a concatenated String from the arguments and constants. The
|
|
* recipe is processed from left to right, and each character represents an
|
|
* input to concatenation. Recipe characters mean:
|
|
*
|
|
* <ul>
|
|
*
|
|
* <li><em>\1 (Unicode point 0001)</em>: an ordinary argument. This
|
|
* input is passed through dynamic argument, and is provided during the
|
|
* concatenation method invocation. This input can be null.</li>
|
|
*
|
|
* <li><em>\2 (Unicode point 0002):</em> a constant. This input passed
|
|
* through static bootstrap argument. This constant can be any value
|
|
* representable in constant pool. If necessary, the factory would call
|
|
* {@code toString} to perform a one-time String conversion.</li>
|
|
*
|
|
* <li><em>Any other char value:</em> a single character constant.</li>
|
|
* </ul>
|
|
*
|
|
* <p>Assume the linkage arguments are as follows:
|
|
*
|
|
* <ul>
|
|
* <li>{@code concatType}, describing the {@code CallSite} signature</li>
|
|
* <li>{@code recipe}, describing the String recipe</li>
|
|
* <li>{@code constants}, the vararg array of constants</li>
|
|
* </ul>
|
|
*
|
|
* <p>Then the following linkage invariants must hold:
|
|
*
|
|
* <ul>
|
|
* <li>The number of parameter slots in {@code concatType} is less than
|
|
* or equal to 200</li>
|
|
*
|
|
* <li>The parameter count in {@code concatType} is equal to number of \1 tags
|
|
* in {@code recipe}</li>
|
|
*
|
|
* <li>The return type in {@code concatType} is assignable
|
|
* from {@link java.lang.String}, and matches the return type of the
|
|
* returned {@link MethodHandle}</li>
|
|
*
|
|
* <li>The number of elements in {@code constants} is equal to number of \2
|
|
* tags in {@code recipe}</li>
|
|
* </ul>
|
|
*
|
|
* @param lookup Represents a lookup context with the accessibility
|
|
* privileges of the caller. Specifically, the lookup
|
|
* context must have
|
|
* {@linkplain MethodHandles.Lookup#hasFullPrivilegeAccess()
|
|
* full privilege access}.
|
|
* When used with {@code invokedynamic}, this is stacked
|
|
* automatically by the VM.
|
|
* @param name The name of the method to implement. This name is
|
|
* arbitrary, and has no meaning for this linkage method.
|
|
* When used with {@code invokedynamic}, this is provided
|
|
* by the {@code NameAndType} of the {@code InvokeDynamic}
|
|
* structure and is stacked automatically by the VM.
|
|
* @param concatType The expected signature of the {@code CallSite}. The
|
|
* parameter types represent the types of dynamic concatenation
|
|
* arguments; the return type is always assignable from {@link
|
|
* java.lang.String}. When used with {@code
|
|
* invokedynamic}, this is provided by the {@code
|
|
* NameAndType} of the {@code InvokeDynamic} structure and
|
|
* is stacked automatically by the VM.
|
|
* @param recipe Concatenation recipe, described above.
|
|
* @param constants A vararg parameter representing the constants passed to
|
|
* the linkage method.
|
|
* @return a CallSite whose target can be used to perform String
|
|
* concatenation, with dynamic concatenation arguments described by the given
|
|
* {@code concatType}.
|
|
* @throws StringConcatException If any of the linkage invariants described
|
|
* here are violated, or the lookup context
|
|
* does not have private access privileges.
|
|
* @throws NullPointerException If any of the incoming arguments is null, or
|
|
* any constant in {@code recipe} is null.
|
|
* This will never happen when a bootstrap method
|
|
* is called with invokedynamic.
|
|
* @apiNote Code generators have three distinct ways to process a constant
|
|
* string operand S in a string concatenation expression. First, S can be
|
|
* materialized as a reference (using ldc) and passed as an ordinary argument
|
|
* (recipe '\1'). Or, S can be stored in the constant pool and passed as a
|
|
* constant (recipe '\2') . Finally, if S contains neither of the recipe
|
|
* tag characters ('\1', '\2') then S can be interpolated into the recipe
|
|
* itself, causing its characters to be inserted into the result.
|
|
*
|
|
* @jls 5.1.11 String Conversion
|
|
* @jls 15.18.1 String Concatenation Operator +
|
|
*/
|
|
public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
|
|
String name,
|
|
MethodType concatType,
|
|
String recipe,
|
|
Object... constants) throws StringConcatException {
|
|
if (DEBUG) {
|
|
System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType + ", {" + recipe + "}, " + Arrays.toString(constants));
|
|
}
|
|
|
|
return doStringConcat(lookup, name, concatType, false, recipe, constants);
|
|
}
|
|
|
|
private static CallSite doStringConcat(MethodHandles.Lookup lookup,
|
|
String name,
|
|
MethodType concatType,
|
|
boolean generateRecipe,
|
|
String recipe,
|
|
Object... constants) throws StringConcatException {
|
|
Objects.requireNonNull(lookup, "Lookup is null");
|
|
Objects.requireNonNull(name, "Name is null");
|
|
Objects.requireNonNull(concatType, "Concat type is null");
|
|
Objects.requireNonNull(constants, "Constants are null");
|
|
|
|
for (Object o : constants) {
|
|
Objects.requireNonNull(o, "Cannot accept null constants");
|
|
}
|
|
|
|
if ((lookup.lookupModes() & MethodHandles.Lookup.PRIVATE) == 0) {
|
|
throw new StringConcatException("Invalid caller: " +
|
|
lookup.lookupClass().getName());
|
|
}
|
|
|
|
int cCount = 0;
|
|
int oCount = 0;
|
|
if (generateRecipe) {
|
|
// Mock the recipe to reuse the concat generator code
|
|
char[] value = new char[concatType.parameterCount()];
|
|
Arrays.fill(value, TAG_ARG);
|
|
recipe = new String(value);
|
|
oCount = concatType.parameterCount();
|
|
} else {
|
|
Objects.requireNonNull(recipe, "Recipe is null");
|
|
|
|
for (int i = 0; i < recipe.length(); i++) {
|
|
char c = recipe.charAt(i);
|
|
if (c == TAG_CONST) cCount++;
|
|
if (c == TAG_ARG) oCount++;
|
|
}
|
|
}
|
|
|
|
if (oCount != concatType.parameterCount()) {
|
|
throw new StringConcatException(
|
|
"Mismatched number of concat arguments: recipe wants " +
|
|
oCount +
|
|
" arguments, but signature provides " +
|
|
concatType.parameterCount());
|
|
}
|
|
|
|
if (cCount != constants.length) {
|
|
throw new StringConcatException(
|
|
"Mismatched number of concat constants: recipe wants " +
|
|
cCount +
|
|
" constants, but only " +
|
|
constants.length +
|
|
" are passed");
|
|
}
|
|
|
|
if (!concatType.returnType().isAssignableFrom(String.class)) {
|
|
throw new StringConcatException(
|
|
"The return type should be compatible with String, but it is " +
|
|
concatType.returnType());
|
|
}
|
|
|
|
if (concatType.parameterSlotCount() > MAX_INDY_CONCAT_ARG_SLOTS) {
|
|
throw new StringConcatException("Too many concat argument slots: " +
|
|
concatType.parameterSlotCount() +
|
|
", can only accept " +
|
|
MAX_INDY_CONCAT_ARG_SLOTS);
|
|
}
|
|
|
|
MethodType mt = adaptType(concatType);
|
|
Recipe rec = new Recipe(recipe, constants);
|
|
MethodHandle mh = generate(lookup, mt, rec);
|
|
return new ConstantCallSite(mh.asType(concatType));
|
|
}
|
|
|
|
/**
|
|
* Adapt method type to an API we are going to use.
|
|
*
|
|
* This strips the concrete classes from the signatures, thus preventing
|
|
* class leakage when we cache the concatenation stubs.
|
|
*
|
|
* @param args actual argument types
|
|
* @return argument types the strategy is going to use
|
|
*/
|
|
private static MethodType adaptType(MethodType args) {
|
|
Class<?>[] ptypes = null;
|
|
for (int i = 0; i < args.parameterCount(); i++) {
|
|
Class<?> ptype = args.parameterType(i);
|
|
if (!ptype.isPrimitive() &&
|
|
ptype != String.class &&
|
|
ptype != Object.class) { // truncate to Object
|
|
if (ptypes == null) {
|
|
ptypes = args.parameterArray();
|
|
}
|
|
ptypes[i] = Object.class;
|
|
}
|
|
// else other primitives or String or Object (unchanged)
|
|
}
|
|
return (ptypes != null)
|
|
? MethodType.methodType(args.returnType(), ptypes)
|
|
: args;
|
|
}
|
|
|
|
private static MethodHandle generate(Lookup lookup, MethodType mt, Recipe recipe) throws StringConcatException {
|
|
try {
|
|
if (STRATEGY == null) {
|
|
return MethodHandleInlineCopyStrategy.generate(mt, recipe);
|
|
}
|
|
switch (STRATEGY) {
|
|
case BC_SB:
|
|
return BytecodeStringBuilderStrategy.generate(lookup, mt, recipe, Mode.DEFAULT);
|
|
case BC_SB_SIZED:
|
|
return BytecodeStringBuilderStrategy.generate(lookup, mt, recipe, Mode.SIZED);
|
|
case BC_SB_SIZED_EXACT:
|
|
return BytecodeStringBuilderStrategy.generate(lookup, mt, recipe, Mode.SIZED_EXACT);
|
|
case MH_SB_SIZED:
|
|
return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED);
|
|
case MH_SB_SIZED_EXACT:
|
|
return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED_EXACT);
|
|
case MH_INLINE_SIZED_EXACT:
|
|
return MethodHandleInlineCopyStrategy.generate(mt, recipe);
|
|
default:
|
|
throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");
|
|
}
|
|
} catch (Error | StringConcatException e) {
|
|
// Pass through any error or existing StringConcatException
|
|
throw e;
|
|
} catch (Throwable t) {
|
|
throw new StringConcatException("Generator failed", t);
|
|
}
|
|
}
|
|
|
|
private enum Mode {
|
|
DEFAULT(false, false),
|
|
SIZED(true, false),
|
|
SIZED_EXACT(true, true);
|
|
|
|
private final boolean sized;
|
|
private final boolean exact;
|
|
|
|
Mode(boolean sized, boolean exact) {
|
|
this.sized = sized;
|
|
this.exact = exact;
|
|
}
|
|
|
|
boolean isSized() {
|
|
return sized;
|
|
}
|
|
|
|
boolean isExact() {
|
|
return exact;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bytecode StringBuilder strategy.
|
|
*
|
|
* <p>This strategy operates in three modes, gated by {@link Mode}.
|
|
*
|
|
* <p><b>{@link Strategy#BC_SB}: "bytecode StringBuilder".</b>
|
|
*
|
|
* <p>This strategy spins up the bytecode that has the same StringBuilder
|
|
* chain javac would otherwise emit. This strategy uses only the public API,
|
|
* and comes as the baseline for the current JDK behavior. On other words,
|
|
* this strategy moves the javac generated bytecode to runtime. The
|
|
* generated bytecode is loaded via Lookup::defineClass, but with
|
|
* the caller class coming from the BSM -- in other words, the protection
|
|
* guarantees are inherited from the method where invokedynamic was
|
|
* originally called. This means, among other things, that the bytecode is
|
|
* verified for all non-JDK uses.
|
|
*
|
|
* <p><b>{@link Strategy#BC_SB_SIZED}: "bytecode StringBuilder, but
|
|
* sized".</b>
|
|
*
|
|
* <p>This strategy acts similarly to {@link Strategy#BC_SB}, but it also
|
|
* tries to guess the capacity required for StringBuilder to accept all
|
|
* arguments without resizing. This strategy only makes an educated guess:
|
|
* it only guesses the space required for known types (e.g. primitives and
|
|
* Strings), but does not otherwise convert arguments. Therefore, the
|
|
* capacity estimate may be wrong, and StringBuilder may have to
|
|
* transparently resize or trim when doing the actual concatenation. While
|
|
* this does not constitute a correctness issue (in the end, that what BC_SB
|
|
* has to do anyway), this does pose a potential performance problem.
|
|
*
|
|
* <p><b>{@link Strategy#BC_SB_SIZED_EXACT}: "bytecode StringBuilder, but
|
|
* sized exactly".</b>
|
|
*
|
|
* <p>This strategy improves on @link Strategy#BC_SB_SIZED}, by first
|
|
* converting all arguments to String in order to get the exact capacity
|
|
* StringBuilder should have. The conversion is done via the public
|
|
* String.valueOf and/or Object.toString methods, and does not touch any
|
|
* private String API.
|
|
*/
|
|
private static final class BytecodeStringBuilderStrategy {
|
|
static final int CLASSFILE_VERSION = 52;
|
|
static final String METHOD_NAME = "concat";
|
|
|
|
private static final ConcurrentMap<Key, MethodHandle> CACHE;
|
|
|
|
/**
|
|
* Enables caching of strategy stubs. This may improve the linkage time by reusing the generated
|
|
* code, at the expense of contaminating the profiles.
|
|
*/
|
|
private static final boolean CACHE_ENABLE;
|
|
|
|
/**
|
|
* Dump generated classes to disk, for debugging purposes.
|
|
*/
|
|
private static final ProxyClassesDumper DUMPER;
|
|
|
|
static {
|
|
CACHE_ENABLE = Boolean.parseBoolean(
|
|
VM.getSavedProperty("java.lang.invoke.stringConcat.cache"));
|
|
CACHE = CACHE_ENABLE ? new ConcurrentHashMap<>() : null;
|
|
|
|
final String dumpPath =
|
|
VM.getSavedProperty("java.lang.invoke.stringConcat.dumpClasses");
|
|
|
|
DUMPER = (dumpPath == null) ? null : ProxyClassesDumper.getInstance(dumpPath);
|
|
}
|
|
|
|
private BytecodeStringBuilderStrategy() {
|
|
// no instantiation
|
|
}
|
|
|
|
private static MethodHandle generate(Lookup lookup, MethodType args, Recipe recipe, Mode mode) throws Exception {
|
|
String className = getClassName(lookup.lookupClass());
|
|
Key key = null;
|
|
if (CACHE_ENABLE) {
|
|
key = new Key(className, args, recipe);
|
|
MethodHandle mh = CACHE.get(key);
|
|
if (mh != null) {
|
|
return mh;
|
|
}
|
|
}
|
|
|
|
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
|
|
|
|
cw.visit(CLASSFILE_VERSION,
|
|
ACC_SUPER + ACC_PUBLIC + ACC_FINAL + ACC_SYNTHETIC,
|
|
className,
|
|
null,
|
|
"java/lang/Object",
|
|
null
|
|
);
|
|
|
|
MethodVisitor mv = cw.visitMethod(
|
|
ACC_PUBLIC + ACC_STATIC + ACC_FINAL,
|
|
METHOD_NAME,
|
|
args.toMethodDescriptorString(),
|
|
null,
|
|
null);
|
|
|
|
// use of @ForceInline no longer has any effect
|
|
mv.visitAnnotation("Ljdk/internal/vm/annotation/ForceInline;", true);
|
|
mv.visitCode();
|
|
|
|
Class<?>[] arr = args.parameterArray();
|
|
boolean[] guaranteedNonNull = new boolean[arr.length];
|
|
|
|
if (mode.isExact()) {
|
|
/*
|
|
In exact mode, we need to convert all arguments to their String representations,
|
|
as this allows to compute their String sizes exactly. We cannot use private
|
|
methods for primitives in here, therefore we need to convert those as well.
|
|
|
|
We also record what arguments are guaranteed to be non-null as the result
|
|
of the conversion. String.valueOf does the null checks for us. The only
|
|
corner case to take care of is String.valueOf(Object) returning null itself.
|
|
|
|
Also, if any conversion happened, then the slot indices in the incoming
|
|
arguments are not equal to the final local maps. The only case this may break
|
|
is when converting 2-slot long/double argument to 1-slot String. Therefore,
|
|
we get away with tracking modified offset, since no conversion can overwrite
|
|
the upcoming the argument.
|
|
*/
|
|
|
|
int off = 0;
|
|
int modOff = 0;
|
|
for (int c = 0; c < arr.length; c++) {
|
|
Class<?> cl = arr[c];
|
|
if (cl == String.class) {
|
|
if (off != modOff) {
|
|
mv.visitIntInsn(getLoadOpcode(cl), off);
|
|
mv.visitIntInsn(ASTORE, modOff);
|
|
}
|
|
} else {
|
|
mv.visitIntInsn(getLoadOpcode(cl), off);
|
|
mv.visitMethodInsn(
|
|
INVOKESTATIC,
|
|
"java/lang/String",
|
|
"valueOf",
|
|
getStringValueOfDesc(cl),
|
|
false
|
|
);
|
|
mv.visitIntInsn(ASTORE, modOff);
|
|
arr[c] = String.class;
|
|
guaranteedNonNull[c] = cl.isPrimitive();
|
|
}
|
|
off += getParameterSize(cl);
|
|
modOff += getParameterSize(String.class);
|
|
}
|
|
}
|
|
|
|
if (mode.isSized()) {
|
|
/*
|
|
When operating in sized mode (this includes exact mode), it makes sense to make
|
|
StringBuilder append chains look familiar to OptimizeStringConcat. For that, we
|
|
need to do null-checks early, not make the append chain shape simpler.
|
|
*/
|
|
|
|
int off = 0;
|
|
for (RecipeElement el : recipe.getElements()) {
|
|
switch (el.getTag()) {
|
|
case TAG_CONST:
|
|
// Guaranteed non-null, no null check required.
|
|
break;
|
|
case TAG_ARG:
|
|
// Null-checks are needed only for String arguments, and when a previous stage
|
|
// did not do implicit null-checks. If a String is null, we eagerly replace it
|
|
// with "null" constant. Note, we omit Objects here, because we don't call
|
|
// .length() on them down below.
|
|
int ac = el.getArgPos();
|
|
Class<?> cl = arr[ac];
|
|
if (cl == String.class && !guaranteedNonNull[ac]) {
|
|
Label l0 = new Label();
|
|
mv.visitIntInsn(ALOAD, off);
|
|
mv.visitJumpInsn(IFNONNULL, l0);
|
|
mv.visitLdcInsn("null");
|
|
mv.visitIntInsn(ASTORE, off);
|
|
mv.visitLabel(l0);
|
|
}
|
|
off += getParameterSize(cl);
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare StringBuilder instance
|
|
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
|
|
mv.visitInsn(DUP);
|
|
|
|
if (mode.isSized()) {
|
|
/*
|
|
Sized mode requires us to walk through the arguments, and estimate the final length.
|
|
In exact mode, this will operate on Strings only. This code would accumulate the
|
|
final length on stack.
|
|
*/
|
|
int len = 0;
|
|
int off = 0;
|
|
|
|
mv.visitInsn(ICONST_0);
|
|
|
|
for (RecipeElement el : recipe.getElements()) {
|
|
switch (el.getTag()) {
|
|
case TAG_CONST:
|
|
len += el.getValue().length();
|
|
break;
|
|
case TAG_ARG:
|
|
/*
|
|
If an argument is String, then we can call .length() on it. Sized/Exact modes have
|
|
converted arguments for us. If an argument is primitive, we can provide a guess
|
|
for its String representation size.
|
|
*/
|
|
Class<?> cl = arr[el.getArgPos()];
|
|
if (cl == String.class) {
|
|
mv.visitIntInsn(ALOAD, off);
|
|
mv.visitMethodInsn(
|
|
INVOKEVIRTUAL,
|
|
"java/lang/String",
|
|
"length",
|
|
"()I",
|
|
false
|
|
);
|
|
mv.visitInsn(IADD);
|
|
} else if (cl.isPrimitive()) {
|
|
len += estimateSize(cl);
|
|
}
|
|
off += getParameterSize(cl);
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
}
|
|
|
|
// Constants have non-zero length, mix in
|
|
if (len > 0) {
|
|
iconst(mv, len);
|
|
mv.visitInsn(IADD);
|
|
}
|
|
|
|
mv.visitMethodInsn(
|
|
INVOKESPECIAL,
|
|
"java/lang/StringBuilder",
|
|
"<init>",
|
|
"(I)V",
|
|
false
|
|
);
|
|
} else {
|
|
mv.visitMethodInsn(
|
|
INVOKESPECIAL,
|
|
"java/lang/StringBuilder",
|
|
"<init>",
|
|
"()V",
|
|
false
|
|
);
|
|
}
|
|
|
|
// At this point, we have a blank StringBuilder on stack, fill it in with .append calls.
|
|
{
|
|
int off = 0;
|
|
for (RecipeElement el : recipe.getElements()) {
|
|
String desc;
|
|
switch (el.getTag()) {
|
|
case TAG_CONST:
|
|
mv.visitLdcInsn(el.getValue());
|
|
desc = getSBAppendDesc(String.class);
|
|
break;
|
|
case TAG_ARG:
|
|
Class<?> cl = arr[el.getArgPos()];
|
|
mv.visitVarInsn(getLoadOpcode(cl), off);
|
|
off += getParameterSize(cl);
|
|
desc = getSBAppendDesc(cl);
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
|
|
mv.visitMethodInsn(
|
|
INVOKEVIRTUAL,
|
|
"java/lang/StringBuilder",
|
|
"append",
|
|
desc,
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
if (DEBUG && mode.isExact()) {
|
|
/*
|
|
Exactness checks compare the final StringBuilder.capacity() with a resulting
|
|
String.length(). If these values disagree, that means StringBuilder had to perform
|
|
storage trimming, which defeats the purpose of exact strategies.
|
|
*/
|
|
|
|
/*
|
|
The logic for this check is as follows:
|
|
|
|
Stack before: Op:
|
|
(SB) dup, dup
|
|
(SB, SB, SB) capacity()
|
|
(int, SB, SB) swap
|
|
(SB, int, SB) toString()
|
|
(S, int, SB) length()
|
|
(int, int, SB) if_icmpeq
|
|
(SB) <end>
|
|
|
|
Note that it leaves the same StringBuilder on exit, like the one on enter.
|
|
*/
|
|
|
|
mv.visitInsn(DUP);
|
|
mv.visitInsn(DUP);
|
|
|
|
mv.visitMethodInsn(
|
|
INVOKEVIRTUAL,
|
|
"java/lang/StringBuilder",
|
|
"capacity",
|
|
"()I",
|
|
false
|
|
);
|
|
|
|
mv.visitInsn(SWAP);
|
|
|
|
mv.visitMethodInsn(
|
|
INVOKEVIRTUAL,
|
|
"java/lang/StringBuilder",
|
|
"toString",
|
|
"()Ljava/lang/String;",
|
|
false
|
|
);
|
|
|
|
mv.visitMethodInsn(
|
|
INVOKEVIRTUAL,
|
|
"java/lang/String",
|
|
"length",
|
|
"()I",
|
|
false
|
|
);
|
|
|
|
Label l0 = new Label();
|
|
mv.visitJumpInsn(IF_ICMPEQ, l0);
|
|
|
|
mv.visitTypeInsn(NEW, "java/lang/AssertionError");
|
|
mv.visitInsn(DUP);
|
|
mv.visitLdcInsn("Failed exactness check");
|
|
mv.visitMethodInsn(INVOKESPECIAL,
|
|
"java/lang/AssertionError",
|
|
"<init>",
|
|
"(Ljava/lang/Object;)V",
|
|
false);
|
|
mv.visitInsn(ATHROW);
|
|
|
|
mv.visitLabel(l0);
|
|
}
|
|
|
|
mv.visitMethodInsn(
|
|
INVOKEVIRTUAL,
|
|
"java/lang/StringBuilder",
|
|
"toString",
|
|
"()Ljava/lang/String;",
|
|
false
|
|
);
|
|
|
|
mv.visitInsn(ARETURN);
|
|
|
|
mv.visitMaxs(-1, -1);
|
|
mv.visitEnd();
|
|
cw.visitEnd();
|
|
|
|
byte[] classBytes = cw.toByteArray();
|
|
try {
|
|
Class<?> innerClass = lookup.defineHiddenClass(classBytes, true, STRONG).lookupClass();
|
|
dumpIfEnabled(className, classBytes);
|
|
MethodHandle mh = lookup.findStatic(innerClass, METHOD_NAME, args);
|
|
if (CACHE_ENABLE) {
|
|
CACHE.put(key, mh);
|
|
}
|
|
return mh;
|
|
} catch (Exception e) {
|
|
dumpIfEnabled(className + "$$FAILED", classBytes);
|
|
throw new StringConcatException("Exception while spinning the class", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The generated class is in the same package as the host class as
|
|
* it's the implementation of the string concatenation for the host
|
|
* class.
|
|
*
|
|
* When cache is enabled, we want to cache as much as we can.
|
|
*/
|
|
private static String getClassName(Class<?> hostClass) {
|
|
if (CACHE_ENABLE) {
|
|
String pkgName = hostClass.getPackageName();
|
|
return (!pkgName.isEmpty() ? pkgName.replace('.', '/') + "/" : "") + "Stubs$$StringConcat";
|
|
} else {
|
|
String name = hostClass.isHidden() ? hostClass.getName().replace('/', '_')
|
|
: hostClass.getName();
|
|
return name.replace('.', '/') + "$$StringConcat";
|
|
}
|
|
}
|
|
|
|
private static void dumpIfEnabled(String name, byte[] bytes) {
|
|
if (DUMPER != null) {
|
|
DUMPER.dumpClass(name, bytes);
|
|
}
|
|
}
|
|
|
|
private static String getSBAppendDesc(Class<?> cl) {
|
|
if (cl.isPrimitive()) {
|
|
if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) {
|
|
return "(I)Ljava/lang/StringBuilder;";
|
|
} else if (cl == Boolean.TYPE) {
|
|
return "(Z)Ljava/lang/StringBuilder;";
|
|
} else if (cl == Character.TYPE) {
|
|
return "(C)Ljava/lang/StringBuilder;";
|
|
} else if (cl == Double.TYPE) {
|
|
return "(D)Ljava/lang/StringBuilder;";
|
|
} else if (cl == Float.TYPE) {
|
|
return "(F)Ljava/lang/StringBuilder;";
|
|
} else if (cl == Long.TYPE) {
|
|
return "(J)Ljava/lang/StringBuilder;";
|
|
} else {
|
|
throw new IllegalStateException("Unhandled primitive StringBuilder.append: " + cl);
|
|
}
|
|
} else if (cl == String.class) {
|
|
return "(Ljava/lang/String;)Ljava/lang/StringBuilder;";
|
|
} else {
|
|
return "(Ljava/lang/Object;)Ljava/lang/StringBuilder;";
|
|
}
|
|
}
|
|
|
|
private static String getStringValueOfDesc(Class<?> cl) {
|
|
if (cl.isPrimitive()) {
|
|
if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) {
|
|
return "(I)Ljava/lang/String;";
|
|
} else if (cl == Boolean.TYPE) {
|
|
return "(Z)Ljava/lang/String;";
|
|
} else if (cl == Character.TYPE) {
|
|
return "(C)Ljava/lang/String;";
|
|
} else if (cl == Double.TYPE) {
|
|
return "(D)Ljava/lang/String;";
|
|
} else if (cl == Float.TYPE) {
|
|
return "(F)Ljava/lang/String;";
|
|
} else if (cl == Long.TYPE) {
|
|
return "(J)Ljava/lang/String;";
|
|
} else {
|
|
throw new IllegalStateException("Unhandled String.valueOf: " + cl);
|
|
}
|
|
} else if (cl == String.class) {
|
|
return "(Ljava/lang/String;)Ljava/lang/String;";
|
|
} else {
|
|
return "(Ljava/lang/Object;)Ljava/lang/String;";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The following method is copied from
|
|
* org.objectweb.asm.commons.InstructionAdapter. Part of ASM: a very small
|
|
* and fast Java bytecode manipulation framework.
|
|
* Copyright (c) 2000-2005 INRIA, France Telecom All rights reserved.
|
|
*/
|
|
private static void iconst(MethodVisitor mv, final int cst) {
|
|
if (cst >= -1 && cst <= 5) {
|
|
mv.visitInsn(Opcodes.ICONST_0 + cst);
|
|
} else if (cst >= Byte.MIN_VALUE && cst <= Byte.MAX_VALUE) {
|
|
mv.visitIntInsn(Opcodes.BIPUSH, cst);
|
|
} else if (cst >= Short.MIN_VALUE && cst <= Short.MAX_VALUE) {
|
|
mv.visitIntInsn(Opcodes.SIPUSH, cst);
|
|
} else {
|
|
mv.visitLdcInsn(cst);
|
|
}
|
|
}
|
|
|
|
private static int getLoadOpcode(Class<?> c) {
|
|
if (c == Void.TYPE) {
|
|
throw new InternalError("Unexpected void type of load opcode");
|
|
}
|
|
return ILOAD + getOpcodeOffset(c);
|
|
}
|
|
|
|
private static int getOpcodeOffset(Class<?> c) {
|
|
if (c.isPrimitive()) {
|
|
if (c == Long.TYPE) {
|
|
return 1;
|
|
} else if (c == Float.TYPE) {
|
|
return 2;
|
|
} else if (c == Double.TYPE) {
|
|
return 3;
|
|
}
|
|
return 0;
|
|
} else {
|
|
return 4;
|
|
}
|
|
}
|
|
|
|
private static int getParameterSize(Class<?> c) {
|
|
if (c == Void.TYPE) {
|
|
return 0;
|
|
} else if (c == Long.TYPE || c == Double.TYPE) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MethodHandle StringBuilder strategy.
|
|
*
|
|
* <p>This strategy operates in two modes, gated by {@link Mode}.
|
|
*
|
|
* <p><b>{@link Strategy#MH_SB_SIZED}: "MethodHandles StringBuilder,
|
|
* sized".</b>
|
|
*
|
|
* <p>This strategy avoids spinning up the bytecode by building the
|
|
* computation on MethodHandle combinators. The computation is built with
|
|
* public MethodHandle APIs, resolved from a public Lookup sequence, and
|
|
* ends up calling the public StringBuilder API. Therefore, this strategy
|
|
* does not use any private API at all since everything is handled under
|
|
* cover by java.lang.invoke APIs.
|
|
*
|
|
* <p><b>{@link Strategy#MH_SB_SIZED_EXACT}: "MethodHandles StringBuilder,
|
|
* sized exactly".</b>
|
|
*
|
|
* <p>This strategy improves on @link Strategy#MH_SB_SIZED}, by first
|
|
* converting all arguments to String in order to get the exact capacity
|
|
* StringBuilder should have. The conversion is done via the public
|
|
* String.valueOf and/or Object.toString methods, and does not touch any
|
|
* private String API.
|
|
*/
|
|
private static final class MethodHandleStringBuilderStrategy {
|
|
private MethodHandleStringBuilderStrategy() {
|
|
// no instantiation
|
|
}
|
|
|
|
private static MethodHandle generate(MethodType mt, Recipe recipe, Mode mode) throws Exception {
|
|
int pc = mt.parameterCount();
|
|
|
|
Class<?>[] ptypes = mt.parameterArray();
|
|
MethodHandle[] filters = new MethodHandle[ptypes.length];
|
|
for (int i = 0; i < ptypes.length; i++) {
|
|
MethodHandle filter;
|
|
switch (mode) {
|
|
case SIZED:
|
|
// In sized mode, we convert all references and floats/doubles
|
|
// to String: there is no specialization for different
|
|
// classes in StringBuilder API, and it will convert to
|
|
// String internally anyhow.
|
|
filter = Stringifiers.forMost(ptypes[i]);
|
|
break;
|
|
case SIZED_EXACT:
|
|
// In exact mode, we convert everything to String:
|
|
// this helps to compute the storage exactly.
|
|
filter = Stringifiers.forAny(ptypes[i]);
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Not supported");
|
|
}
|
|
if (filter != null) {
|
|
filters[i] = filter;
|
|
ptypes[i] = filter.type().returnType();
|
|
}
|
|
}
|
|
|
|
MethodHandle[] lengthers = new MethodHandle[pc];
|
|
|
|
// Figure out lengths: constants' lengths can be deduced on the spot.
|
|
// All reference arguments were filtered to String in the combinators below, so we can
|
|
// call the usual String.length(). Primitive values string sizes can be estimated.
|
|
int initial = 0;
|
|
for (RecipeElement el : recipe.getElements()) {
|
|
switch (el.getTag()) {
|
|
case TAG_CONST:
|
|
initial += el.getValue().length();
|
|
break;
|
|
case TAG_ARG:
|
|
final int i = el.getArgPos();
|
|
Class<?> type = ptypes[i];
|
|
if (type.isPrimitive()) {
|
|
MethodHandle est = MethodHandles.constant(int.class, estimateSize(type));
|
|
est = MethodHandles.dropArguments(est, 0, type);
|
|
lengthers[i] = est;
|
|
} else {
|
|
lengthers[i] = STRING_LENGTH;
|
|
}
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
}
|
|
|
|
// Create (StringBuilder, <args>) shape for appending:
|
|
MethodHandle builder = MethodHandles.dropArguments(MethodHandles.identity(StringBuilder.class), 1, ptypes);
|
|
|
|
// Compose append calls. This is done in reverse because the application order is
|
|
// reverse as well.
|
|
List<RecipeElement> elements = recipe.getElements();
|
|
for (int i = elements.size() - 1; i >= 0; i--) {
|
|
RecipeElement el = elements.get(i);
|
|
MethodHandle appender;
|
|
switch (el.getTag()) {
|
|
case TAG_CONST:
|
|
MethodHandle mh = appender(adaptToStringBuilder(String.class));
|
|
appender = MethodHandles.insertArguments(mh, 1, el.getValue());
|
|
break;
|
|
case TAG_ARG:
|
|
int ac = el.getArgPos();
|
|
appender = appender(ptypes[ac]);
|
|
|
|
// Insert dummy arguments to match the prefix in the signature.
|
|
// The actual appender argument will be the ac-ith argument.
|
|
if (ac != 0) {
|
|
appender = MethodHandles.dropArguments(appender, 1, Arrays.copyOf(ptypes, ac));
|
|
}
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
builder = MethodHandles.foldArguments(builder, appender);
|
|
}
|
|
|
|
// Build the sub-tree that adds the sizes and produces a StringBuilder:
|
|
|
|
// a) Start with the reducer that accepts all arguments, plus one
|
|
// slot for the initial value. Inject the initial value right away.
|
|
// This produces (<ints>)int shape:
|
|
MethodHandle sum = getReducerFor(pc + 1);
|
|
MethodHandle adder = MethodHandles.insertArguments(sum, 0, initial);
|
|
|
|
// b) Apply lengthers to transform arguments to lengths, producing (<args>)int
|
|
adder = MethodHandles.filterArguments(adder, 0, lengthers);
|
|
|
|
// c) Instantiate StringBuilder (<args>)int -> (<args>)StringBuilder
|
|
MethodHandle newBuilder = MethodHandles.filterReturnValue(adder, NEW_STRING_BUILDER);
|
|
|
|
// d) Fold in StringBuilder constructor, this produces (<args>)StringBuilder
|
|
MethodHandle mh = MethodHandles.foldArguments(builder, newBuilder);
|
|
|
|
// Convert non-primitive arguments to Strings
|
|
mh = MethodHandles.filterArguments(mh, 0, filters);
|
|
|
|
// Convert (<args>)StringBuilder to (<args>)String
|
|
if (DEBUG && mode.isExact()) {
|
|
mh = MethodHandles.filterReturnValue(mh, BUILDER_TO_STRING_CHECKED);
|
|
} else {
|
|
mh = MethodHandles.filterReturnValue(mh, BUILDER_TO_STRING);
|
|
}
|
|
|
|
return mh;
|
|
}
|
|
|
|
private static MethodHandle getReducerFor(int cnt) {
|
|
return SUMMERS.computeIfAbsent(cnt, SUMMER);
|
|
}
|
|
|
|
private static MethodHandle appender(Class<?> appendType) {
|
|
MethodHandle appender = lookupVirtual(MethodHandles.publicLookup(), StringBuilder.class, "append",
|
|
StringBuilder.class, adaptToStringBuilder(appendType));
|
|
|
|
// appenders should return void, this would not modify the target signature during folding
|
|
MethodType nt = MethodType.methodType(void.class, StringBuilder.class, appendType);
|
|
return appender.asType(nt);
|
|
}
|
|
|
|
private static String toStringChecked(StringBuilder sb) {
|
|
String s = sb.toString();
|
|
if (s.length() != sb.capacity()) {
|
|
throw new AssertionError("Exactness check failed: result length = " + s.length() + ", buffer capacity = " + sb.capacity());
|
|
}
|
|
return s;
|
|
}
|
|
|
|
private static int sum(int v1, int v2) {
|
|
return v1 + v2;
|
|
}
|
|
|
|
private static int sum(int v1, int v2, int v3) {
|
|
return v1 + v2 + v3;
|
|
}
|
|
|
|
private static int sum(int v1, int v2, int v3, int v4) {
|
|
return v1 + v2 + v3 + v4;
|
|
}
|
|
|
|
private static int sum(int v1, int v2, int v3, int v4, int v5) {
|
|
return v1 + v2 + v3 + v4 + v5;
|
|
}
|
|
|
|
private static int sum(int v1, int v2, int v3, int v4, int v5, int v6) {
|
|
return v1 + v2 + v3 + v4 + v5 + v6;
|
|
}
|
|
|
|
private static int sum(int v1, int v2, int v3, int v4, int v5, int v6, int v7) {
|
|
return v1 + v2 + v3 + v4 + v5 + v6 + v7;
|
|
}
|
|
|
|
private static int sum(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8) {
|
|
return v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8;
|
|
}
|
|
|
|
private static int sum(int initial, int[] vs) {
|
|
int sum = initial;
|
|
for (int v : vs) {
|
|
sum += v;
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
private static final Lookup MHSBS_LOOKUP = lookup();
|
|
|
|
private static final ConcurrentMap<Integer, MethodHandle> SUMMERS;
|
|
|
|
// This one is deliberately non-lambdified to optimize startup time:
|
|
private static final Function<Integer, MethodHandle> SUMMER = new Function<Integer, MethodHandle>() {
|
|
@Override
|
|
public MethodHandle apply(Integer cnt) {
|
|
if (cnt == 1) {
|
|
return MethodHandles.identity(int.class);
|
|
} else if (cnt <= 8) {
|
|
// Variable-arity collectors are not as efficient as small-count methods,
|
|
// unroll some initial sizes.
|
|
Class<?>[] cls = new Class<?>[cnt];
|
|
Arrays.fill(cls, int.class);
|
|
return lookupStatic(MHSBS_LOOKUP, MethodHandleStringBuilderStrategy.class, "sum", int.class, cls);
|
|
} else {
|
|
return lookupStatic(MHSBS_LOOKUP, MethodHandleStringBuilderStrategy.class, "sum", int.class, int.class, int[].class)
|
|
.asCollector(int[].class, cnt - 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
private static final MethodHandle NEW_STRING_BUILDER, STRING_LENGTH, BUILDER_TO_STRING, BUILDER_TO_STRING_CHECKED;
|
|
|
|
static {
|
|
SUMMERS = new ConcurrentHashMap<>();
|
|
Lookup publicLookup = MethodHandles.publicLookup();
|
|
NEW_STRING_BUILDER = lookupConstructor(publicLookup, StringBuilder.class, int.class);
|
|
STRING_LENGTH = lookupVirtual(publicLookup, String.class, "length", int.class);
|
|
BUILDER_TO_STRING = lookupVirtual(publicLookup, StringBuilder.class, "toString", String.class);
|
|
if (DEBUG) {
|
|
BUILDER_TO_STRING_CHECKED = lookupStatic(MHSBS_LOOKUP, MethodHandleStringBuilderStrategy.class,
|
|
"toStringChecked", String.class, StringBuilder.class);
|
|
} else {
|
|
BUILDER_TO_STRING_CHECKED = null;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* <p><b>{@link Strategy#MH_INLINE_SIZED_EXACT}: "MethodHandles inline,
|
|
* sized exactly".</b>
|
|
*
|
|
* <p>This strategy replicates what StringBuilders are doing: it builds the
|
|
* byte[] array on its own and passes that byte[] array to String
|
|
* constructor. This strategy requires access to some private APIs in JDK,
|
|
* most notably, the read-only Integer/Long.stringSize methods that measure
|
|
* the character length of the integers, and the private String constructor
|
|
* that accepts byte[] arrays without copying. While this strategy assumes a
|
|
* particular implementation details for String, this opens the door for
|
|
* building a very optimal concatenation sequence. This is the only strategy
|
|
* that requires porting if there are private JDK changes occur.
|
|
*/
|
|
private static final class MethodHandleInlineCopyStrategy {
|
|
private MethodHandleInlineCopyStrategy() {
|
|
// no instantiation
|
|
}
|
|
|
|
static MethodHandle generate(MethodType mt, Recipe recipe) throws Throwable {
|
|
|
|
// Fast-path two-argument Object + Object concatenations
|
|
if (recipe.getElements().size() == 2) {
|
|
// Two object arguments
|
|
if (mt.parameterCount() == 2 &&
|
|
!mt.parameterType(0).isPrimitive() &&
|
|
!mt.parameterType(1).isPrimitive() &&
|
|
recipe.getElements().get(0).getTag() == TAG_ARG &&
|
|
recipe.getElements().get(1).getTag() == TAG_ARG) {
|
|
|
|
return simpleConcat();
|
|
|
|
} else if (mt.parameterCount() == 1 &&
|
|
!mt.parameterType(0).isPrimitive()) {
|
|
// One Object argument, one constant
|
|
MethodHandle mh = simpleConcat();
|
|
|
|
if (recipe.getElements().get(0).getTag() == TAG_CONST &&
|
|
recipe.getElements().get(1).getTag() == TAG_ARG) {
|
|
// First recipe element is a constant
|
|
return MethodHandles.insertArguments(mh, 0,
|
|
recipe.getElements().get(0).getValue());
|
|
|
|
} else if (recipe.getElements().get(1).getTag() == TAG_CONST &&
|
|
recipe.getElements().get(0).getTag() == TAG_ARG) {
|
|
// Second recipe element is a constant
|
|
return MethodHandles.insertArguments(mh, 1,
|
|
recipe.getElements().get(1).getValue());
|
|
|
|
}
|
|
}
|
|
// else... fall-through to slow-path
|
|
}
|
|
|
|
// Create filters and obtain filtered parameter types. Filters would be used in the beginning
|
|
// to convert the incoming arguments into the arguments we can process (e.g. Objects -> Strings).
|
|
// The filtered argument type list is used all over in the combinators below.
|
|
Class<?>[] ptypes = mt.parameterArray();
|
|
MethodHandle[] filters = null;
|
|
for (int i = 0; i < ptypes.length; i++) {
|
|
MethodHandle filter = Stringifiers.forMost(ptypes[i]);
|
|
if (filter != null) {
|
|
if (filters == null) {
|
|
filters = new MethodHandle[ptypes.length];
|
|
}
|
|
filters[i] = filter;
|
|
ptypes[i] = filter.type().returnType();
|
|
}
|
|
}
|
|
|
|
// Start building the combinator tree. The tree "starts" with (<parameters>)String, and "finishes"
|
|
// with the (byte[], long)String shape to invoke newString in StringConcatHelper. The combinators are
|
|
// assembled bottom-up, which makes the code arguably hard to read.
|
|
|
|
// Drop all remaining parameter types, leave only helper arguments:
|
|
MethodHandle mh;
|
|
|
|
mh = MethodHandles.dropArguments(newString(), 2, ptypes);
|
|
|
|
long initialLengthCoder = INITIAL_CODER;
|
|
|
|
// Mix in prependers. This happens when (byte[], long) = (storage, indexCoder) is already
|
|
// known from the combinators below. We are assembling the string backwards, so the index coded
|
|
// into indexCoder is the *ending* index.
|
|
|
|
// We need one prepender per argument, but also need to fold in constants. We do so by greedily
|
|
// create prependers that fold in surrounding constants into the argument prepender. This reduces
|
|
// the number of unique MH combinator tree shapes we'll create in an application.
|
|
String prefixConstant = null, suffixConstant = null;
|
|
int pos = -1;
|
|
for (RecipeElement el : recipe.getElements()) {
|
|
// Do the prepend, and put "new" index at index 1
|
|
switch (el.getTag()) {
|
|
case TAG_CONST: {
|
|
String constantValue = el.getValue();
|
|
|
|
// Eagerly update the initialLengthCoder value
|
|
initialLengthCoder = (long)mixer(String.class).invoke(initialLengthCoder, constantValue);
|
|
|
|
if (pos < 0) {
|
|
// Collecting into prefixConstant
|
|
prefixConstant = prefixConstant == null ? constantValue : prefixConstant + constantValue;
|
|
} else {
|
|
// Collecting into suffixConstant
|
|
suffixConstant = suffixConstant == null ? constantValue : suffixConstant + constantValue;
|
|
}
|
|
break;
|
|
}
|
|
case TAG_ARG: {
|
|
|
|
if (pos >= 0) {
|
|
// Flush the previous non-constant arg with any prefix/suffix constant
|
|
mh = MethodHandles.filterArgumentsWithCombiner(
|
|
mh, 1,
|
|
prepender(prefixConstant, ptypes[pos], suffixConstant),
|
|
1, 0, // indexCoder, storage
|
|
2 + pos // selected argument
|
|
);
|
|
prefixConstant = suffixConstant = null;
|
|
}
|
|
// Mark the pos of next non-constant arg
|
|
pos = el.getArgPos();
|
|
break;
|
|
}
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
}
|
|
|
|
// Insert any trailing args, constants
|
|
if (pos >= 0) {
|
|
mh = MethodHandles.filterArgumentsWithCombiner(
|
|
mh, 1,
|
|
prepender(prefixConstant, ptypes[pos], suffixConstant),
|
|
1, 0, // indexCoder, storage
|
|
2 + pos // selected argument
|
|
);
|
|
} else if (prefixConstant != null) {
|
|
assert (suffixConstant == null);
|
|
// Sole prefixConstant can only happen if there were no non-constant arguments
|
|
mh = MethodHandles.filterArgumentsWithCombiner(
|
|
mh, 1,
|
|
MethodHandles.insertArguments(prepender(null, String.class, null), 2, prefixConstant),
|
|
1, 0 // indexCoder, storage
|
|
);
|
|
}
|
|
|
|
// Fold in byte[] instantiation at argument 0
|
|
mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArray(),
|
|
1 // index
|
|
);
|
|
|
|
// Start combining length and coder mixers.
|
|
//
|
|
// Length is easy: constant lengths can be computed on the spot, and all non-constant
|
|
// shapes have been either converted to Strings, or explicit methods for getting the
|
|
// string length out of primitives are provided.
|
|
//
|
|
// Coders are more interesting. Only Object, String and char arguments (and constants)
|
|
// can have non-Latin1 encoding. It is easier to blindly convert constants to String,
|
|
// and deduce the coder from there. Arguments would be either converted to Strings
|
|
// during the initial filtering, or handled by specializations in MIXERS.
|
|
//
|
|
// The method handle shape before all mixers are combined in is:
|
|
// (long, <args>)String = ("indexCoder", <args>)
|
|
//
|
|
// We will bind the initialLengthCoder value to the last mixer (the one that will be
|
|
// executed first), then fold that in. This leaves the shape after all mixers are
|
|
// combined in as:
|
|
// (<args>)String = (<args>)
|
|
|
|
int ac = -1;
|
|
MethodHandle mix = null;
|
|
for (RecipeElement el : recipe.getElements()) {
|
|
switch (el.getTag()) {
|
|
case TAG_CONST:
|
|
// Constants already handled in the code above
|
|
break;
|
|
case TAG_ARG:
|
|
if (ac >= 0) {
|
|
// Compute new "index" in-place using old value plus the appropriate argument.
|
|
mh = MethodHandles.filterArgumentsWithCombiner(mh, 0, mix,
|
|
0, // old-index
|
|
1 + ac // selected argument
|
|
);
|
|
}
|
|
|
|
ac = el.getArgPos();
|
|
Class<?> argClass = ptypes[ac];
|
|
mix = mixer(argClass);
|
|
|
|
break;
|
|
default:
|
|
throw new StringConcatException("Unhandled tag: " + el.getTag());
|
|
}
|
|
}
|
|
|
|
// Insert the initialLengthCoder value into the final mixer, then
|
|
// fold that into the base method handle
|
|
if (ac >= 0) {
|
|
mix = MethodHandles.insertArguments(mix, 0, initialLengthCoder);
|
|
mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, mix,
|
|
1 + ac // selected argument
|
|
);
|
|
} else {
|
|
// No mixer (constants only concat), insert initialLengthCoder directly
|
|
mh = MethodHandles.insertArguments(mh, 0, initialLengthCoder);
|
|
}
|
|
|
|
// The method handle shape here is (<args>).
|
|
|
|
// Apply filters, converting the arguments:
|
|
if (filters != null) {
|
|
mh = MethodHandles.filterArguments(mh, 0, filters);
|
|
}
|
|
|
|
return mh;
|
|
}
|
|
|
|
private static MethodHandle prepender(String prefix, Class<?> cl, String suffix) {
|
|
return MethodHandles.insertArguments(
|
|
MethodHandles.insertArguments(
|
|
PREPENDERS.computeIfAbsent(cl, PREPEND), 2, prefix), 3, suffix);
|
|
}
|
|
|
|
private static MethodHandle mixer(Class<?> cl) {
|
|
return MIXERS.computeIfAbsent(cl, MIX);
|
|
}
|
|
|
|
// This one is deliberately non-lambdified to optimize startup time:
|
|
private static final Function<Class<?>, MethodHandle> PREPEND = new Function<>() {
|
|
@Override
|
|
public MethodHandle apply(Class<?> c) {
|
|
return JLA.stringConcatHelper("prepend",
|
|
methodType(long.class, long.class, byte[].class,
|
|
String.class, Wrapper.asPrimitiveType(c), String.class));
|
|
}
|
|
};
|
|
|
|
// This one is deliberately non-lambdified to optimize startup time:
|
|
private static final Function<Class<?>, MethodHandle> MIX = new Function<>() {
|
|
@Override
|
|
public MethodHandle apply(Class<?> c) {
|
|
return JLA.stringConcatHelper("mix", methodType(long.class, long.class, Wrapper.asPrimitiveType(c)));
|
|
}
|
|
};
|
|
|
|
private @Stable static MethodHandle SIMPLE_CONCAT;
|
|
private static MethodHandle simpleConcat() {
|
|
if (SIMPLE_CONCAT == null) {
|
|
SIMPLE_CONCAT = JLA.stringConcatHelper("simpleConcat", methodType(String.class, Object.class, Object.class));
|
|
}
|
|
return SIMPLE_CONCAT;
|
|
}
|
|
|
|
private @Stable static MethodHandle NEW_STRING;
|
|
private static MethodHandle newString() {
|
|
MethodHandle mh = NEW_STRING;
|
|
if (mh == null) {
|
|
NEW_STRING = mh =
|
|
JLA.stringConcatHelper("newString", methodType(String.class, byte[].class, long.class));
|
|
}
|
|
return mh;
|
|
}
|
|
private @Stable static MethodHandle NEW_ARRAY;
|
|
private static MethodHandle newArray() {
|
|
MethodHandle mh = NEW_ARRAY;
|
|
if (mh == null) {
|
|
NEW_ARRAY = mh =
|
|
JLA.stringConcatHelper("newArray", methodType(byte[].class, long.class));
|
|
}
|
|
return mh;
|
|
}
|
|
|
|
private static final ConcurrentMap<Class<?>, MethodHandle> PREPENDERS;
|
|
private static final ConcurrentMap<Class<?>, MethodHandle> MIXERS;
|
|
private static final long INITIAL_CODER;
|
|
|
|
static {
|
|
INITIAL_CODER = JLA.stringConcatInitialCoder();
|
|
PREPENDERS = new ConcurrentHashMap<>();
|
|
MIXERS = new ConcurrentHashMap<>();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Public gateways to public "stringify" methods. These methods have the form String apply(T obj), and normally
|
|
* delegate to {@code String.valueOf}, depending on argument's type.
|
|
*/
|
|
private static final class Stringifiers {
|
|
private Stringifiers() {
|
|
// no instantiation
|
|
}
|
|
|
|
private static final MethodHandle OBJECT_INSTANCE =
|
|
JLA.stringConcatHelper("stringOf", methodType(String.class, Object.class));
|
|
|
|
private static class FloatStringifiers {
|
|
private static final MethodHandle FLOAT_INSTANCE =
|
|
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, float.class);
|
|
|
|
private static final MethodHandle DOUBLE_INSTANCE =
|
|
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, double.class);
|
|
}
|
|
|
|
private static class StringifierAny extends ClassValue<MethodHandle> {
|
|
|
|
private static final ClassValue<MethodHandle> INSTANCE = new StringifierAny();
|
|
|
|
@Override
|
|
protected MethodHandle computeValue(Class<?> cl) {
|
|
if (cl == byte.class || cl == short.class || cl == int.class) {
|
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, int.class);
|
|
} else if (cl == boolean.class) {
|
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, boolean.class);
|
|
} else if (cl == char.class) {
|
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, char.class);
|
|
} else if (cl == long.class) {
|
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, long.class);
|
|
} else {
|
|
MethodHandle mh = forMost(cl);
|
|
if (mh != null) {
|
|
return mh;
|
|
} else {
|
|
throw new IllegalStateException("Unknown class: " + cl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a stringifier for references and floats/doubles only.
|
|
* Always returns null for other primitives.
|
|
*
|
|
* @param t class to stringify
|
|
* @return stringifier; null, if not available
|
|
*/
|
|
static MethodHandle forMost(Class<?> t) {
|
|
if (!t.isPrimitive()) {
|
|
return OBJECT_INSTANCE;
|
|
} else if (t == float.class) {
|
|
return FloatStringifiers.FLOAT_INSTANCE;
|
|
} else if (t == double.class) {
|
|
return FloatStringifiers.DOUBLE_INSTANCE;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns a stringifier for any type. Never returns null.
|
|
*
|
|
* @param t class to stringify
|
|
* @return stringifier
|
|
*/
|
|
static MethodHandle forAny(Class<?> t) {
|
|
return StringifierAny.INSTANCE.get(t);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------- Common utilities ------------------------------------ */
|
|
|
|
static MethodHandle lookupStatic(Lookup lookup, Class<?> refc, String name, Class<?> rtype, Class<?>... ptypes) {
|
|
try {
|
|
return lookup.findStatic(refc, name, MethodType.methodType(rtype, ptypes));
|
|
} catch (NoSuchMethodException | IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
static MethodHandle lookupVirtual(Lookup lookup, Class<?> refc, String name, Class<?> rtype, Class<?>... ptypes) {
|
|
try {
|
|
return lookup.findVirtual(refc, name, MethodType.methodType(rtype, ptypes));
|
|
} catch (NoSuchMethodException | IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
static MethodHandle lookupConstructor(Lookup lookup, Class<?> refc, Class<?> ptypes) {
|
|
try {
|
|
return lookup.findConstructor(refc, MethodType.methodType(void.class, ptypes));
|
|
} catch (NoSuchMethodException | IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
static int estimateSize(Class<?> cl) {
|
|
if (cl == Integer.TYPE) {
|
|
return 11; // "-2147483648"
|
|
} else if (cl == Boolean.TYPE) {
|
|
return 5; // "false"
|
|
} else if (cl == Byte.TYPE) {
|
|
return 4; // "-128"
|
|
} else if (cl == Character.TYPE) {
|
|
return 1; // duh
|
|
} else if (cl == Short.TYPE) {
|
|
return 6; // "-32768"
|
|
} else if (cl == Double.TYPE) {
|
|
return 26; // apparently, no larger than this, see FloatingDecimal.BinaryToASCIIBuffer.buffer
|
|
} else if (cl == Float.TYPE) {
|
|
return 26; // apparently, no larger than this, see FloatingDecimal.BinaryToASCIIBuffer.buffer
|
|
} else if (cl == Long.TYPE) {
|
|
return 20; // "-9223372036854775808"
|
|
} else {
|
|
throw new IllegalArgumentException("Cannot estimate the size for " + cl);
|
|
}
|
|
}
|
|
|
|
static Class<?> adaptToStringBuilder(Class<?> c) {
|
|
if (c.isPrimitive()) {
|
|
if (c == Byte.TYPE || c == Short.TYPE) {
|
|
return int.class;
|
|
}
|
|
} else {
|
|
if (c != String.class) {
|
|
return Object.class;
|
|
}
|
|
}
|
|
return c;
|
|
}
|
|
|
|
private StringConcatFactory() {
|
|
// no instantiation
|
|
}
|
|
|
|
}
|