first Kotlin version
This commit is contained in:
166
src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt
Normal file
166
src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt
Normal file
@@ -0,0 +1,166 @@
|
||||
@file:JvmName("Reflection")
|
||||
package mods.octarinecore.metaprog
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import mods.octarinecore.metaprog.Namespace.*
|
||||
import mods.octarinecore.tryDefault
|
||||
|
||||
/** Get a Java class with the given name. */
|
||||
fun getJavaClass(name: String) = tryDefault(null) { Class.forName(name) }
|
||||
|
||||
/** Get the field with the given name and type using reflection. */
|
||||
inline fun <reified T> Any.reflectField(field: String): T? =
|
||||
tryDefault(null) { this.javaClass.getDeclaredField(field) }?.let {
|
||||
it.isAccessible = true
|
||||
it.get(this) as T
|
||||
}
|
||||
|
||||
/** Get the static field with the given name and type using reflection. */
|
||||
inline fun <reified T> Class<*>.reflectStaticField(field: String): T? =
|
||||
tryDefault(null) { this.getDeclaredField(field) }?.let {
|
||||
it.isAccessible = true
|
||||
it.get(null) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nested _object_s of this _object_ with reflection.
|
||||
*
|
||||
* @return [Pair]s of (name, instance)
|
||||
*/
|
||||
val Any.reflectNestedObjects: List<Pair<String, Any>> get() = this.javaClass.declaredClasses.map {
|
||||
tryDefault(null) { it.name.split("$")[1] to it.getField("INSTANCE").get(null) }
|
||||
}.filterNotNull()
|
||||
|
||||
/**
|
||||
* Get all fields of this instance that match (or subclass) any of the given classes.
|
||||
*
|
||||
* @param[types] classes to look for
|
||||
* @return [Pair]s of (field name, instance)
|
||||
*/
|
||||
fun Any.reflectFieldsOfType(vararg types: Class<*>) = this.javaClass.declaredFields
|
||||
.filter { field -> types.any { it.isAssignableFrom(field.type) } }
|
||||
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) } }
|
||||
.filterNotNull()
|
||||
|
||||
enum class Namespace { OBF, SRG, MCP }
|
||||
|
||||
abstract class Resolvable<T> {
|
||||
abstract fun resolve(): T?
|
||||
val element: T? by lazy { resolve() }
|
||||
}
|
||||
|
||||
/** Return true if all given elements are found. */
|
||||
fun allAvailable(vararg codeElement: Resolvable<*>) = codeElement.all { it.element != null }
|
||||
|
||||
/**
|
||||
* Reference to a class.
|
||||
*
|
||||
* @param[mcpName] MCP name of the class
|
||||
* @param[obfName] obfuscated name of the class
|
||||
*/
|
||||
open class ClassRef(val mcpName: String, val obfName: String) : Resolvable<Class<*>>() {
|
||||
constructor(mcpName: String) : this(mcpName, mcpName)
|
||||
|
||||
companion object {
|
||||
val int = ClassRefPrimitive("I", Int::class.java)
|
||||
val float = ClassRefPrimitive("F", Float::class.java)
|
||||
val boolean = ClassRefPrimitive("Z", Boolean::class.java)
|
||||
val void = ClassRefPrimitive("V", null)
|
||||
}
|
||||
|
||||
fun name(namespace: Namespace) = if (namespace == Namespace.OBF) obfName else mcpName
|
||||
open fun asmDescriptor(namespace: Namespace) = "L${name(namespace).replace(".", "/")};"
|
||||
|
||||
override fun resolve() = listOf(mcpName, obfName).map { getJavaClass(it) }.filterNotNull().firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a primitive type.
|
||||
*
|
||||
* @param[name] ASM descriptor of this primitive type
|
||||
* @param[clazz] class of this primitive type
|
||||
*/
|
||||
class ClassRefPrimitive(name: String, val clazz: Class<*>?) : ClassRef(name) {
|
||||
override fun asmDescriptor(namespace: Namespace) = mcpName
|
||||
override fun resolve() = clazz
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a method.
|
||||
*
|
||||
* @param[parentClass] reference to the class containing the method
|
||||
* @param[mcpName] MCP name of the method
|
||||
* @param[srgName] SRG name of the method
|
||||
* @param[obfName] obfuscated name of the method
|
||||
* @param[returnType] reference to the return type
|
||||
* @param[returnType] references to the argument types
|
||||
*/
|
||||
class MethodRef(val parentClass: ClassRef,
|
||||
val mcpName: String,
|
||||
val srgName: String?,
|
||||
val obfName: String?,
|
||||
val returnType: ClassRef,
|
||||
vararg argTypes: ClassRef
|
||||
) : Resolvable<Method>() {
|
||||
constructor(parentClass: ClassRef, mcpName: String, returnType: ClassRef, vararg argTypes: ClassRef) :
|
||||
this(parentClass, mcpName, mcpName, mcpName, returnType, *argTypes)
|
||||
|
||||
val argTypes = argTypes
|
||||
|
||||
fun name(namespace: Namespace) = when(namespace) { OBF -> obfName!!; SRG -> srgName!!; MCP -> mcpName }
|
||||
fun asmDescriptor(namespace: Namespace) = "(${argTypes.map { it.asmDescriptor(namespace) }.fold(""){ s1, s2 -> s1 + s2 } })${returnType.asmDescriptor(namespace)}"
|
||||
|
||||
override fun resolve(): Method? =
|
||||
if (parentClass.element == null || argTypes.any { it.element == null }) null
|
||||
else {
|
||||
val args = argTypes.map { it.element!! }.toTypedArray()
|
||||
listOf(srgName!!, mcpName).map { tryDefault(null) {
|
||||
parentClass.element!!.getDeclaredMethod(it, *args)
|
||||
}}.filterNotNull().firstOrNull()
|
||||
?.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
/** Invoke this method using reflection. */
|
||||
fun invoke(receiver: Any, vararg args: Any) = element?.invoke(receiver, *args)
|
||||
|
||||
/** Invoke this static method using reflection. */
|
||||
fun invokeStatic(vararg args: Any) = element?.invoke(null, *args)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a field.
|
||||
*
|
||||
* @param[parentClass] reference to the class containing the field
|
||||
* @param[mcpName] MCP name of the field
|
||||
* @param[srgName] SRG name of the field
|
||||
* @param[obfName] obfuscated name of the field
|
||||
* @param[type] reference to the field type
|
||||
*/
|
||||
class FieldRef(val parentClass: ClassRef,
|
||||
val mcpName: String,
|
||||
val srgName: String?,
|
||||
val obfName: String?,
|
||||
val type: ClassRef?
|
||||
) : Resolvable<Field>() {
|
||||
constructor(parentClass: ClassRef, mcpName: String, type: ClassRef?) : this(parentClass, mcpName, mcpName, mcpName, type)
|
||||
|
||||
fun name(namespace: Namespace) = when(namespace) { OBF -> obfName!!; SRG -> srgName!!; MCP -> mcpName }
|
||||
fun asmDescriptor(namespace: Namespace) = type!!.asmDescriptor(namespace)
|
||||
|
||||
override fun resolve(): Field? =
|
||||
if (parentClass.element == null) null
|
||||
else {
|
||||
listOf(srgName!!, mcpName).map { tryDefault(null) {
|
||||
parentClass.element!!.getDeclaredField(it)
|
||||
}}.filterNotNull().firstOrNull()
|
||||
?.apply{ isAccessible = true }
|
||||
}
|
||||
|
||||
/** Get this field using reflection. */
|
||||
fun get(receiver: Any?) = element?.get(receiver)
|
||||
|
||||
/** Get this static field using reflection. */
|
||||
fun getStatic() = get(null)
|
||||
}
|
||||
199
src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt
Normal file
199
src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt
Normal file
@@ -0,0 +1,199 @@
|
||||
package mods.octarinecore.metaprog
|
||||
|
||||
import cpw.mods.fml.relauncher.IFMLLoadingPlugin
|
||||
import net.minecraft.launchwrapper.IClassTransformer
|
||||
import mods.octarinecore.metaprog.Namespace.*
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.tree.*
|
||||
|
||||
@IFMLLoadingPlugin.TransformerExclusions(
|
||||
"mods.octarinecore.metaprog",
|
||||
"kotlin"
|
||||
)
|
||||
open class ASMPlugin(vararg val classes: Class<*>) : IFMLLoadingPlugin {
|
||||
override fun getASMTransformerClass() = classes.map { it.canonicalName }.toTypedArray()
|
||||
override fun getAccessTransformerClass() = null
|
||||
override fun getModContainerClass() = null
|
||||
override fun getSetupClass() = null
|
||||
override fun injectData(data: Map<String, Any>) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for convenient bytecode transformers.
|
||||
*/
|
||||
open class Transformer : IClassTransformer {
|
||||
|
||||
val log = LogManager.getLogger(this)
|
||||
|
||||
/** The type of environment we are in. Assume MCP until proven otherwise. */
|
||||
var environment: Namespace = MCP
|
||||
|
||||
/** The list of transformers and targets. */
|
||||
var transformers: MutableList<Pair<MethodRef, MethodTransformContext.()->Unit>> = arrayListOf()
|
||||
|
||||
/** Add a transformation to perform. Call this during instance initialization.
|
||||
*
|
||||
* @param[method] the target method of the transformation
|
||||
* @param[trans] method transformation lambda
|
||||
*/
|
||||
fun transformMethod(method: MethodRef, trans: MethodTransformContext.()->Unit) = transformers.add(method to trans)
|
||||
|
||||
override fun transform(name: String?, transformedName: String?, classData: ByteArray?): ByteArray? {
|
||||
if (classData == null) return null
|
||||
if (name != transformedName) environment = OBF
|
||||
|
||||
val classNode = ClassNode().apply { val reader = ClassReader(classData); reader.accept(this, 0) }
|
||||
var workDone = false
|
||||
|
||||
val transformations: List<Pair<MethodTransformContext.()->Unit, MethodNode?>> = transformers.map { transformer ->
|
||||
if (transformedName != transformer.first.parentClass.mcpName) return@map transformer.second to null
|
||||
log.debug("Found class: $name -> $transformedName")
|
||||
log.debug(" searching: ${transformer.first.name(OBF)} ${transformer.first.asmDescriptor(OBF)} -> ${transformer.first.name(MCP)} ${transformer.first.asmDescriptor(MCP)}")
|
||||
transformer.second to classNode.methods.find {
|
||||
log.debug(" ${it.name} ${it.desc}")
|
||||
|
||||
it.name == transformer.first.name(MCP) && it.desc == transformer.first.asmDescriptor(MCP) ||
|
||||
it.name == transformer.first.name(OBF) && it.desc == transformer.first.asmDescriptor(OBF)
|
||||
}
|
||||
}
|
||||
|
||||
transformations.filter { it.second != null }.forEach {
|
||||
synchronized(it.second!!) {
|
||||
try {
|
||||
val trans = it.first
|
||||
MethodTransformContext(it.second!!, environment).trans()
|
||||
workDone = true
|
||||
} catch (e: Throwable) {
|
||||
log.warn("Error transforming method ${it.second!!.name} ${it.second!!.desc}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (!workDone) classData else ClassWriter(0).apply { classNode.accept(this) }.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows builder-style declarative definition of transformations. Transformation lambdas are extension
|
||||
* methods on this class.
|
||||
*
|
||||
* @param[method] the [MethodNode] currently being transformed
|
||||
* @param[environment] the type of environment we are in
|
||||
*/
|
||||
class MethodTransformContext(val method: MethodNode, val environment: Namespace) {
|
||||
/**
|
||||
* Find the first instruction that matches a predicate.
|
||||
*
|
||||
* @param[start] the instruction node to start iterating from
|
||||
* @param[predicate] the predicate to check
|
||||
*/
|
||||
fun find(start: AbstractInsnNode, predicate: (AbstractInsnNode) -> Boolean): AbstractInsnNode? {
|
||||
var current: AbstractInsnNode? = start
|
||||
while (current != null && !predicate(current)) current = current.next
|
||||
return current
|
||||
}
|
||||
|
||||
/** Find the first instruction in the current [MethodNode] that matches a predicate. */
|
||||
fun find(predicate: (AbstractInsnNode)->Boolean): AbstractInsnNode? = find(method.instructions.first, predicate)
|
||||
|
||||
/** Find the first instruction in the current [MethodNode] with the given opcode. */
|
||||
fun find(opcode: Int) = find { it.opcode == opcode }
|
||||
|
||||
/**
|
||||
* Insert new instructions after this one.
|
||||
*
|
||||
* @param[init] builder-style lambda to assemble instruction list
|
||||
*/
|
||||
fun AbstractInsnNode.insertAfter(init: InstructionList.()->Unit) = InstructionList(environment).apply{
|
||||
this.init(); list.reversed().forEach { method.instructions.insert(this@insertAfter, it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new instructions before this one.
|
||||
*
|
||||
* @param[init] builder-style lambda to assemble instruction list
|
||||
*/
|
||||
fun AbstractInsnNode.insertBefore(init: InstructionList.()->Unit) = InstructionList(environment).apply{
|
||||
this.init(); list.forEach { method.instructions.insertBefore(this@insertBefore, it) }
|
||||
}
|
||||
|
||||
/** Remove all isntructiuons between the given two (inclusive). */
|
||||
fun Pair<AbstractInsnNode, AbstractInsnNode>.remove() {
|
||||
var current: AbstractInsnNode? = first
|
||||
while (current != null && current != second) {
|
||||
val next = current.next
|
||||
method.instructions.remove(current)
|
||||
current = next
|
||||
}
|
||||
if (current != null) method.instructions.remove(current)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all isntructiuons between the given two (inclusive) with the specified instruction list.
|
||||
*
|
||||
* @param[init] builder-style lambda to assemble instruction list
|
||||
*/
|
||||
fun Pair<AbstractInsnNode, AbstractInsnNode>.replace(init: InstructionList.()->Unit) {
|
||||
val beforeInsn = first.previous
|
||||
remove()
|
||||
beforeInsn.insertAfter(init)
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches variable instructions.
|
||||
*
|
||||
* @param[opcode] instruction opcode
|
||||
* @param[idx] variable the opcode references
|
||||
*/
|
||||
fun varinsn(opcode: Int, idx: Int): (AbstractInsnNode)->Boolean = { insn ->
|
||||
insn.opcode == opcode && insn is VarInsnNode && insn.`var` == idx
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows builder-style declarative definition of instruction lists.
|
||||
*
|
||||
* @param[environment] the type of environment we are in
|
||||
*/
|
||||
class InstructionList(val environment: Namespace) {
|
||||
|
||||
/** The instruction list being assembled. */
|
||||
val list: MutableList<AbstractInsnNode> = arrayListOf()
|
||||
|
||||
/**
|
||||
* Adds a variable instruction.
|
||||
*
|
||||
* @param[opcode] instruction opcode
|
||||
* @param[idx] variable the opcode references
|
||||
*/
|
||||
fun varinsn(opcode: Int, idx: Int) = list.add(VarInsnNode(opcode, idx))
|
||||
|
||||
/**
|
||||
* Adds an INVOKESTATIC instruction.
|
||||
*
|
||||
* @param[target] the target method of the instruction
|
||||
* @param[isInterface] true if the target method is defined by an interface
|
||||
*/
|
||||
fun invokeStatic(target: MethodRef, isInterface: Boolean = false) = list.add(MethodInsnNode(
|
||||
Opcodes.INVOKESTATIC,
|
||||
target.parentClass.name(environment).replace(".", "/"),
|
||||
target.name(environment),
|
||||
target.asmDescriptor(environment),
|
||||
isInterface
|
||||
))
|
||||
|
||||
/**
|
||||
* Adds a GETFIELD instruction.
|
||||
*
|
||||
* @param[target] the target field of the instruction
|
||||
*/
|
||||
fun getField(target: FieldRef) = list.add(FieldInsnNode(
|
||||
Opcodes.GETFIELD,
|
||||
target.parentClass.name(environment).replace(".", "/"),
|
||||
target.name(environment),
|
||||
target.asmDescriptor(environment)
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user