diff --git a/build.gradle b/build.gradle index 85d6d86..826bc55 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,8 @@ runtimeCommon 'net.openhft:koloboke-impl-jdk8:0.6.8' runtime 'mysql:mysql-connector-java:5.1.31' + + testCompile "org.spockframework:spock-core:1.1-groovy-2.4-rc-1" } task injectVersion(type: SpeicialClassTransformTask) { diff --git a/src/main/java/org/ultramine/core/service/InjectService.java b/src/main/java/org/ultramine/core/service/InjectService.java new file mode 100644 index 0000000..8451c0d --- /dev/null +++ b/src/main/java/org/ultramine/core/service/InjectService.java @@ -0,0 +1,24 @@ +package org.ultramine.core.service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * For any annotated field bytecode transformation will be applied for the corresponding service provider injection. + * The field should be declared as static and non-final. After transformation field become final. Example:
+ *

+ *    {@literal @}InjectService
+ *     private static SomeService service;
+ * 
+ * Will be transformed to:
+ *

+ *     private static final SomeService service = (SomeService) ServiceBytecodeAdapter.provideService(SomeService.class);
+ * 
+ */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = ElementType.FIELD) +public @interface InjectService +{ +} diff --git a/src/main/java/org/ultramine/core/service/Service.java b/src/main/java/org/ultramine/core/service/Service.java new file mode 100644 index 0000000..4f12d6e --- /dev/null +++ b/src/main/java/org/ultramine/core/service/Service.java @@ -0,0 +1,15 @@ +package org.ultramine.core.service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = ElementType.TYPE) +public @interface Service +{ + String name() default ""; + String description() default ""; + boolean singleProvider() default false; +} diff --git a/src/main/java/org/ultramine/core/service/ServiceBytecodeAdapter.java b/src/main/java/org/ultramine/core/service/ServiceBytecodeAdapter.java new file mode 100644 index 0000000..cd22310 --- /dev/null +++ b/src/main/java/org/ultramine/core/service/ServiceBytecodeAdapter.java @@ -0,0 +1,18 @@ +package org.ultramine.core.service; + +import org.ultramine.server.service.UMServiceManager; + +public class ServiceBytecodeAdapter +{ + private static ServiceManager manager = new UMServiceManager(); + + static + { + manager.register(ServiceManager.class, manager, 0); + } + + public static Object provideService(Class serviceClass) + { + return manager.provide(serviceClass); + } +} diff --git a/src/main/java/org/ultramine/core/service/ServiceDelegate.java b/src/main/java/org/ultramine/core/service/ServiceDelegate.java new file mode 100644 index 0000000..e70d1d7 --- /dev/null +++ b/src/main/java/org/ultramine/core/service/ServiceDelegate.java @@ -0,0 +1,12 @@ +package org.ultramine.core.service; + +import javax.annotation.Nullable; + +public interface ServiceDelegate +{ + void setProvider(T obj); + + @Nullable T getProvider(); + + T asService(); +} diff --git a/src/main/java/org/ultramine/core/service/ServiceManager.java b/src/main/java/org/ultramine/core/service/ServiceManager.java new file mode 100644 index 0000000..b3d5744 --- /dev/null +++ b/src/main/java/org/ultramine/core/service/ServiceManager.java @@ -0,0 +1,13 @@ +package org.ultramine.core.service; + +import javax.annotation.Nonnull; + +@Service +public interface ServiceManager +{ + void register(@Nonnull Class serviceClass, @Nonnull T provider, int priority); + + void register(@Nonnull Class serviceClass, @Nonnull ServiceProviderLoader providerLoader, int priority); + + @Nonnull T provide(@Nonnull Class service); +} diff --git a/src/main/java/org/ultramine/core/service/ServiceProviderLoader.java b/src/main/java/org/ultramine/core/service/ServiceProviderLoader.java new file mode 100644 index 0000000..06da99d --- /dev/null +++ b/src/main/java/org/ultramine/core/service/ServiceProviderLoader.java @@ -0,0 +1,8 @@ +package org.ultramine.core.service; + +public interface ServiceProviderLoader +{ + void load(ServiceDelegate service); + + void unload(); +} diff --git a/src/main/java/org/ultramine/core/service/ServiceSwitchEvent.java b/src/main/java/org/ultramine/core/service/ServiceSwitchEvent.java new file mode 100644 index 0000000..0602c36 --- /dev/null +++ b/src/main/java/org/ultramine/core/service/ServiceSwitchEvent.java @@ -0,0 +1,60 @@ +package org.ultramine.core.service; + +import cpw.mods.fml.common.eventhandler.Event; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class ServiceSwitchEvent extends Event +{ + private final Class serviceClass; + private final ServiceDelegate delegate; + private final @Nullable ServiceProviderLoader oldProviderLoader; + private final @Nonnull ServiceProviderLoader newProviderLoader; + + public ServiceSwitchEvent(Class serviceClass, ServiceDelegate delegate, ServiceProviderLoader oldProviderLoader, @Nonnull ServiceProviderLoader newProviderLoader) + { + this.serviceClass = serviceClass; + this.delegate = delegate; + this.oldProviderLoader = oldProviderLoader; + this.newProviderLoader = newProviderLoader; + } + + public ServiceDelegate getServiceDelegate() + { + return delegate; + } + + public Class getServiceClass() + { + return serviceClass; + } + + @Nullable + public ServiceProviderLoader getOldProviderLoader() + { + return oldProviderLoader; + } + + @Nonnull + public ServiceProviderLoader getNewProviderLoader() + { + return newProviderLoader; + } + + public static class Pre extends ServiceSwitchEvent + { + public Pre(Class serviceClass, ServiceDelegate delegate, ServiceProviderLoader oldProvider, @Nonnull ServiceProviderLoader newProvider) + { + super(serviceClass, delegate, oldProvider, newProvider); + } + } + + public static class Post extends ServiceSwitchEvent + { + public Post(Class serviceClass, ServiceDelegate delegate, ServiceProviderLoader oldProvider, @Nonnull ServiceProviderLoader newProvider) + { + super(serviceClass, delegate, oldProvider, newProvider); + } + } +} diff --git a/src/main/java/org/ultramine/server/asm/transformers/ServiceInjectionTransformer.java b/src/main/java/org/ultramine/server/asm/transformers/ServiceInjectionTransformer.java new file mode 100644 index 0000000..49ff791 --- /dev/null +++ b/src/main/java/org/ultramine/server/asm/transformers/ServiceInjectionTransformer.java @@ -0,0 +1,88 @@ +package org.ultramine.server.asm.transformers; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.ultramine.server.asm.UMTBatchTransformer.IUMClassTransformer; +import org.ultramine.server.asm.UMTBatchTransformer.TransformResult; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +public class ServiceInjectionTransformer implements IUMClassTransformer +{ + private static final String INJECT_SERVICE_DESC = "Lorg/ultramine/core/service/InjectService;"; + private static final String SBA_CLASS = "org/ultramine/core/service/ServiceBytecodeAdapter"; + + @Nonnull + @Override + public TransformResult transform(String name, String transformedName, ClassReader classReader, ClassNode classNode) + { + List fieldsToInject = new ArrayList<>(); + for(FieldNode f : classNode.fields) + if(f.visibleAnnotations != null) + for(AnnotationNode ann : f.visibleAnnotations) + if(ann.desc.equals(INJECT_SERVICE_DESC)) + fieldsToInject.add(f); + + if(fieldsToInject.size() > 0) + { + for(FieldNode field : fieldsToInject) + { + if((field.access & Opcodes.ACC_STATIC) == 0) + throw new RuntimeException("Service injection for non-static fields is not supported: " + classNode.name + "#" + field.name); + if((field.access & Opcodes.ACC_FINAL) != 0) + throw new RuntimeException("Service injection for final fields is not supported: " + classNode.name + "#" + field.name); + } + + for(FieldNode field : fieldsToInject) + field.access |= Opcodes.ACC_FINAL; + + boolean clinitFound = false; + for(MethodNode method : classNode.methods) + { + if(method.name.equals("")) + { + for(FieldNode field : fieldsToInject) + method.instructions.insert(buildInjectorFor(classNode.name, field)); + clinitFound = true; + break; + } + } + + if(!clinitFound) + { + MethodNode method = new MethodNode(Opcodes.ACC_STATIC, "", "()V", null, null); + for(FieldNode field : fieldsToInject) + method.instructions.insert(buildInjectorFor(classNode.name, field)); + method.instructions.add(new InsnNode(Opcodes.RETURN)); + classNode.methods.add(method); + } + + return TransformResult.MODIFIED_STACK; + } + + return TransformResult.NOT_MODIFIED; + } + + private static InsnList buildInjectorFor(String owner, FieldNode field) + { + InsnList list = new InsnList(); + list.add(new LdcInsnNode(Type.getType(field.desc))); + list.add(new MethodInsnNode(Opcodes.INVOKESTATIC, SBA_CLASS, "provideService", "(Ljava/lang/Class;)Ljava/lang/Object;", false)); + list.add(new TypeInsnNode(Opcodes.CHECKCAST, field.desc.substring(1, field.desc.length()-1))); + list.add(new FieldInsnNode(Opcodes.PUTSTATIC, owner, field.name, field.desc)); + return list; + } +} diff --git a/src/main/java/org/ultramine/server/asm/transformers/UMTransformerCollection.java b/src/main/java/org/ultramine/server/asm/transformers/UMTransformerCollection.java index abe143e..2757660 100644 --- a/src/main/java/org/ultramine/server/asm/transformers/UMTransformerCollection.java +++ b/src/main/java/org/ultramine/server/asm/transformers/UMTransformerCollection.java @@ -8,6 +8,7 @@ { registerGlobalTransformer(new PrintStackTraceTransformer()); registerGlobalTransformer(new TrigMathTransformer()); + registerGlobalTransformer(new ServiceInjectionTransformer()); registerSpecialTransformer(new BlockLeavesBaseFixer(), "net.minecraft.block.BlockLeavesBase"); } } diff --git a/src/main/java/org/ultramine/server/service/ServiceDelegateGenerator.java b/src/main/java/org/ultramine/server/service/ServiceDelegateGenerator.java new file mode 100644 index 0000000..b3920c1 --- /dev/null +++ b/src/main/java/org/ultramine/server/service/ServiceDelegateGenerator.java @@ -0,0 +1,128 @@ +package org.ultramine.server.service; + +import static org.objectweb.asm.Opcodes.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.ultramine.server.util.UnsafeUtil; + +import org.ultramine.core.service.ServiceDelegate; +import sun.misc.Unsafe; + +public class ServiceDelegateGenerator +{ + private static final Unsafe U = UnsafeUtil.getUnsafe(); + + @SuppressWarnings("unchecked") + public static Class> makeServiceDelegate(Class base, String name, Class iface) + { + return (Class>) U.defineAnonymousClass(base, makeServiceDelegate(name, iface), null); + } + + public static byte[] makeServiceDelegate(String name, Class iface) + { + if(!iface.isInterface()) + throw new IllegalArgumentException("iface should be an interface"); + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + + String thisClassInternalName = name.replace('.', '/'); + String ifaceInternalName = Type.getInternalName(iface); + String ifaceDesc = Type.getDescriptor(iface); + + cw.visit(V1_5, ACC_PUBLIC | ACC_SUPER, thisClassInternalName, null, "java/lang/Object", new String[]{ ifaceInternalName, Type.getInternalName(ServiceDelegate.class) }); + cw.visitSource(".dynamic", null); + + { + cw.visitField(ACC_PUBLIC, "instance", ifaceDesc, null, null).visitEnd(); + } + + { + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + { + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "setProvider", "(Ljava/lang/Object;)V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitTypeInsn(CHECKCAST, ifaceInternalName); + mv.visitFieldInsn(PUTFIELD, thisClassInternalName, "instance", ifaceDesc); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + { + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getProvider", "()Ljava/lang/Object;", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, thisClassInternalName, "instance", ifaceDesc); + mv.visitInsn(ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + { + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "asService", "()Ljava/lang/Object;", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitInsn(ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + for(Method method : iface.getDeclaredMethods()) + { + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, method.getName(), Type.getMethodDescriptor(method), null, null); + mv.visitCode(); + + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, thisClassInternalName, "instance", ifaceDesc); + int argCounter = 1; + for(Parameter par : method.getParameters()) + { + int insn = loadInsnForType(par.getType()); + mv.visitVarInsn(insn, argCounter); + argCounter += insn == LLOAD || insn == DLOAD ? 2 : 1; + } + mv.visitMethodInsn(INVOKEINTERFACE, ifaceInternalName, method.getName(), Type.getMethodDescriptor(method), true); + + mv.visitInsn(returnInsnForType(method.getReturnType())); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + cw.visitEnd(); + return cw.toByteArray(); + } + + private static int loadInsnForType(Class cls) + { + if(cls == boolean.class || cls == byte.class || cls == short.class || cls == int.class) return ILOAD; + if(cls == long.class) return LLOAD; + if(cls == float.class) return FLOAD; + if(cls == double.class) return DLOAD; + return ALOAD; + } + + private static int returnInsnForType(Class cls) + { + if(cls == boolean.class || cls == byte.class || cls == short.class || cls == int.class) return IRETURN; + if(cls == long.class) return LRETURN; + if(cls == float.class) return FRETURN; + if(cls == double.class) return DRETURN; + if(cls == void.class) return RETURN; + return ARETURN; + } +} diff --git a/src/main/java/org/ultramine/server/service/UMServiceManager.java b/src/main/java/org/ultramine/server/service/UMServiceManager.java new file mode 100644 index 0000000..ca214ab --- /dev/null +++ b/src/main/java/org/ultramine/server/service/UMServiceManager.java @@ -0,0 +1,160 @@ +package org.ultramine.server.service; + +import net.minecraftforge.common.MinecraftForge; +import org.ultramine.core.service.Service; +import org.ultramine.core.service.ServiceDelegate; +import org.ultramine.core.service.ServiceManager; +import org.ultramine.core.service.ServiceProviderLoader; +import org.ultramine.core.service.ServiceSwitchEvent; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UMServiceManager implements ServiceManager +{ + private final Map, ServiceWrapper> services = new HashMap<>(); + + private @Nonnull ServiceWrapper getOrCreateService(Class serviceClass) + { + @SuppressWarnings("unchecked") + ServiceWrapper service = services.get(serviceClass); + if(service == null) + { + Service desc = serviceClass.getAnnotation(Service.class); + if(desc == null) + throw new IllegalArgumentException("Given class is not a service class: "+serviceClass.getName()); + ServiceDelegate delegate; + try { + delegate = ServiceDelegateGenerator.makeServiceDelegate(getClass(), serviceClass.getSimpleName() + "_delegate", serviceClass).newInstance(); + } catch(InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + service = new ServiceWrapper<>(serviceClass, delegate, desc); + services.put(serviceClass, service); + } + return service; + } + + @Override + public void register(@Nonnull Class serviceClass, @Nonnull T provider, int priority) + { + serviceClass.getClass(); // NPE + provider.getClass(); // NPE + register(serviceClass, new SimpleServiceProviderLoader<>(provider), priority); + } + + @Override + public void register(@Nonnull Class serviceClass, @Nonnull ServiceProviderLoader providerLoader, int priority) + { + serviceClass.getClass(); // NPE + providerLoader.getClass(); // NPE + ServiceWrapper service = getOrCreateService(serviceClass); + service.addProvider(new ServiceProviderRegistration<>(providerLoader, priority)); + } + + @Nonnull + @Override + public T provide(@Nonnull Class service) + { + return getOrCreateService(service).provide(); + } + + private static class ServiceWrapper + { + private final Class serviceClass; + private final ServiceDelegate delegate; + private final Service desc; + private final List> providers = new ArrayList<>(); + private ServiceProviderRegistration currentProvider; + + public ServiceWrapper(Class serviceClass, ServiceDelegate delegate, Service desc) + { + this.serviceClass = serviceClass; + this.delegate = delegate; + this.desc = desc; + } + + public Service getDesc() + { + return desc; + } + + private void switchTo(ServiceProviderRegistration newProvider) + { + if(providers.isEmpty()) + throw new IllegalStateException("Service provider is not registered"); + ServiceProviderRegistration oldProvider = currentProvider; + MinecraftForge.EVENT_BUS.post(new ServiceSwitchEvent.Pre(serviceClass, delegate, oldProvider == null ? null : oldProvider.providerLoader, newProvider.providerLoader)); + if(oldProvider != null) + oldProvider.providerLoader.unload(); + newProvider.providerLoader.load(delegate); + currentProvider = newProvider; + MinecraftForge.EVENT_BUS.post(new ServiceSwitchEvent.Post(serviceClass, delegate, oldProvider == null ? null : oldProvider.providerLoader, newProvider.providerLoader)); + } + + public void addProvider(ServiceProviderRegistration provider) + { + if(desc.singleProvider() && providers.size() != 0) + throw new IllegalStateException("Tried to register second provider for single-impl service'"+serviceClass.getName() + + "'. First provider: " + providers.get(0).providerLoader + ", second provider: " + provider); + providers.add(provider); + if(currentProvider == null || provider.priority >= currentProvider.priority) + switchTo(provider); + } + + public T provide() + { + return delegate.asService(); + } + } + + private static class ServiceProviderRegistration implements Comparable + { + public final ServiceProviderLoader providerLoader; + public final int priority; + + private ServiceProviderRegistration(ServiceProviderLoader providerLoader, int priority) + { + this.providerLoader = providerLoader; + this.priority = priority; + } + + public int compareTo(ServiceProviderRegistration o) + { + return Integer.compare(priority, o.priority); + } + } + + private static class SimpleServiceProviderLoader implements ServiceProviderLoader + { + public final T provider; + + private SimpleServiceProviderLoader(T provider) + { + this.provider = provider; + } + + @Override + public void load(ServiceDelegate service) + { + service.setProvider(provider); + } + + @Override + public void unload() + { + + } + + @Override + public String toString() + { + return "SimpleServiceProviderLoader{" + + "provider=" + provider + + '}'; + } + } +} diff --git a/src/test/java/org/ultramine/service/ServiceDelegateGeneratorTest.groovy b/src/test/java/org/ultramine/service/ServiceDelegateGeneratorTest.groovy new file mode 100644 index 0000000..724d3f5 --- /dev/null +++ b/src/test/java/org/ultramine/service/ServiceDelegateGeneratorTest.groovy @@ -0,0 +1,58 @@ +package org.ultramine.service + +import org.ultramine.core.service.ServiceDelegate +import org.ultramine.server.service.ServiceDelegateGenerator +import spock.lang.Specification + +class ServiceDelegateGeneratorTest extends Specification { + def "MakeInterfaceDelegate"() { + setup: + ServiceDelegate delegate = ServiceDelegateGenerator.makeServiceDelegate(getClass(), "qwe", TestInterface.class).newInstance(); + def receiver = Mock(TestInterface) + delegate.setProvider(receiver) + def wrapper = (TestInterface) delegate; + + expect: + wrapper == delegate.asService() + receiver == delegate.getProvider() + + when: + wrapper.testBooleanArg(true) + wrapper.testByteArg((byte) 1) + wrapper.testShortArg((short) 1) + wrapper.testIntArg(1) + wrapper.testLongArg(Long.MAX_VALUE, 1) + wrapper.testPrimitives(false, (byte)1, (short)2, 3, 4L, 5f, 6d); + wrapper.testObject("123") + then: + 1 * receiver.testBooleanArg(true); + 1 * receiver.testByteArg(1); + 1 * receiver.testShortArg(1) + 1 * receiver.testIntArg(1) + 1 * receiver.testLongArg(Long.MAX_VALUE, 1) + 1 * receiver.testPrimitives(false, 1, 2, 3, 4L, 5f, 6d) + 1 * receiver.testObject("123") + + when: + receiver.testReturnInt() >> 15 + receiver.testReturnLong() >> 16 + receiver.testReturnObject() >> "123" + then: + wrapper.testReturnInt() == 15 + wrapper.testReturnLong() == 16 + wrapper.testReturnObject() == "123" + } + + interface TestInterface { + void testBooleanArg(boolean b); + void testByteArg(byte b); + void testShortArg(short b); + void testIntArg(int b); + void testLongArg(long b, long b1); + void testPrimitives(boolean b, byte bt, short s, int i, long l, float f, double d); + void testObject(String str) + int testReturnInt(); + long testReturnLong(); + String testReturnObject(); + } +}