|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package java.lang.invoke; |
|
|
|
import jdk.internal.misc.Unsafe; |
|
import jdk.internal.org.objectweb.asm.ClassWriter; |
|
import jdk.internal.org.objectweb.asm.Label; |
|
import jdk.internal.org.objectweb.asm.MethodVisitor; |
|
import jdk.internal.org.objectweb.asm.Opcodes; |
|
import jdk.internal.vm.annotation.ForceInline; |
|
import sun.invoke.util.Wrapper; |
|
import sun.security.action.GetPropertyAction; |
|
|
|
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.Properties; |
|
import java.util.concurrent.ConcurrentHashMap; |
|
import java.util.concurrent.ConcurrentMap; |
|
import java.util.function.Function; |
|
|
|
import static jdk.internal.org.objectweb.asm.Opcodes.*; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public final class StringConcatFactory { |
|
|
|
|
|
|
|
*/ |
|
private static final char TAG_ARG = '\u0001'; |
|
|
|
|
|
|
|
*/ |
|
private static final char TAG_CONST = '\u0002'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private static final int MAX_INDY_CONCAT_ARG_SLOTS = 200; |
|
|
|
|
|
|
|
|
|
*/ |
|
private static Strategy STRATEGY; |
|
|
|
|
|
|
|
*/ |
|
private static final Strategy DEFAULT_STRATEGY = Strategy.MH_INLINE_SIZED_EXACT; |
|
|
|
private enum Strategy { |
|
|
|
|
|
*/ |
|
BC_SB, |
|
|
|
|
|
|
|
|
|
*/ |
|
BC_SB_SIZED, |
|
|
|
|
|
|
|
|
|
*/ |
|
BC_SB_SIZED_EXACT, |
|
|
|
|
|
|
|
|
|
*/ |
|
MH_SB_SIZED, |
|
|
|
|
|
|
|
|
|
*/ |
|
MH_SB_SIZED_EXACT, |
|
|
|
|
|
|
|
|
|
*/ |
|
MH_INLINE_SIZED_EXACT |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static final boolean DEBUG; |
|
|
|
|
|
|
|
|
|
*/ |
|
private static final boolean CACHE_ENABLE; |
|
|
|
private static final ConcurrentMap<Key, MethodHandle> CACHE; |
|
|
|
|
|
|
|
*/ |
|
private static final ProxyClassesDumper DUMPER; |
|
|
|
static { |
|
// In case we need to double-back onto the StringConcatFactory during this |
|
// static initialization, make sure we have the reasonable defaults to complete |
|
// the static initialization properly. After that, actual users would use |
|
|
|
STRATEGY = DEFAULT_STRATEGY; |
|
// CACHE_ENABLE = false; // implied |
|
// CACHE = null; // implied |
|
// DEBUG = false; // implied |
|
// DUMPER = null; // implied |
|
|
|
Properties props = GetPropertyAction.privilegedGetProperties(); |
|
final String strategy = |
|
props.getProperty("java.lang.invoke.stringConcat"); |
|
CACHE_ENABLE = Boolean.parseBoolean( |
|
props.getProperty("java.lang.invoke.stringConcat.cache")); |
|
DEBUG = Boolean.parseBoolean( |
|
props.getProperty("java.lang.invoke.stringConcat.debug")); |
|
final String dumpPath = |
|
props.getProperty("java.lang.invoke.stringConcat.dumpClasses"); |
|
|
|
STRATEGY = (strategy == null) ? DEFAULT_STRATEGY : Strategy.valueOf(strategy); |
|
CACHE = CACHE_ENABLE ? new ConcurrentHashMap<>() : null; |
|
DUMPER = (dumpPath == null) ? null : ProxyClassesDumper.getInstance(dumpPath); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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 |
|
|
|
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 |
|
|
|
acc.append(c); |
|
} |
|
} |
|
|
|
|
|
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 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 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. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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) { |
|
|
|
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); |
|
} |
|
|
|
String className = getClassName(lookup.lookupClass()); |
|
MethodType mt = adaptType(concatType); |
|
Recipe rec = new Recipe(recipe, constants); |
|
|
|
MethodHandle mh; |
|
if (CACHE_ENABLE) { |
|
Key key = new Key(className, mt, rec); |
|
mh = CACHE.get(key); |
|
if (mh == null) { |
|
mh = generate(lookup, className, mt, rec); |
|
CACHE.put(key, mh); |
|
} |
|
} else { |
|
mh = generate(lookup, className, mt, rec); |
|
} |
|
return new ConstantCallSite(mh.asType(concatType)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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) { |
|
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 String getClassName(Class<?> hostClass) throws StringConcatException { |
|
/* |
|
When cache is enabled, we want to cache as much as we can. |
|
|
|
However, there are two peculiarities: |
|
|
|
a) The generated class should stay within the same package as the |
|
host class, to allow Unsafe.defineAnonymousClass access controls |
|
to work properly. JDK may choose to fail with IllegalAccessException |
|
when accessing a VM anonymous class with non-privileged callers, |
|
see JDK-8058575. |
|
|
|
b) If we mark the stub with some prefix, say, derived from the package |
|
name because of (a), we can technically use that stub in other packages. |
|
But the call stack traces would be extremely puzzling to unsuspecting users |
|
and profiling tools: whatever stub wins the race, would be linked in all |
|
similar callsites. |
|
|
|
Therefore, we set the class prefix to match the host class package, and use |
|
the prefix as the cache key too. This only affects BC_* strategies, and only when |
|
cache is enabled. |
|
*/ |
|
|
|
switch (STRATEGY) { |
|
case BC_SB: |
|
case BC_SB_SIZED: |
|
case BC_SB_SIZED_EXACT: { |
|
if (CACHE_ENABLE) { |
|
String pkgName = hostClass.getPackageName(); |
|
return (pkgName != null && !pkgName.isEmpty() ? pkgName.replace('.', '/') + "/" : "") + "Stubs$$StringConcat"; |
|
} else { |
|
return hostClass.getName().replace('.', '/') + "$$StringConcat"; |
|
} |
|
} |
|
case MH_SB_SIZED: |
|
case MH_SB_SIZED_EXACT: |
|
case MH_INLINE_SIZED_EXACT: |
|
|
|
return ""; |
|
default: |
|
throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented"); |
|
} |
|
} |
|
|
|
private static MethodHandle generate(Lookup lookup, String className, MethodType mt, Recipe recipe) throws StringConcatException { |
|
try { |
|
switch (STRATEGY) { |
|
case BC_SB: |
|
return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.DEFAULT); |
|
case BC_SB_SIZED: |
|
return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED); |
|
case BC_SB_SIZED_EXACT: |
|
return BytecodeStringBuilderStrategy.generate(lookup, className, 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) { |
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private static final class BytecodeStringBuilderStrategy { |
|
static final Unsafe UNSAFE = Unsafe.getUnsafe(); |
|
static final int CLASSFILE_VERSION = 52; |
|
static final String METHOD_NAME = "concat"; |
|
|
|
private BytecodeStringBuilderStrategy() { |
|
// no instantiation |
|
} |
|
|
|
private static MethodHandle generate(Lookup lookup, String className, MethodType args, Recipe recipe, Mode mode) throws Exception { |
|
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); |
|
|
|
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: |
|
|
|
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 |
|
|
|
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()); |
|
} |
|
} |
|
} |
|
|
|
|
|
mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); |
|
mv.visitInsn(DUP); |
|
|
|
if (mode.isSized()) { |
|
|
|
|
|
|
|
|
|
*/ |
|
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: |
|
|
|
|
|
|
|
|
|
*/ |
|
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()); |
|
} |
|
} |
|
|
|
|
|
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 |
|
); |
|
} |
|
|
|
|
|
{ |
|
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<?> hostClass = lookup.lookupClass(); |
|
Class<?> innerClass = UNSAFE.defineAnonymousClass(hostClass, classBytes, null); |
|
UNSAFE.ensureClassInitialized(innerClass); |
|
dumpIfEnabled(innerClass.getName(), classBytes); |
|
return Lookup.IMPL_LOOKUP.findStatic(innerClass, METHOD_NAME, args); |
|
} catch (Exception e) { |
|
dumpIfEnabled(className + "$$FAILED", classBytes); |
|
throw new StringConcatException("Exception while spinning the class", e); |
|
} |
|
} |
|
|
|
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;"; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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 |
|
|
|
filter = Stringifiers.forMost(ptypes[i]); |
|
break; |
|
case SIZED_EXACT: |
|
// In exact mode, we convert everything to String: |
|
|
|
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 |
|
|
|
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()); |
|
} |
|
} |
|
|
|
|
|
MethodHandle builder = MethodHandles.dropArguments(MethodHandles.identity(StringBuilder.class), 1, ptypes); |
|
|
|
// Compose append calls. This is done in reverse because the application order is |
|
|
|
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. |
|
|
|
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. |
|
|
|
MethodHandle sum = getReducerFor(pc + 1); |
|
MethodHandle adder = MethodHandles.insertArguments(sum, 0, initial); |
|
|
|
|
|
adder = MethodHandles.filterArguments(adder, 0, lengthers); |
|
|
|
|
|
MethodHandle newBuilder = MethodHandles.filterReturnValue(adder, NEW_STRING_BUILDER); |
|
|
|
|
|
MethodHandle mh = MethodHandles.foldArguments(builder, newBuilder); |
|
|
|
|
|
mh = MethodHandles.filterArguments(mh, 0, filters); |
|
|
|
|
|
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)); |
|
|
|
|
|
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 ConcurrentMap<Integer, MethodHandle> SUMMERS; |
|
|
|
|
|
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, |
|
|
|
Class<?>[] cls = new Class<?>[cnt]; |
|
Arrays.fill(cls, int.class); |
|
return lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleStringBuilderStrategy.class, "sum", int.class, cls); |
|
} else { |
|
return lookupStatic(Lookup.IMPL_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(MethodHandles.Lookup.IMPL_LOOKUP, |
|
MethodHandleStringBuilderStrategy.class, "toStringChecked", String.class, StringBuilder.class); |
|
} else { |
|
BUILDER_TO_STRING_CHECKED = null; |
|
} |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private static final class MethodHandleInlineCopyStrategy { |
|
static final Unsafe UNSAFE = Unsafe.getUnsafe(); |
|
|
|
private MethodHandleInlineCopyStrategy() { |
|
// no instantiation |
|
} |
|
|
|
static MethodHandle generate(MethodType mt, Recipe recipe) throws Throwable { |
|
|
|
// 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). |
|
|
|
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 (int, byte[], byte)String in String helper. The combinators are assembled bottom-up, |
|
// which makes the code arguably hard to read. |
|
|
|
|
|
MethodHandle mh; |
|
|
|
mh = MethodHandles.dropArguments(NEW_STRING, 3, ptypes); |
|
|
|
// Mix in prependers. This happens when (byte[], int, byte) = (storage, index, coder) is already |
|
// known from the combinators below. We are assembling the string backwards, so "index" is the |
|
|
|
for (RecipeElement el : recipe.getElements()) { |
|
|
|
mh = MethodHandles.dropArguments(mh, 2, int.class); |
|
switch (el.getTag()) { |
|
case TAG_CONST: { |
|
MethodHandle prepender = MethodHandles.insertArguments(prepender(String.class), 3, el.getValue()); |
|
mh = MethodHandles.foldArguments(mh, 1, prepender, |
|
2, 0, 3 |
|
); |
|
break; |
|
} |
|
case TAG_ARG: { |
|
int pos = el.getArgPos(); |
|
MethodHandle prepender = prepender(ptypes[pos]); |
|
mh = MethodHandles.foldArguments(mh, 1, prepender, |
|
2, 0, 3, |
|
4 + pos |
|
); |
|
break; |
|
} |
|
default: |
|
throw new StringConcatException("Unhandled tag: " + el.getTag()); |
|
} |
|
} |
|
|
|
|
|
mh = MethodHandles.foldArguments(mh, 0, NEW_ARRAY, |
|
1, 2 |
|
); |
|
|
|
// 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 primitive specializations in CODER_MIXERS. |
|
// |
|
// The method handle shape after all length and coder mixers is: |
|
|
|
byte initialCoder = INITIAL_CODER; |
|
int initialLen = 0; |
|
for (RecipeElement el : recipe.getElements()) { |
|
switch (el.getTag()) { |
|
case TAG_CONST: |
|
String constant = el.getValue(); |
|
initialCoder = (byte) coderMixer(String.class).invoke(initialCoder, constant); |
|
initialLen += constant.length(); |
|
break; |
|
case TAG_ARG: |
|
int ac = el.getArgPos(); |
|
|
|
Class<?> argClass = ptypes[ac]; |
|
MethodHandle lm = lengthMixer(argClass); |
|
MethodHandle cm = coderMixer(argClass); |
|
|
|
// Read this bottom up: |
|
|
|
|
|
mh = MethodHandles.dropArguments(mh, 2, int.class, byte.class); |
|
|
|
// 3. Compute "new-index", producing ("new-index", "new-coder", "old-index", "old-coder", <args>) |
|
|
|
mh = MethodHandles.foldArguments(mh, 0, lm, |
|
2, |
|
4 + ac |
|
); |
|
|
|
// 2. Compute "new-coder", producing ("new-coder", "old-index", "old-coder", <args>) |
|
|
|
mh = MethodHandles.foldArguments(mh, 0, cm, |
|
2, |
|
3 + ac |
|
); |
|
|
|
|
|
break; |
|
default: |
|
throw new StringConcatException("Unhandled tag: " + el.getTag()); |
|
} |
|
} |
|
|
|
// Insert initial lengths and coders here. |
|
|
|
mh = MethodHandles.insertArguments(mh, 0, initialLen, initialCoder); |
|
|
|
|
|
if (filters != null) { |
|
mh = MethodHandles.filterArguments(mh, 0, filters); |
|
} |
|
|
|
return mh; |
|
} |
|
|
|
@ForceInline |
|
private static byte[] newArray(int length, byte coder) { |
|
return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length << coder); |
|
} |
|
|
|
private static MethodHandle prepender(Class<?> cl) { |
|
return PREPENDERS.computeIfAbsent(cl, PREPEND); |
|
} |
|
|
|
private static MethodHandle coderMixer(Class<?> cl) { |
|
return CODER_MIXERS.computeIfAbsent(cl, CODER_MIX); |
|
} |
|
|
|
private static MethodHandle lengthMixer(Class<?> cl) { |
|
return LENGTH_MIXERS.computeIfAbsent(cl, LENGTH_MIX); |
|
} |
|
|
|
|
|
private static final Function<Class<?>, MethodHandle> PREPEND = new Function<Class<?>, MethodHandle>() { |
|
@Override |
|
public MethodHandle apply(Class<?> c) { |
|
return lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "prepend", int.class, int.class, byte[].class, byte.class, |
|
Wrapper.asPrimitiveType(c)); |
|
} |
|
}; |
|
|
|
|
|
private static final Function<Class<?>, MethodHandle> CODER_MIX = new Function<Class<?>, MethodHandle>() { |
|
@Override |
|
public MethodHandle apply(Class<?> c) { |
|
return lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "mixCoder", byte.class, byte.class, |
|
Wrapper.asPrimitiveType(c)); |
|
} |
|
}; |
|
|
|
|
|
private static final Function<Class<?>, MethodHandle> LENGTH_MIX = new Function<Class<?>, MethodHandle>() { |
|
@Override |
|
public MethodHandle apply(Class<?> c) { |
|
return lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "mixLen", int.class, int.class, |
|
Wrapper.asPrimitiveType(c)); |
|
} |
|
}; |
|
|
|
private static final MethodHandle NEW_STRING; |
|
private static final MethodHandle NEW_ARRAY; |
|
private static final ConcurrentMap<Class<?>, MethodHandle> PREPENDERS; |
|
private static final ConcurrentMap<Class<?>, MethodHandle> LENGTH_MIXERS; |
|
private static final ConcurrentMap<Class<?>, MethodHandle> CODER_MIXERS; |
|
private static final byte INITIAL_CODER; |
|
static final Class<?> STRING_HELPER; |
|
|
|
static { |
|
try { |
|
STRING_HELPER = Class.forName("java.lang.StringConcatHelper"); |
|
MethodHandle initCoder = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "initialCoder", byte.class); |
|
INITIAL_CODER = (byte) initCoder.invoke(); |
|
} catch (Throwable e) { |
|
throw new AssertionError(e); |
|
} |
|
|
|
PREPENDERS = new ConcurrentHashMap<>(); |
|
LENGTH_MIXERS = new ConcurrentHashMap<>(); |
|
CODER_MIXERS = new ConcurrentHashMap<>(); |
|
|
|
NEW_STRING = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "newString", String.class, byte[].class, int.class, byte.class); |
|
NEW_ARRAY = lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleInlineCopyStrategy.class, "newArray", byte[].class, int.class, byte.class); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static final class Stringifiers { |
|
private Stringifiers() { |
|
// no instantiation |
|
} |
|
|
|
private static class StringifierMost extends ClassValue<MethodHandle> { |
|
@Override |
|
protected MethodHandle computeValue(Class<?> cl) { |
|
if (cl == String.class) { |
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, Object.class); |
|
} else if (cl == float.class) { |
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, float.class); |
|
} else if (cl == double.class) { |
|
return lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, double.class); |
|
} else if (!cl.isPrimitive()) { |
|
MethodHandle mhObject = lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, Object.class); |
|
|
|
// We need the additional conversion here, because String.valueOf(Object) may return null. |
|
// String conversion rules in Java state we need to produce "null" String in this case. |
|
|
|
return MethodHandles.filterReturnValue(mhObject, |
|
mhObject.asType(MethodType.methodType(String.class, String.class))); |
|
} |
|
|
|
return null; |
|
} |
|
} |
|
|
|
private static class StringifierAny extends ClassValue<MethodHandle> { |
|
@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 = STRINGIFIERS_MOST.get(cl); |
|
if (mh != null) { |
|
return mh; |
|
} else { |
|
throw new IllegalStateException("Unknown class: " + cl); |
|
} |
|
} |
|
} |
|
} |
|
|
|
private static final ClassValue<MethodHandle> STRINGIFIERS_MOST = new StringifierMost(); |
|
private static final ClassValue<MethodHandle> STRINGIFIERS_ANY = new StringifierAny(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
static MethodHandle forMost(Class<?> t) { |
|
return STRINGIFIERS_MOST.get(t); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
static MethodHandle forAny(Class<?> t) { |
|
return STRINGIFIERS_ANY.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; |
|
} else if (cl == Boolean.TYPE) { |
|
return 5; |
|
} else if (cl == Byte.TYPE) { |
|
return 4; |
|
} else if (cl == Character.TYPE) { |
|
return 1; |
|
} else if (cl == Short.TYPE) { |
|
return 6; |
|
} else if (cl == Double.TYPE) { |
|
return 26; |
|
} else if (cl == Float.TYPE) { |
|
return 26; |
|
} else if (cl == Long.TYPE) { |
|
return 20; |
|
} 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 |
|
} |
|
|
|
} |