/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.script;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DRETURN;
import static org.objectweb.asm.Opcodes.FRETURN;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.LRETURN;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_6;
import org.lanternpowered.api.script.Script;
import org.lanternpowered.server.util.ClassLoaderUtil;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
final class ScriptFunctionGenerator {
interface Builder<F> {
F get(Script<F> function);
}
private final Map<Class<?>, Builder> functionProviders = new ConcurrentHashMap<>();
public <F> Builder<F> get(ScriptFunctionMethod<F> functionMethod) {
//noinspection unchecked
return this.functionProviders.computeIfAbsent(
functionMethod.getFunctionClass(), functionClass -> getBuilder(functionMethod));
}
private static <F> Builder<F> getBuilder(ScriptFunctionMethod<F> functionMethod) {
//noinspection unchecked
final Class<F> functionClass = (Class<F>) generateScriptFunctionClass(functionMethod);
final Constructor<F> constructor;
try {
constructor = functionClass.getConstructor(LanternScript.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
return function -> {
try {
return constructor.newInstance(function);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
};
}
/**
* Generates a {@link ScriptFunction} class for the specified {@link ScriptFunctionMethod}.
*
* @param functionMethod The script function method
* @return The script function class
*/
private static Class<? extends ScriptFunction> generateScriptFunctionClass(ScriptFunctionMethod<?> functionMethod) {
final Class<?> functionClass = functionMethod.getFunctionClass();
final String name = functionClass.getName() + ScriptFunction.class.getSimpleName() + "Impl35962493";
final String internalName = name.replace('.', '/');
final String superClass;
final String[] interfaces;
if (functionMethod.getFunctionClass().isInterface()) {
interfaces = new String[2];
interfaces[1] = Type.getInternalName(functionClass);
superClass = Type.getInternalName(Object.class);
} else {
interfaces = new String[1];
superClass = Type.getInternalName(functionClass);
}
interfaces[0] = Type.getInternalName(ScriptFunction.class);
// public class <I>ScriptReferencedImpl35962493 implements <I>, ScriptFunction {
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cw.visit(V1_6, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, internalName, null, superClass, interfaces);
// private final LanternScript script;
FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "script", Type.getDescriptor(LanternScript.class), null, null);
fv.visitEnd();
// public <I>ScriptReferencedImpl35962493(LanternScript script) {
// this.script = script;
// }
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(" + Type.getDescriptor(LanternScript.class) + ")V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, internalName, "script", Type.getDescriptor(LanternScript.class));
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
// @Override
// public LanternScript getScript() {
// return this.script;
// }
mv = cw.visitMethod(ACC_PUBLIC, "getScript", "()" + Type.getDescriptor(LanternScript.class), null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, internalName, "script", Type.getDescriptor(LanternScript.class));
mv.visitInsn(ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
// The script method
@SuppressWarnings("ConstantConditions") @Nonnull
final Method method = functionMethod.getMethod();
mv = cw.visitMethod(ACC_PUBLIC, method.getName(), Type.getMethodDescriptor(method), null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, internalName, "script", Type.getDescriptor(LanternScript.class));
mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(LanternScript.class), "getRaw", "()" + Type.getDescriptor(Object.class), false);
mv.visitTypeInsn(CHECKCAST, Type.getInternalName(functionClass));
// Load all the method parameters to pass them through the delegated method
for (int i = 1; i <= method.getParameterTypes().length; i++) {
mv.visitVarInsn(ALOAD, i);
}
mv.visitMethodInsn(getInvokeMethodInsnOpcode(method), Type.getDescriptor(method.getDeclaringClass()), method.getName(),
Type.getMethodDescriptor(method), true);
final Class<?> returnType = method.getReturnType();
mv.visitInsn(getReturnInsnOpcode(returnType));
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();
final byte[] byteCode = cw.toByteArray();
//noinspection unchecked
return (Class<? extends ScriptFunction>) ClassLoaderUtil.defineClass(ScriptFunction.class.getClassLoader(), name, byteCode);
}
/**
* Gets the opcode that should be used to invoke the specified method.
*
* @param method The method
* @return The opcode
*/
private static int getInvokeMethodInsnOpcode(Method method) {
if (method.getDeclaringClass().isInterface()) {
return INVOKEINTERFACE;
} else {
return INVOKEVIRTUAL;
}
}
/**
* Gets the opcode that should be used to return in a method
* with the specified return type.
*
* @param returnType The return type
* @return The opcode
*/
private static int getReturnInsnOpcode(Class<?> returnType) {
if (void.class.isAssignableFrom(returnType)) {
return RETURN;
} else if (returnType.isPrimitive()) {
if (long.class.isAssignableFrom(returnType)) {
return LRETURN;
} else if (double.class.isAssignableFrom(returnType)) {
return DRETURN;
} else if (float.class.isAssignableFrom(returnType)) {
return FRETURN;
}
return IRETURN;
}
return ARETURN;
}
}