diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java index 7abc4ff1..39075681 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java @@ -25,6 +25,7 @@ import org.bukkit.Server; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import com.comphenix.protocol.compiler.BackgroundCompiler; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.metrics.Statistics; @@ -39,6 +40,9 @@ public class ProtocolLibrary extends JavaPlugin { // Metrics and statistisc private Statistics statistisc; + // Structure compiler + private BackgroundCompiler backgroundCompiler; + @Override public void onLoad() { logger = getLoggerSafely(); @@ -50,6 +54,12 @@ public class ProtocolLibrary extends JavaPlugin { Server server = getServer(); PluginManager manager = server.getPluginManager(); + // Initialize background compiler + if (backgroundCompiler == null) { + backgroundCompiler = new BackgroundCompiler(getClassLoader()); + BackgroundCompiler.setInstance(backgroundCompiler); + } + // Notify server managers of incompatible plugins checkForIncompatibility(manager); @@ -83,6 +93,13 @@ public class ProtocolLibrary extends JavaPlugin { @Override public void onDisable() { + // Disable compiler + if (backgroundCompiler != null) { + backgroundCompiler.shutdownAll(); + backgroundCompiler = null; + BackgroundCompiler.setInstance(null); + } + protocolManager.close(); protocolManager = null; statistisc = null; diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/BackgroundCompiler.java b/ProtocolLib/src/com/comphenix/protocol/compiler/BackgroundCompiler.java new file mode 100644 index 00000000..313593ab --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/compiler/BackgroundCompiler.java @@ -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. + *

+ * 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 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() { + @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; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/BoxingHelper.java b/ProtocolLib/src/com/comphenix/protocol/compiler/BoxingHelper.java new file mode 100644 index 00000000..3692381e --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/compiler/BoxingHelper.java @@ -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("", 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 null. + */ + 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); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/CompiledStructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/compiler/CompiledStructureModifier.java new file mode 100644 index 00000000..570a06b6 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/compiler/CompiledStructureModifier.java @@ -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 Field type. + */ +public class CompiledStructureModifier extends StructureModifier { + // Used to compile instances of structure modifiers + protected StructureCompiler compiler; + + // Speed up the default writer + @SuppressWarnings("unchecked") + @Override + public StructureModifier writeDefaults() throws FieldAccessException { + + DefaultInstances generator = DefaultInstances.DEFAULT; + + // Write a default instance to every field + for (Map.Entry entry : defaultFields.entrySet()) { + Integer index = entry.getValue(); + Field field = entry.getKey(); + + write(index, (TField) generator.getDefault(field.getType())); + } + + return this; + } + + @Override + public StructureModifier withTarget(Object target) { + if (compiler != null) + return compiler.compile(super.withTarget(target)); + else + return super.withTarget(target); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/MethodDescriptor.java b/ProtocolLib/src/com/comphenix/protocol/compiler/MethodDescriptor.java new file mode 100644 index 00000000..4f5e8c5c --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/compiler/MethodDescriptor.java @@ -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 DESCRIPTORS; + + static { + DESCRIPTORS = new HashMap(); + 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 method 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 method 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(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/StructureCompiler.java b/ProtocolLib/src/com/comphenix/protocol/compiler/StructureCompiler.java new file mode 100644 index 00000000..f79a949b --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/compiler/StructureCompiler.java @@ -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 extends CompiledStructureModifier { +// +// private Packet20NamedEntitySpawn typedTarget; +// +// public CompiledStructure$Packet20NamedEntitySpawnObject(StructureModifier 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 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 compiledCache = new HashMap(); + + // 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. + *

+ * 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 StructureModifier compile(StructureModifier 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) 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 Class generateClass(StructureModifier 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, + "L" + COMPILED_CLASS + ";", + 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 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 fields, String targetSignature, String targetName) { + + String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + String methodSignature = "(ITTField;)L" + SUPER_CLASS + ";"; + 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", "", "(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", "", "(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 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", "", "(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", "", "(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, "", + "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", + "(L" + SUPER_CLASS + ";L" + SUPER_CLASS + ";)V", null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "", "()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(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java index aecd71bd..d79733b2 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java @@ -21,15 +21,23 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; 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.google.common.base.Function; import com.google.common.collect.ImmutableList; +/** + * Provides list-oriented access to the fields of a Minecraft packet. + *

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