openjdk/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
2020-05-20 11:35:58 +02:00

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
}
}