Archiviert
13
0

Complicated feature - auto-compilation of structure modifier.

Using ASM we can automatically generate a faster structure modifier
that doesn't use reflection to read or write public fields.

Note that because the compilation itself is a bit slow (10 ms++), 
we have to create a background compilation thread. Future work:
 * Disable the thread if it's idle after 60 seconds.
 * Don't recreate the thread when the server reloads.
Dieser Commit ist enthalten in:
Kristian S. Stangeland 2012-09-27 03:24:34 +02:00
Ursprung 90970d1b9b
Commit 240df9dc7a
7 geänderte Dateien mit 1189 neuen und 16 gelöschten Zeilen

Datei anzeigen

@ -25,6 +25,7 @@ import org.bukkit.Server;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import com.comphenix.protocol.compiler.BackgroundCompiler;
import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.injector.PacketFilterManager;
import com.comphenix.protocol.metrics.Statistics; import com.comphenix.protocol.metrics.Statistics;
@ -39,6 +40,9 @@ public class ProtocolLibrary extends JavaPlugin {
// Metrics and statistisc // Metrics and statistisc
private Statistics statistisc; private Statistics statistisc;
// Structure compiler
private BackgroundCompiler backgroundCompiler;
@Override @Override
public void onLoad() { public void onLoad() {
logger = getLoggerSafely(); logger = getLoggerSafely();
@ -50,6 +54,12 @@ public class ProtocolLibrary extends JavaPlugin {
Server server = getServer(); Server server = getServer();
PluginManager manager = server.getPluginManager(); PluginManager manager = server.getPluginManager();
// Initialize background compiler
if (backgroundCompiler == null) {
backgroundCompiler = new BackgroundCompiler(getClassLoader());
BackgroundCompiler.setInstance(backgroundCompiler);
}
// Notify server managers of incompatible plugins // Notify server managers of incompatible plugins
checkForIncompatibility(manager); checkForIncompatibility(manager);
@ -83,6 +93,13 @@ public class ProtocolLibrary extends JavaPlugin {
@Override @Override
public void onDisable() { public void onDisable() {
// Disable compiler
if (backgroundCompiler != null) {
backgroundCompiler.shutdownAll();
backgroundCompiler = null;
BackgroundCompiler.setInstance(null);
}
protocolManager.close(); protocolManager.close();
protocolManager = null; protocolManager = null;
statistisc = null; statistisc = null;

Datei anzeigen

@ -0,0 +1,162 @@
package com.comphenix.protocol.compiler;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.comphenix.protocol.reflect.StructureModifier;
/**
* Compiles structure modifiers on a background thread.
* <p>
* This is necessary as we cannot block the main thread.
*
* @author Kristian
*/
public class BackgroundCompiler {
// How long to wait for a shutdown
public static final int SHUTDOWN_DELAY_MS = 2000;
// The single background compiler we're using
private static BackgroundCompiler backgroundCompiler;
private StructureCompiler compiler;
private boolean enabled;
private boolean shuttingDown;
private ExecutorService executor;
/**
* Retrieves the current background compiler.
* @return Current background compiler.
*/
public static BackgroundCompiler getInstance() {
return backgroundCompiler;
}
/**
* Sets the single background compiler we're using.
* @param backgroundCompiler - current background compiler, or NULL if the library is not loaded.
*/
public static void setInstance(BackgroundCompiler backgroundCompiler) {
BackgroundCompiler.backgroundCompiler = backgroundCompiler;
}
/**
* Initialize a background compiler.
* @param loader - class loader from Bukkit.
*/
public BackgroundCompiler(ClassLoader loader) {
this(loader, Executors.newSingleThreadExecutor());
}
/**
* Initialize a background compiler utilizing the given thread pool.
* @param loader - class loader from Bukkit.
* @param executor - thread pool we'll use.
*/
public BackgroundCompiler(ClassLoader loader, ExecutorService executor) {
if (loader == null)
throw new IllegalArgumentException("loader cannot be NULL");
if (executor == null)
throw new IllegalArgumentException("executor cannot be NULL");
this.compiler = new StructureCompiler(loader);
this.executor = executor;
this.enabled = true;
}
/**
* Ensure that the indirectly given structure modifier is eventually compiled.
* @param cache - store of structure modifiers.
* @param key - key of the structure modifier to compile.
*/
@SuppressWarnings("rawtypes")
public void scheduleCompilation(final Map<Class, StructureModifier> cache, final Class key) {
// Only schedule if we're enabled
if (enabled && !shuttingDown) {
// Don't try to schedule anything
if (executor == null || executor.isShutdown())
return;
try {
executor.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
StructureModifier<?> modifier = cache.get(key);
// Update the cache!
modifier = compiler.compile(modifier);
cache.put(key, modifier);
// We'll also return the new structure modifier
return modifier;
}
});
} catch (RejectedExecutionException e) {
// Occures when the underlying queue is overflowing. Since the compilation
// is only an optmization and not really essential we'll just log this failure
// and move on.
Logger.getLogger("Minecraft").log(Level.WARNING, "Unable to schedule compilation task.", e);
}
}
}
/**
* Clean up after ourselves using the default timeout.
*/
public void shutdownAll() {
shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS);
}
/**
* Clean up after ourselves.
* @param timeout - the maximum time to wait.
* @param unit - the time unit of the timeout argument.
*/
public void shutdownAll(long timeout, TimeUnit unit) {
setEnabled(false);
shuttingDown = true;
executor.shutdown();
try {
executor.awaitTermination(timeout, unit);
} catch (InterruptedException e) {
// Unlikely to ever occur.
e.printStackTrace();
}
}
/**
* Retrieve whether or not the background compiler is enabled.
* @return TRUE if it is enabled, FALSE otherwise.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether or not the background compiler is enabled.
* @param enabled - TRUE to enable it, FALSE otherwise.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Retrieve the current structure compiler.
* @return Current structure compiler.
*/
public StructureCompiler getCompiler() {
return compiler;
}
}

Datei anzeigen

@ -0,0 +1,275 @@
package com.comphenix.protocol.compiler;
import net.sf.cglib.asm.*;
/**
* Used by the compiler to automatically box and unbox values.
*/
class BoxingHelper {
private final static Type BYTE_TYPE = Type.getObjectType("java/lang/Byte");
private final static Type BOOLEAN_TYPE = Type.getObjectType("java/lang/Boolean");
private final static Type SHORT_TYPE = Type.getObjectType("java/lang/Short");
private final static Type CHARACTER_TYPE = Type.getObjectType("java/lang/Character");
private final static Type INTEGER_TYPE = Type.getObjectType("java/lang/Integer");
private final static Type FLOAT_TYPE = Type.getObjectType("java/lang/Float");
private final static Type LONG_TYPE = Type.getObjectType("java/lang/Long");
private final static Type DOUBLE_TYPE = Type.getObjectType("java/lang/Double");
private final static Type NUMBER_TYPE = Type.getObjectType("java/lang/Number");
private final static Type OBJECT_TYPE = Type.getObjectType("java/lang/Object");
private final static MethodDescriptor BOOLEAN_VALUE = MethodDescriptor.getMethod("boolean booleanValue()");
private final static MethodDescriptor CHAR_VALUE = MethodDescriptor.getMethod("char charValue()");
private final static MethodDescriptor INT_VALUE = MethodDescriptor.getMethod("int intValue()");
private final static MethodDescriptor FLOAT_VALUE = MethodDescriptor.getMethod("float floatValue()");
private final static MethodDescriptor LONG_VALUE = MethodDescriptor.getMethod("long longValue()");
private final static MethodDescriptor DOUBLE_VALUE = MethodDescriptor.getMethod("double doubleValue()");
private MethodVisitor mv;
public BoxingHelper(MethodVisitor mv) {
this.mv = mv;
}
/**
* Generates the instructions to box the top stack value. This value is
* replaced by its boxed equivalent on top of the stack.
*
* @param type the type of the top stack value.
*/
public void box(final Type type){
if(type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
return;
}
if(type == Type.VOID_TYPE) {
push((String) null);
} else {
Type boxed = type;
switch(type.getSort()) {
case Type.BYTE:
boxed = BYTE_TYPE;
break;
case Type.BOOLEAN:
boxed = BOOLEAN_TYPE;
break;
case Type.SHORT:
boxed = SHORT_TYPE;
break;
case Type.CHAR:
boxed = CHARACTER_TYPE;
break;
case Type.INT:
boxed = INTEGER_TYPE;
break;
case Type.FLOAT:
boxed = FLOAT_TYPE;
break;
case Type.LONG:
boxed = LONG_TYPE;
break;
case Type.DOUBLE:
boxed = DOUBLE_TYPE;
break;
}
newInstance(boxed);
if(type.getSize() == 2) {
// Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
dupX2();
dupX2();
pop();
} else {
// p -> po -> opo -> oop -> o
dupX1();
swap();
}
invokeConstructor(boxed, new MethodDescriptor("<init>", Type.VOID_TYPE, new Type[] {type}));
}
}
/**
* Generates the instruction to invoke a constructor.
*
* @param type the class in which the constructor is defined.
* @param method the constructor to be invoked.
*/
public void invokeConstructor(final Type type, final MethodDescriptor method){
invokeInsn(Opcodes.INVOKESPECIAL, type, method);
}
/**
* Generates a DUP_X1 instruction.
*/
public void dupX1(){
mv.visitInsn(Opcodes.DUP_X1);
}
/**
* Generates a DUP_X2 instruction.
*/
public void dupX2(){
mv.visitInsn(Opcodes.DUP_X2);
}
/**
* Generates a POP instruction.
*/
public void pop(){
mv.visitInsn(Opcodes.POP);
}
/**
* Generates a SWAP instruction.
*/
public void swap(){
mv.visitInsn(Opcodes.SWAP);
}
/**
* Generates the instruction to push the given value on the stack.
*
* @param value the value to be pushed on the stack.
*/
public void push(final boolean value){
push(value ? 1 : 0);
}
/**
* Generates the instruction to push the given value on the stack.
*
* @param value the value to be pushed on the stack.
*/
public void push(final int value) {
if (value >= -1 && value <= 5) {
mv.visitInsn(Opcodes.ICONST_0 + value);
} else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
mv.visitIntInsn(Opcodes.BIPUSH, value);
} else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
mv.visitIntInsn(Opcodes.SIPUSH, value);
} else {
mv.visitLdcInsn(new Integer(value));
}
}
/**
* Generates the instruction to create a new object.
*
* @param type the class of the object to be created.
*/
public void newInstance(final Type type){
typeInsn(Opcodes.NEW, type);
}
/**
* Generates the instruction to push the given value on the stack.
*
* @param value the value to be pushed on the stack. May be <tt>null</tt>.
*/
public void push(final String value) {
if (value == null) {
mv.visitInsn(Opcodes.ACONST_NULL);
} else {
mv.visitLdcInsn(value);
}
}
/**
* Generates the instructions to unbox the top stack value. This value is
* replaced by its unboxed equivalent on top of the stack.
*
* @param type
* the type of the top stack value.
*/
public void unbox(final Type type){
Type t = NUMBER_TYPE;
MethodDescriptor sig = null;
switch(type.getSort()) {
case Type.VOID:
return;
case Type.CHAR:
t = CHARACTER_TYPE;
sig = CHAR_VALUE;
break;
case Type.BOOLEAN:
t = BOOLEAN_TYPE;
sig = BOOLEAN_VALUE;
break;
case Type.DOUBLE:
sig = DOUBLE_VALUE;
break;
case Type.FLOAT:
sig = FLOAT_VALUE;
break;
case Type.LONG:
sig = LONG_VALUE;
break;
case Type.INT:
case Type.SHORT:
case Type.BYTE:
sig = INT_VALUE;
}
if(sig == null) {
checkCast(type);
} else {
checkCast(t);
invokeVirtual(t, sig);
}
}
/**
* Generates the instruction to check that the top stack value is of the
* given type.
*
* @param type a class or interface type.
*/
public void checkCast(final Type type){
if(!type.equals(OBJECT_TYPE)) {
typeInsn(Opcodes.CHECKCAST, type);
}
}
/**
* Generates the instruction to invoke a normal method.
*
* @param owner the class in which the method is defined.
* @param method the method to be invoked.
*/
public void invokeVirtual(final Type owner, final MethodDescriptor method){
invokeInsn(Opcodes.INVOKEVIRTUAL, owner, method);
}
/**
* Generates an invoke method instruction.
*
* @param opcode the instruction's opcode.
* @param type the class in which the method is defined.
* @param method the method to be invoked.
*/
private void invokeInsn(final int opcode, final Type type, final MethodDescriptor method){
String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName();
mv.visitMethodInsn(opcode, owner, method.getName(), method.getDescriptor());
}
/**
* Generates a type dependent instruction.
*
* @param opcode the instruction's opcode.
* @param type the instruction's operand.
*/
private void typeInsn(final int opcode, final Type type){
String desc;
if(type.getSort() == Type.ARRAY) {
desc = type.getDescriptor();
} else {
desc = type.getInternalName();
}
mv.visitTypeInsn(opcode, desc);
}
}

Datei anzeigen

@ -0,0 +1,45 @@
package com.comphenix.protocol.compiler;
import java.lang.reflect.Field;
import java.util.Map;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.instances.DefaultInstances;
/**
* Represents a compiled structure modifier.
*
* @author Kristian
* @param <TField> Field type.
*/
public class CompiledStructureModifier<TField> extends StructureModifier<TField> {
// Used to compile instances of structure modifiers
protected StructureCompiler compiler;
// Speed up the default writer
@SuppressWarnings("unchecked")
@Override
public StructureModifier<TField> writeDefaults() throws FieldAccessException {
DefaultInstances generator = DefaultInstances.DEFAULT;
// Write a default instance to every field
for (Map.Entry<Field, Integer> entry : defaultFields.entrySet()) {
Integer index = entry.getValue();
Field field = entry.getKey();
write(index, (TField) generator.getDefault(field.getType()));
}
return this;
}
@Override
public StructureModifier<TField> withTarget(Object target) {
if (compiler != null)
return compiler.compile(super.withTarget(target));
else
return super.withTarget(target);
}
}

Datei anzeigen

@ -0,0 +1,220 @@
package com.comphenix.protocol.compiler;
import java.util.HashMap;
import java.util.Map;
import net.sf.cglib.asm.Type;
/**
* Represents a method.
*/
class MethodDescriptor {
/**
* The method name.
*/
private final String name;
/**
* The method descriptor.
*/
private final String desc;
/**
* Maps primitive Java type names to their descriptors.
*/
private static final Map<String, String> DESCRIPTORS;
static {
DESCRIPTORS = new HashMap<String, String>();
DESCRIPTORS.put("void", "V");
DESCRIPTORS.put("byte", "B");
DESCRIPTORS.put("char", "C");
DESCRIPTORS.put("double", "D");
DESCRIPTORS.put("float", "F");
DESCRIPTORS.put("int", "I");
DESCRIPTORS.put("long", "J");
DESCRIPTORS.put("short", "S");
DESCRIPTORS.put("boolean", "Z");
}
/**
* Creates a new {@link Method}.
*
* @param name the method's name.
* @param desc the method's descriptor.
*/
public MethodDescriptor(final String name, final String desc) {
this.name = name;
this.desc = desc;
}
/**
* Creates a new {@link Method}.
*
* @param name the method's name.
* @param returnType the method's return type.
* @param argumentTypes the method's argument types.
*/
public MethodDescriptor(
final String name,
final Type returnType,
final Type[] argumentTypes)
{
this(name, Type.getMethodDescriptor(returnType, argumentTypes));
}
/**
* Returns a {@link Method} corresponding to the given Java method
* declaration.
*
* @param method a Java method declaration, without argument names, of the
* form "returnType name (argumentType1, ... argumentTypeN)", where
* the types are in plain Java (e.g. "int", "float",
* "java.util.List", ...). Classes of the java.lang package can be
* specified by their unqualified name; all other classes names must
* be fully qualified.
* @return a {@link Method} corresponding to the given Java method
* declaration.
* @throws IllegalArgumentException if <code>method</code> could not get
* parsed.
*/
public static MethodDescriptor getMethod(final String method)
throws IllegalArgumentException
{
return getMethod(method, false);
}
/**
* Returns a {@link Method} corresponding to the given Java method
* declaration.
*
* @param method a Java method declaration, without argument names, of the
* form "returnType name (argumentType1, ... argumentTypeN)", where
* the types are in plain Java (e.g. "int", "float",
* "java.util.List", ...). Classes of the java.lang package may be
* specified by their unqualified name, depending on the
* defaultPackage argument; all other classes names must be fully
* qualified.
* @param defaultPackage true if unqualified class names belong to the
* default package, or false if they correspond to java.lang classes.
* For instance "Object" means "Object" if this option is true, or
* "java.lang.Object" otherwise.
* @return a {@link Method} corresponding to the given Java method
* declaration.
* @throws IllegalArgumentException if <code>method</code> could not get
* parsed.
*/
public static MethodDescriptor getMethod(
final String method,
final boolean defaultPackage) throws IllegalArgumentException
{
int space = method.indexOf(' ');
int start = method.indexOf('(', space) + 1;
int end = method.indexOf(')', start);
if (space == -1 || start == -1 || end == -1) {
throw new IllegalArgumentException();
}
String returnType = method.substring(0, space);
String methodName = method.substring(space + 1, start - 1).trim();
StringBuffer sb = new StringBuffer();
sb.append('(');
int p;
do {
String s;
p = method.indexOf(',', start);
if (p == -1) {
s = map(method.substring(start, end).trim(), defaultPackage);
} else {
s = map(method.substring(start, p).trim(), defaultPackage);
start = p + 1;
}
sb.append(s);
} while (p != -1);
sb.append(')');
sb.append(map(returnType, defaultPackage));
return new MethodDescriptor(methodName, sb.toString());
}
private static String map(final String type, final boolean defaultPackage) {
if ("".equals(type)) {
return type;
}
StringBuffer sb = new StringBuffer();
int index = 0;
while ((index = type.indexOf("[]", index) + 1) > 0) {
sb.append('[');
}
String t = type.substring(0, type.length() - sb.length() * 2);
String desc = (String) DESCRIPTORS.get(t);
if (desc != null) {
sb.append(desc);
} else {
sb.append('L');
if (t.indexOf('.') < 0) {
if (!defaultPackage) {
sb.append("java/lang/");
}
sb.append(t);
} else {
sb.append(t.replace('.', '/'));
}
sb.append(';');
}
return sb.toString();
}
/**
* Returns the name of the method described by this object.
*
* @return the name of the method described by this object.
*/
public String getName() {
return name;
}
/**
* Returns the descriptor of the method described by this object.
*
* @return the descriptor of the method described by this object.
*/
public String getDescriptor() {
return desc;
}
/**
* Returns the return type of the method described by this object.
*
* @return the return type of the method described by this object.
*/
public Type getReturnType() {
return Type.getReturnType(desc);
}
/**
* Returns the argument types of the method described by this object.
*
* @return the argument types of the method described by this object.
*/
public Type[] getArgumentTypes() {
return Type.getArgumentTypes(desc);
}
public String toString() {
return name + desc;
}
public boolean equals(final Object o) {
if (!(o instanceof MethodDescriptor)) {
return false;
}
MethodDescriptor other = (MethodDescriptor) o;
return name.equals(other.name) && desc.equals(other.desc);
}
public int hashCode() {
return name.hashCode() ^ desc.hashCode();
}
}

Datei anzeigen

@ -0,0 +1,432 @@
package com.comphenix.protocol.compiler;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.comphenix.protocol.reflect.PrimitiveUtils;
import com.comphenix.protocol.reflect.StructureModifier;
import com.google.common.base.Objects;
import net.sf.cglib.asm.*;
// This class will automatically generate the following type of structure modifier:
//
// public class CompiledStructure$Packet20NamedEntitySpawnObject<TField> extends CompiledStructureModifier<TField> {
//
// private Packet20NamedEntitySpawn typedTarget;
//
// public CompiledStructure$Packet20NamedEntitySpawnObject(StructureModifier<TField> other, StructureCompiler compiler) {
// initialize(other);
// this.typedTarget = (Packet20NamedEntitySpawn) other.getTarget();
// this.compiler = compiler;
// }
//
// @SuppressWarnings("unchecked")
// @Override
// public TField read(int fieldIndex) throws FieldAccessException {
//
// Packet20NamedEntitySpawn target = typedTarget;
//
// switch (fieldIndex) {
// case 0: return (TField) (Object) target.a;
// case 1: return (TField) (Object) target.b;
// case 2: return (TField) (Object) target.c;
// case 3: return (TField) (Object) target.d;
// case 4: return (TField) (Object) target.e;
// case 5: return (TField) (Object) target.f;
// case 6: return (TField) (Object) target.g;
// case 7: return (TField) (Object) target.h;
// default:
// throw new IllegalArgumentException("Invalid index " + fieldIndex);
// }
// }
//
// @Override
// public StructureModifier<TField> write(int index, Object value) {
//
// Packet20NamedEntitySpawn target = typedTarget;
//
// switch (index) {
// case 0: target.a = (Integer) value; break;
// case 1: target.b = (String) value; break;
// case 2: target.c = (Integer) value; break;
// case 3: target.d = (Integer) value; break;
// case 4: target.e = (Integer) value; break;
// case 5: target.f = (Byte) value; break;
// case 6: target.g = (Byte) value; break;
// case 7: target.h = (Integer) value; break;
// default:
// throw new IllegalArgumentException("Invalid index " + index);
// }
//
// // Chaining
// return this;
// }
// }
/**
* Represents a StructureModifier compiler.
*
* @author Kristian
*/
public final class StructureCompiler {
// Used to store generated classes of different types
@SuppressWarnings("rawtypes")
private class StructureKey {
private Class targetType;
private Class fieldType;
public StructureKey(Class targetType, Class fieldType) {
this.targetType = targetType;
this.fieldType = fieldType;
}
@Override
public int hashCode() {
return Objects.hashCode(targetType, fieldType);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof StructureKey) {
StructureKey other = (StructureKey) obj;
return Objects.equal(targetType, other.targetType) &&
Objects.equal(fieldType, other.fieldType);
}
return false;
}
}
// Used to load classes
private static Method defineMethod;
@SuppressWarnings("rawtypes")
private Map<StructureKey, Class> compiledCache = new HashMap<StructureKey, Class>();
// The class loader we'll store our classes
private ClassLoader loader;
// References to other classes
private static String PACKAGE_NAME = "com/comphenix/protocol/compiler";
private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier";
private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier";
/**
* Construct a structure compiler.
* @param loader - main class loader.
*/
StructureCompiler(ClassLoader loader) {
this.loader = loader;
}
/**
* Compiles the given structure modifier.
* <p>
* WARNING: Do NOT call this method in the main thread. Compiling may easily take 10 ms, which is already
* over 1/4 of a tick (50 ms). Let the background thread automatically compile the structure modifiers instead.
* @param source - structure modifier to compile.
* @return A compiled structure modifier.
*/
@SuppressWarnings("unchecked")
public synchronized <TField> StructureModifier<TField> compile(StructureModifier<TField> source) {
// We cannot optimize a structure modifier with no public fields
if (!isAnyPublic(source.getFields())) {
return source;
}
StructureKey key = new StructureKey(source.getTargetType(), source.getFieldType());
Class<?> compiledClass = compiledCache.get(key);
if (!compiledCache.containsKey(key)) {
compiledClass = generateClass(source);
compiledCache.put(key, compiledClass);
}
// Next, create an instance of this class
try {
return (StructureModifier<TField>) compiledClass.getConstructor(
StructureModifier.class, StructureCompiler.class).
newInstance(source, this);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Used invalid parameters in instance creation", e);
} catch (SecurityException e) {
throw new RuntimeException("Security limitation!", e);
} catch (InstantiationException e) {
throw new RuntimeException("Error occured while instancing generated class.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Security limitation! Cannot create instance of dynamic class.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error occured while instancing generated class.", e);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Cannot happen.", e);
}
}
private <TField> Class<?> generateClass(StructureModifier<TField> source) {
ClassWriter cw = new ClassWriter(0);
@SuppressWarnings("rawtypes")
Class targetType = source.getTargetType();
String className = "CompiledStructure$" + targetType.getSimpleName() + source.getFieldType().getSimpleName();
String targetSignature = Type.getDescriptor(targetType);
String targetName = targetType.getName().replace('.', '/');
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className,
"<TField:Ljava/lang/Object;>L" + COMPILED_CLASS + "<TTField;>;",
COMPILED_CLASS, null);
createFields(cw, targetSignature);
createConstructor(cw, className, targetSignature, targetName);
createReadMethod(cw, className, source.getFields(), targetSignature, targetName);
createWriteMethod(cw, className, source.getFields(), targetSignature, targetName);
cw.visitEnd();
byte[] data = cw.toByteArray();
// Call the define method
try {
if (defineMethod == null) {
defineMethod = ClassLoader.class.getDeclaredMethod("defineClass",
new Class<?>[] { String.class, byte[].class, int.class, int.class });
// Awesome. Now, create and return it.
defineMethod.setAccessible(true);
}
@SuppressWarnings("rawtypes")
Class clazz = (Class) defineMethod.invoke(loader, null, data, 0, data.length);
// DEBUG CODE: Print the content of the generated class.
//org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(data);
//cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0);
return clazz;
} catch (SecurityException e) {
throw new RuntimeException("Cannot use reflection to dynamically load a class.", e);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Incompatible JVM.", e);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Cannot call defineMethod - wrong JVM?", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Security limitation! Cannot dynamically load class.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error occured in code generator.", e);
}
}
/**
* Determine if at least one of the given fields is public.
* @param fields - field to test.
* @return TRUE if one or more field is publically accessible, FALSE otherwise.
*/
private boolean isAnyPublic(List<Field> fields) {
// Are any of the fields public?
for (int i = 0; i < fields.size(); i++) {
if (isPublic(fields.get(i))) {
return true;
}
}
return false;
}
private boolean isPublic(Field field) {
return Modifier.isPublic(field.getModifiers());
}
private void createFields(ClassWriter cw, String targetSignature) {
FieldVisitor typedField = cw.visitField(Opcodes.ACC_PRIVATE, "typedTarget", targetSignature, null, null);
typedField.visitEnd();
}
private void createWriteMethod(ClassWriter cw, String className, List<Field> fields, String targetSignature, String targetName) {
String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";";
String methodSignature = "(ITTField;)L" + SUPER_CLASS + "<TTField;>;";
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL, "write", methodDescriptor, methodSignature,
new String[] { "com/comphenix/protocol/reflect/FieldAccessException" });
BoxingHelper boxingHelper = new BoxingHelper(mv);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature);
mv.visitVarInsn(Opcodes.ASTORE, 3);
mv.visitVarInsn(Opcodes.ILOAD, 1);
// The last label is for the default switch
Label[] labels = new Label[fields.size()];
Label errorLabel = new Label();
Label returnLabel = new Label();
// Generate labels
for (int i = 0; i < fields.size(); i++) {
labels[i] = new Label();
}
mv.visitTableSwitchInsn(0, labels.length - 1, errorLabel, labels);
for (int i = 0; i < fields.size(); i++) {
Class<?> outputType = fields.get(i).getType();
Class<?> inputType = PrimitiveUtils.wrap(outputType);
String typeDescriptor = Type.getDescriptor(outputType);
String inputPath = inputType.getName().replace('.', '/');
mv.visitLabel(labels[i]);
// Push the compare object
if (i == 0)
mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null);
else
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
// Only write to public fields
if (isPublic(fields.get(i))) {
mv.visitVarInsn(Opcodes.ALOAD, 3);
mv.visitVarInsn(Opcodes.ALOAD, 2);
if (!PrimitiveUtils.isPrimitive(outputType))
mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath);
else
boxingHelper.unbox(Type.getType(outputType));
mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, fields.get(i).getName(), typeDescriptor);
} else {
// Use reflection. We don't have a choice, unfortunately.
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "write", "(ILjava/lang/Object;)L" + SUPER_CLASS + ";");
mv.visitInsn(Opcodes.POP);
}
mv.visitJumpInsn(Opcodes.GOTO, returnLabel);
}
mv.visitLabel(errorLabel);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(Opcodes.DUP);
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn("Invalid index ");
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "(Ljava/lang/String;)V");
mv.visitInsn(Opcodes.ATHROW);
mv.visitLabel(returnLabel);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(5, 4);
mv.visitEnd();
}
private void createReadMethod(ClassWriter cw, String className, List<Field> fields, String targetSignature, String targetName) {
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL, "read", "(I)Ljava/lang/Object;", "(I)TTField;",
new String[] { "com/comphenix/protocol/reflect/FieldAccessException" });
BoxingHelper boxingHelper = new BoxingHelper(mv);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature);
mv.visitVarInsn(Opcodes.ASTORE, 2);
mv.visitVarInsn(Opcodes.ILOAD, 1);
// The last label is for the default switch
Label[] labels = new Label[fields.size()];
Label errorLabel = new Label();
// Generate labels
for (int i = 0; i < fields.size(); i++) {
labels[i] = new Label();
}
mv.visitTableSwitchInsn(0, fields.size() - 1, errorLabel, labels);
for (int i = 0; i < fields.size(); i++) {
Class<?> outputType = fields.get(i).getType();
String typeDescriptor = Type.getDescriptor(outputType);
mv.visitLabel(labels[i]);
// Push the compare object
if (i == 0)
mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null);
else
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
// Note that byte code cannot access non-public fields
if (isPublic(fields.get(i))) {
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitFieldInsn(Opcodes.GETFIELD, targetName, fields.get(i).getName(), typeDescriptor);
boxingHelper.box(Type.getType(outputType));
} else {
// We have to use reflection for private and protected fields.
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "read", "(I)Ljava/lang/Object;");
}
mv.visitInsn(Opcodes.ARETURN);
}
mv.visitLabel(errorLabel);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(Opcodes.DUP);
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn("Invalid index ");
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "(Ljava/lang/String;)V");
mv.visitInsn(Opcodes.ATHROW);
mv.visitMaxs(5, 3);
mv.visitEnd();
}
private void createConstructor(ClassWriter cw, String className, String targetSignature, String targetName) {
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V",
"(L" + SUPER_CLASS + "<TTField;>;L" + SUPER_CLASS + ";)V", null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "<init>", "()V");
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, PACKAGE_NAME + "/" + className, "initialize", "(L" + SUPER_CLASS + ";)V");
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, SUPER_CLASS, "getTarget", "()Ljava/lang/Object;");
mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "target", "Ljava/lang/Object;");
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "target", "Ljava/lang/Object;");
mv.visitTypeInsn(Opcodes.CHECKCAST, targetName);
mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;");
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 3);
mv.visitEnd();
}
}

Datei anzeigen

@ -21,15 +21,23 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.concurrent.ConcurrentHashMap;
import com.comphenix.protocol.compiler.BackgroundCompiler;
import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.reflect.instances.DefaultInstances;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
/**
* Provides list-oriented access to the fields of a Minecraft packet.
* <p>
* Implemented by using reflection. Use a CompiledStructureModifier, if speed is essential.
*
* @author Kristian
* @param <TField> Type of the fields to retrieve.
*/
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public class StructureModifier<TField> { public class StructureModifier<TField> {
@ -45,7 +53,7 @@ public class StructureModifier<TField> {
protected List<Field> data = new ArrayList<Field>(); protected List<Field> data = new ArrayList<Field>();
// Improved default values // Improved default values
protected Set<Field> defaultFields; protected Map<Field, Integer> defaultFields;
// Cache of previous types // Cache of previous types
protected Map<Class, StructureModifier> subtypeCache; protected Map<Class, StructureModifier> subtypeCache;
@ -58,9 +66,9 @@ public class StructureModifier<TField> {
*/ */
public StructureModifier(Class targetType, Class superclassExclude, boolean requireDefault) { public StructureModifier(Class targetType, Class superclassExclude, boolean requireDefault) {
List<Field> fields = getFields(targetType, superclassExclude); List<Field> fields = getFields(targetType, superclassExclude);
Set<Field> defaults = requireDefault ? generateDefaultFields(fields) : new HashSet<Field>(); Map<Field, Integer> defaults = requireDefault ? generateDefaultFields(fields) : new HashMap<Field, Integer>();
initialize(targetType, Object.class, fields, defaults, null, new HashMap<Class, StructureModifier>()); initialize(targetType, Object.class, fields, defaults, null, new ConcurrentHashMap<Class, StructureModifier>());
} }
/** /**
@ -89,7 +97,7 @@ public class StructureModifier<TField> {
* @param subTypeCache - a structure modifier cache. * @param subTypeCache - a structure modifier cache.
*/ */
protected void initialize(Class targetType, Class fieldType, protected void initialize(Class targetType, Class fieldType,
List<Field> data, Set<Field> defaultFields, List<Field> data, Map<Field, Integer> defaultFields,
EquivalentConverter<TField> converter, Map<Class, StructureModifier> subTypeCache) { EquivalentConverter<TField> converter, Map<Class, StructureModifier> subTypeCache) {
this.targetType = targetType; this.targetType = targetType;
this.fieldType = fieldType; this.fieldType = fieldType;
@ -213,7 +221,7 @@ public class StructureModifier<TField> {
DefaultInstances generator = DefaultInstances.DEFAULT; DefaultInstances generator = DefaultInstances.DEFAULT;
// Write a default instance to every field // Write a default instance to every field
for (Field field : defaultFields) { for (Field field : defaultFields.keySet()) {
try { try {
FieldUtils.writeField(field, target, FieldUtils.writeField(field, target,
generator.getDefault(field.getType()), true); generator.getDefault(field.getType()), true);
@ -239,22 +247,32 @@ public class StructureModifier<TField> {
// Do we need to update the cache? // Do we need to update the cache?
if (result == null) { if (result == null) {
List<Field> filtered = new ArrayList<Field>(); List<Field> filtered = new ArrayList<Field>();
Set<Field> defaults = new HashSet<Field>(); Map<Field, Integer> defaults = new HashMap<Field, Integer>();
int index = 0;
for (Field field : data) { for (Field field : data) {
if (fieldType != null && fieldType.isAssignableFrom(field.getType())) { if (fieldType != null && fieldType.isAssignableFrom(field.getType())) {
filtered.add(field); filtered.add(field);
if (defaultFields.contains(field)) // Don't use the original index
defaults.add(field); if (defaultFields.containsKey(field))
defaults.put(field, index);
} }
// Keep track of the field index
index++;
} }
// Cache structure modifiers // Cache structure modifiers
result = withFieldType(fieldType, filtered, defaults, converter); result = withFieldType(fieldType, filtered, defaults, converter);
if (fieldType != null) if (fieldType != null) {
subtypeCache.put(fieldType, result); subtypeCache.put(fieldType, result);
// Automatically compile the structure modifier
if (BackgroundCompiler.getInstance() != null)
BackgroundCompiler.getInstance().scheduleCompilation(subtypeCache, fieldType);
}
} }
// Add the target too // Add the target too
@ -320,11 +338,11 @@ public class StructureModifier<TField> {
*/ */
protected <T> StructureModifier<T> withFieldType( protected <T> StructureModifier<T> withFieldType(
Class fieldType, List<Field> filtered, Class fieldType, List<Field> filtered,
Set<Field> defaults, EquivalentConverter<T> converter) { Map<Field, Integer> defaults, EquivalentConverter<T> converter) {
StructureModifier<T> result = new StructureModifier<T>(); StructureModifier<T> result = new StructureModifier<T>();
result.initialize(targetType, fieldType, filtered, defaults, result.initialize(targetType, fieldType, filtered, defaults,
converter, new HashMap<Class, StructureModifier>()); converter, new ConcurrentHashMap<Class, StructureModifier>());
return result; return result;
} }
@ -387,10 +405,11 @@ public class StructureModifier<TField> {
} }
// Used to generate plausible default values // Used to generate plausible default values
private static Set<Field> generateDefaultFields(List<Field> fields) { private static Map<Field, Integer> generateDefaultFields(List<Field> fields) {
Set<Field> requireDefaults = new HashSet<Field>(); Map<Field, Integer> requireDefaults = new HashMap<Field, Integer>();
DefaultInstances generator = DefaultInstances.DEFAULT; DefaultInstances generator = DefaultInstances.DEFAULT;
int index = 0;
for (Field field : fields) { for (Field field : fields) {
Class<?> type = field.getType(); Class<?> type = field.getType();
@ -400,9 +419,12 @@ public class StructureModifier<TField> {
// Next, see if we actually can generate a default value // Next, see if we actually can generate a default value
if (generator.getDefault(type) != null) { if (generator.getDefault(type) != null) {
// If so, require it // If so, require it
requireDefaults.add(field); requireDefaults.put(field, index);
} }
} }
// Increment field index
index++;
} }
return requireDefaults; return requireDefaults;