diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..550b373 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# fabric + +run/ diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..4b07c8b --- /dev/null +++ b/README.MD @@ -0,0 +1,54 @@ +# gudASM + +The ~~worst~~ best thing to happen to Fabric modding. + +#### TL;DR + +It allows you to do direct bytecode manipulation and aims to make it "easy". You should only use this when [mixins](https://github.com/SpongePowered/Mixin/wiki) really can't do what you need. + +#### What can this do? + +Anything you want it to, since it allows you to edit classes at the most basic level. + +Just be mindful of what classes you load and things should work fine. + +#### Features + +- Raw class transformations +- A large collection of helper methods +- @ForceBootstrap and @ForceInline for when you need ~~to break things~~ speed. +- Custom entry point + +#### How to use + +Create a class the implements `AsmInitializer` and put it in your mod config file as an entrypoint, something like: +```JSON +{ + ... + "entrypoints": { + "gud_asm": [ + "com.example.mod.ILikeBreakingThings" + ] + }, + ... +} +``` + +Then implement the `AsmInitializer.onInitializeAsm()` method and do something like this: +```Java +package com.example.mod; + +import net.gudenau.asm.api.v0.AsmInitializer; +import net.gudenau.asm.api.v0.AsmRegistry; + +public class ILikeBreakingThings implements AsmInitializer{ + @Override + public void onInitializeAsm(){ + AsmRegistry.getInstance().registerTransformer(new AmazingTransformer()); + } +} +``` + +The rest should be pretty straight forward. + +Also, please don't combine `AsmInitializer` with other initializers. That is just asking for crashes. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..723c533 --- /dev/null +++ b/build.gradle @@ -0,0 +1,78 @@ +plugins { + id 'fabric-loom' version '0.4-SNAPSHOT' + id 'maven-publish' +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +archivesBaseName = project.archives_base_name +version = project.mod_version +group = project.maven_group + +dependencies { + //to change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. + compile "org.jetbrains:annotations:${project.annotations_version}" +} + +processResources { + inputs.property "version", project.version + + from(sourceSets.main.resources.srcDirs) { + include "fabric.mod.json" + expand "version": project.version + } + + from(sourceSets.main.resources.srcDirs) { + exclude "fabric.mod.json" + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = "sources" + from sourceSets.main.allSource +} + +jar { + from "LICENSE" +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + // add all the jars that should be included when publishing to maven + artifact(remapJar) { + builtBy remapJar + } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } + + // select the repositories you want to publish to + repositories { + // uncomment to publish to the local maven + // mavenLocal() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..35235ca --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G + +# Fabric Properties +# check these on https://fabricmc.net/use +minecraft_version=1.16.2 +yarn_mappings=1.16.2+build.46:v2 +loader_version=0.9.2+build.206 + +# Mod Properties +mod_version = 0.0.1 +maven_group = net.gudenau.minecraft +archives_base_name = gud_asm + +# Dependencies +# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api +fabric_version=0.20.0+build.399-1.16 +annotations_version=16.0.2 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..490fda8 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6c9a224 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..62bd9b9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5b60df3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/GudAsm.java b/src/main/java/net/gudenau/minecraft/asm/GudAsm.java new file mode 100644 index 0000000..9ac51e6 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/GudAsm.java @@ -0,0 +1,11 @@ +package net.gudenau.minecraft.asm; + +import net.fabricmc.api.ModInitializer; +import org.apache.logging.log4j.LogManager; + +public class GudAsm implements ModInitializer{ + @Override + public void onInitialize(){ + LogManager.getLogger("gud_asm").fatal("Welcome to the wacky world of gudASM, things might break in weird and wonderful ways!"); + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmInitializer.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmInitializer.java new file mode 100644 index 0000000..3c8d27d --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmInitializer.java @@ -0,0 +1,13 @@ +package net.gudenau.minecraft.asm.api.v0; + +/** + * An ASM mod initializer. + * + * Load as few classes as you can get away with here! + * */ +public interface AsmInitializer{ + /** + * Called to initialize this asm mod. + * */ + void onInitializeAsm(); +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmRegistry.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmRegistry.java new file mode 100644 index 0000000..95f55d8 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmRegistry.java @@ -0,0 +1,42 @@ +package net.gudenau.minecraft.asm.api.v0; + +import net.gudenau.minecraft.asm.impl.RegistryImpl; + +/** + * The place to register your gross ASM hacks. + * */ +public interface AsmRegistry{ + /** + * Gets the instance of the registry. + * + * @return The registry + * */ + static AsmRegistry getInstance(){ + return RegistryImpl.INSTANCE; + } + + /** + * Registers a class transformer for transforming classes before mixins. + * + * This one should not be used unless it is 100% required. + * + * @param transformer The transformer to register + * */ + void registerEarlyTransformer(Transformer transformer); + + /** + * Registers a class transformer for transforming classes after mixins. + * + * This is the most compatible one. + * + * @param transformer The transformer to register + * */ + void registerTransformer(Transformer transformer); + + /** + * Registers a class cache. + * + * @param cache The class cache + * */ + void registerClassCache(ClassCache cache); +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmUtils.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmUtils.java new file mode 100644 index 0000000..0d3abb0 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/AsmUtils.java @@ -0,0 +1,666 @@ +package net.gudenau.minecraft.asm.api.v0; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import net.gudenau.minecraft.asm.api.v0.functional.BooleanFunction; +import net.gudenau.minecraft.asm.util.AsmUtilsImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; + +/** + * General ASM utils. + * */ +@SuppressWarnings({"unused", "RedundantSuppression"}) // Idea is silly +public interface AsmUtils{ + /** + * Get the handle to AsmUtils. + * + * @return The AsmUtils handle + * */ + @NotNull + static AsmUtils getInstance(){ + return AsmUtilsImpl.INSTANCE; + } + + /** + * Get the handle to the Type cache. + * + * @return The handle to the type cache + * */ + @NotNull + TypeCache getTypeCache(); + + // --- Annotation stuff --- + + /** + * Checks if a method has an annotation. + * + * @param methodNode The method to check + * @param type The annotation type to check for + * + * @return True if the annotation is present, false if absent + * */ + default boolean hasAnnotation(@NotNull MethodNode methodNode, @NotNull Type type){ + return hasAnnotation(methodNode.visibleAnnotations, methodNode.invisibleAnnotations, type); + } + + /** + * Checks if a class has an annotation. + * + * @param owner The class to check + * @param type The annotation type to check for + * + * @return True if the annotation is present, false if absent + * */ + default boolean hasAnnotation(@NotNull ClassNode owner, @NotNull Type type){ + return hasAnnotation(owner.visibleAnnotations, owner.invisibleAnnotations, type); + } + + /** + * Checks if an annotation exists in lists. + * + * @param visibleAnnotations Visible annotations + * @param invisibleAnnotations Invisible annotations + * @param type The annotation type to check for + * + * @return True if the annotation is present, false if absent + * */ + default boolean hasAnnotation(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type){ + return getAnnotation(visibleAnnotations, invisibleAnnotations, type).isPresent(); + } + + /** + * Gets annotations in a method. + * + * @param method The method to get annotations from + * @param type The annotation type to check for + * + * @return Found annotations + * */ + default List getAnnotations(@NotNull MethodNode method, @NotNull Type type){ + return getAnnotations(method.visibleAnnotations, method.invisibleAnnotations, type); + } + + /** + * Gets annotations in a class. + * + * @param owner The class to get annotations from + * @param type The annotation type to check for + * + * @return Found annotations + * */ + default List getAnnotations(@NotNull ClassNode owner, @NotNull Type type){ + return getAnnotations(owner.visibleAnnotations, owner.invisibleAnnotations, type); + } + + /** + * Gets annotations in the lists. + * + * @param visibleAnnotations Visible annotations + * @param invisibleAnnotations Invisible annotations + * @param type The annotation type to check for + * + * @return Found annotations + * */ + List getAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type); + + /** + * Gets the first matching annotation in a method. + * + * @param method The method to get annotations from + * @param type The annotation type to check for + * + * @return Found annotation + * */ + default Optional getAnnotation(@NotNull MethodNode method, @NotNull Type type){ + return getAnnotation(method.visibleAnnotations, method.invisibleAnnotations, type); + } + + /** + * Gets the first matching annotation in a class. + * + * @param owner The class to get annotations from + * @param type The annotation type to check for + * + * @return Found annotations + * */ + default Optional getAnnotation(@NotNull ClassNode owner, @NotNull Type type){ + return getAnnotation(owner.visibleAnnotations, owner.invisibleAnnotations, type); + } + + /** + * Gets the first matching annotation in the lists. + * + * @param visibleAnnotations Visible annotations + * @param invisibleAnnotations Invisible annotations + * @param type The annotation type to check for + * + * @return Found annotation + * */ + Optional getAnnotation(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type); + + /** + * Adds an annotation to a method. + * + * @param method The method to add annotations to + * @param visible Are they be visible + * @param annotation The annotation to add + * */ + default void addAnnotation(@NotNull MethodNode method, boolean visible, @NotNull AnnotationNode annotation){ + addAnnotations(method, visible, Collections.singletonList(annotation)); + } + + /** + * Adds an annotation to a class. + * + * @param owner The class to add annotations to + * @param visible Are they be visible + * @param annotation The annotation to add + * */ + default void addAnnotation(@NotNull ClassNode owner, boolean visible, @NotNull AnnotationNode annotation){ + addAnnotations(owner, visible, Collections.singletonList(annotation)); + } + + /** + * Adds annotations to a method. + * + * @param method The method to add annotations to + * @param visible Are they be visible + * @param annotations The annotations to add + * */ + default void addAnnotations(@NotNull MethodNode method, boolean visible, @NotNull AnnotationNode... annotations){ + addAnnotations(method, visible, Arrays.asList(annotations)); + } + + /** + * Adds annotations to a method. + * + * @param method The method to add annotations to + * @param visible Are they be visible + * @param annotations The annotations to add + * */ + void addAnnotations(@NotNull MethodNode method, boolean visible, @NotNull Collection annotations); + + /** + * Adds annotations to a class. + * + * @param owner The class to add annotations to + * @param visible Are they be visible + * @param annotations The annotations to add + * */ + default void addAnnotations(@NotNull ClassNode owner, boolean visible, @NotNull AnnotationNode... annotations){ + addAnnotations(owner, visible, Arrays.asList(annotations)); + } + + /** + * Adds annotations to a class. + * + * @param owner The class to add annotations to + * @param visible Are they be visible + * @param annotations The annotations to add + * */ + void addAnnotations(@NotNull ClassNode owner, boolean visible, @NotNull Collection annotations); + + /** + * Removes annotations from a method. + * + * @param method The method to remove annotations from + * @param annotations The annotations to remove + * */ + default boolean removeAnnotations(@NotNull MethodNode method, @NotNull AnnotationNode... annotations){ + return removeAnnotations(method.visibleAnnotations, method.invisibleAnnotations, annotations); + } + + /** + * Removes annotations from a class. + * + * @param owner The class to remove annotations from + * @param annotations The annotations to remove + * */ + default boolean removeAnnotations(@NotNull ClassNode owner, @NotNull AnnotationNode... annotations){ + return removeAnnotations(owner.visibleAnnotations, owner.invisibleAnnotations, annotations); + } + + /** + * Removes annotations from lists. + * + * @param visibleAnnotations The visible annotations + * @param invisibleAnnotations The invisible annotations + * @param annotations The annotations to remove + * */ + default boolean removeAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull AnnotationNode... annotations){ + return removeAnnotations(visibleAnnotations, invisibleAnnotations, Arrays.asList(annotations)); + } + + /** + * Removes annotations from a method. + * + * @param method The method to remove annotations from + * @param annotations The annotations to remove + * */ + default boolean removeAnnotations(@NotNull MethodNode method, @NotNull Collection annotations){ + return removeAnnotations(method.visibleAnnotations, method.invisibleAnnotations, annotations); + } + + /** + * Removes annotations from a class. + * + * @param owner The class to remove annotations from + * @param annotations The annotations to remove + * */ + default boolean removeAnnotations(@NotNull ClassNode owner, @NotNull Collection annotations){ + return removeAnnotations(owner.visibleAnnotations, owner.invisibleAnnotations, annotations); + } + + /** + * Removes annotations from lists. + * + * @param visibleAnnotations The visible annotations + * @param invisibleAnnotations The invisible annotations + * @param annotations The annotations to remove + * */ + boolean removeAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Collection annotations); + + /** + * Removes annotations from a method. + * + * @param method The method to remove annotations from + * @param type The type of the annotations to remove + * */ + default boolean removeAnnotations(@NotNull MethodNode method, @NotNull Type type){ + return removeAnnotations(method.visibleAnnotations, method.invisibleAnnotations, type); + } + + /** + * Removes annotations from a class. + * + * @param owner The class to remove annotations from + * @param type The type of the annotations to remove + * */ + default boolean removeAnnotations(@NotNull ClassNode owner, @NotNull Type type){ + return removeAnnotations(owner.visibleAnnotations, owner.invisibleAnnotations, type); + } + + /** + * Removes annotations from lists. + * + * @param visibleAnnotations The visible annotations + * @param invisibleAnnotations The invisible annotations + * @param type The type of the annotations to remove + * */ + boolean removeAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type); + + /** + * Removes an annotation from a method. + * + * @param method The method to remove an annotation from + * @param annotation The annotation to remove + * */ + default boolean removeAnnotation(@NotNull MethodNode method, @NotNull AnnotationNode annotation){ + return removeAnnotation(method.visibleAnnotations, method.invisibleAnnotations, annotation); + } + + /** + * Removes an annotation from a class. + * + * @param owner The class to remove an annotation from + * @param annotation The annotation to remove + * */ + default boolean removeAnnotation(@NotNull ClassNode owner, @NotNull AnnotationNode annotation){ + return removeAnnotation(owner.visibleAnnotations, owner.invisibleAnnotations, annotation); + } + + /** + * Removes an annotation from lists. + * + * @param visibleAnnotations The visible annotations + * @param invisibleAnnotations The invisible annotations + * @param annotation The annotation to remove + * */ + default boolean removeAnnotation(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull AnnotationNode annotation){ + return removeAnnotations(visibleAnnotations, invisibleAnnotations, Collections.singletonList(annotation)); + } + + // --- Instruction stuff --- + + /** + * Finds all nodes in the method that are of the provided type and opcode. + * + * @param method The method to search + * @param opcode The instruction opcode + * @param type The node type + * + * @return A list of all matching nodes + * */ + @NotNull + default List findMatchingNodes(@NotNull MethodNode method, int opcode, @NotNull Class type){ + return findMatchingNodes(method.instructions, (node)->node.getOpcode() == opcode && type.isInstance(node)); + } + + /** + * Finds all nodes in the instructions that are of the provided type and opcode. + * + * @param instructions The instructions to search + * @param opcode The instruction opcode + * @param type The node type + * + * @return A list of all matching nodes + * */ + @NotNull + default List findMatchingNodes(@NotNull InsnList instructions, int opcode, @NotNull Class type){ + return findMatchingNodes(instructions, (node)->node.getOpcode() == opcode && type.isInstance(node)); + } + + /** + * Finds all nodes in the method that are of the provided type. + * + * @param method The method to search + * @param type The node type + * + * @return A list of all matching nodes + * */ + @NotNull + default List findMatchingNodes(@NotNull MethodNode method, @NotNull Class type){ + return findMatchingNodes(method.instructions, type::isInstance); + } + + /** + * Finds all nodes in the instructions that are of the provided type. + * + * @param instructions The instructions to search + * @param type The node type + * + * @return A list of all matching nodes + * */ + @NotNull + default List findMatchingNodes(@NotNull InsnList instructions, @NotNull Class type){ + return findMatchingNodes(instructions, type::isInstance); + } + + /** + * Finds all nodes in the method that are match the provider checker. + * + * @param method The method to search + * @param checker The node checker + * + * @return A list of all matching nodes + * */ + @NotNull + default List findMatchingNodes(@NotNull MethodNode method, @NotNull BooleanFunction checker){ + return findMatchingNodes(method.instructions, checker); + } + + /** + * Finds all nodes in the instructions that are match the provider checker. + * + * @param instructions The instructions to search + * @param checker The node checker + * + * @return A list of all matching nodes + * */ + @NotNull + List findMatchingNodes(@NotNull InsnList instructions, @NotNull BooleanFunction checker); + + /** + * Searches for method call instructions in a method. + * + * @param method The method to search in + * @param owner Owner, or null if it doesn't matter + * @param name Name, or null if it doesn't matter + * @param description Description, or null if it doesn't matter + * + * @return A list of all matching method calls + * */ + @NotNull + default List findMethodCalls(@NotNull MethodNode method, @Nullable String owner, @Nullable String name, @Nullable String description){ + return findMethodCalls(method.instructions, -1, owner, name, description); + } + + /** + * Searches for method call in an instruction list. + * + * @param instructions The instructions to search in + * @param owner Owner, or null if it doesn't matter + * @param name Name, or null if it doesn't matter + * @param description Description, or null if it doesn't matter + * + * @return A list of all matching method calls + * */ + @NotNull + default List findMethodCalls(@NotNull InsnList instructions, @Nullable String owner, @Nullable String name, @Nullable String description){ + return findMethodCalls(instructions, -1, owner, name, description); + } + + /** + * Searches for method call instructions in a method. + * + * @param method The method to search in + * @param opcode Opcode, or -1 if it doesn't matter + * @param owner Owner, or null if it doesn't matter + * @param name Name, or null if it doesn't matter + * @param description Description, or null if it doesn't matter + * + * @return A list of all matching method calls + * */ + @NotNull + default List findMethodCalls(@NotNull MethodNode method, int opcode, @Nullable String owner, @Nullable String name, @Nullable String description){ + return findMethodCalls(method.instructions, opcode, owner, name, description); + } + + /** + * Searches for method call in an instruction list. + * + * @param instructions The instructions to search in + * @param opcode Opcode, or -1 if it doesn't matter + * @param owner Owner, or null if it doesn't matter + * @param name Name, or null if it doesn't matter + * @param description Description, or null if it doesn't matter + * + * @return A list of all matching method calls + * */ + @NotNull + List findMethodCalls(@NotNull InsnList instructions, int opcode, @Nullable String owner, @Nullable String name, @Nullable String description); + + /** + * Finds up to count nodes after the provided node. + * + * This is exclusive. + * + * @param node The initial node + * @param count The maximum amount of nodes + * + * @return A list of count or fewer trailing nodes + * */ + @NotNull + default List findTrailingNodes(@NotNull AbstractInsnNode node, int count){ + List nodes = findSurroundingNodes(node, 0, count); + nodes.remove(0); + return nodes; + } + + /** + * Finds up to count nodes before the provided node. + * + * This is exclusive. + * + * @param node The initial node + * @param count The maximum amount of nodes + * + * @return A list of count or fewer leading nodes + * */ + @NotNull + default List findLeadingNodes(@NotNull AbstractInsnNode node, int count){ + List nodes = findSurroundingNodes(node, 0, count); + nodes.remove(nodes.size() - 1); + return nodes; + } + + /** + * Finds up to nodes surrounding another node. + * + * This is inclusive. + * + * @param node The initial node + * @param leading The maximum amount of nodes + * @param trailing The maximum amount of trailing nodes + * + * @return A list of matching nodes + * */ + @NotNull + List findSurroundingNodes(@NotNull AbstractInsnNode node, int leading, int trailing); + + /** + * Finds return instructions in a method. + * + * @param method The method to search + * + * @return A list of return instructions + * */ + @NotNull + default List findReturns(@NotNull MethodNode method){ + return findReturns(method.instructions); + } + + /** + * Finds return instructions in a list of instructions. + * + * @param instructions The instructions to search + * + * @return A list of return instructions + * */ + @NotNull + List findReturns(@NotNull InsnList instructions); + + /** + * Finds nodes between a beginning and end, exclusive. + * + * @param range A pair of nodes + * + * @return The middle instructions, or null on error + * */ + @Nullable + default List findInRange(Pair range){ + return findInRange(range.getA(), range.getB()); + } + + /** + * Finds nodes between a beginning and end, exclusive. + * + * @param start The first node + * @param end The last node + * + * @return The middle instructions, or null on error + * */ + @Nullable + List findInRange(AbstractInsnNode start, AbstractInsnNode end); + + // --- Dynamic instruction stuff --- + + /** + * Translates the handle's tag to an opcode. + * + * @param handle The handle + * + * @return The handle's corresponding opcode + * */ + default int getOpcodeFromHandle(@NotNull Handle handle){ + return getOpcodeFromHandleTag(handle.getTag()); + } + + /** + * Translates the a handle tag to an opcode. + * + * @param tag The handle tag + * + * @return The tag's corresponding opcode + * */ + int getOpcodeFromHandleTag(int tag); + + /** + * Translates an instruction's opcode into a handle tag. + * + * @param instruction The instruction + * + * @return The instruction's corresponding tag + * */ + default int getHandleTagFromInstruction(@NotNull AbstractInsnNode instruction){ + return getHandleTagFromOpcode(instruction.getOpcode()); + } + + /** + * Translates an opcode into a handle tag. + * + * @param opcode The opcode + * + * @return The opcode's corresponding tag + * */ + int getHandleTagFromOpcode(int opcode); + + // --- Code generation stuff --- + + /** + * Generates a `throw new type` InsnList. + * + * @param type The type of exception + * + * @return The instruction list + * */ + @NotNull + default InsnList createExceptionList(@NotNull Type type){ + return createExceptionList(type, null); + } + + /** + * Generates a `throw new type` InsnList. + * + * @param type The type of exception + * @param message The message, or null for none + * + * @return The instruction list + * */ + @NotNull + InsnList createExceptionList(@NotNull Type type, @Nullable String message); + + // -- ClassNode stuff -- + + /** + * Finds a method in a class. + * + * @param owner The class + * @param name The name of the method + * @param desc The description of the method + * + * @return The method + * */ + @NotNull + Optional findMethod(@NotNull ClassNode owner, @NotNull String name, @NotNull String desc); + + // --- Misc --- + + /** + * Gets the name of an instruction. + * + * @param instruction The instruction + * + * @return The name of the instruction + * */ + @NotNull + default String getInstructionName(@NotNull AbstractInsnNode instruction){ + return getOpcodeName(instruction.getOpcode()); + } + + /** + * Gets the name of an opcode. + * + * @param opcode The opcode + * + * @return The name of the opcode + * */ + @NotNull + String getOpcodeName(int opcode); +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/ClassCache.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/ClassCache.java new file mode 100644 index 0000000..aaa52c9 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/ClassCache.java @@ -0,0 +1,41 @@ +package net.gudenau.minecraft.asm.api.v0; + +import java.io.IOException; +import java.util.Optional; + +/** + * A way to cache classes to speed up class loading. + * */ +public interface ClassCache{ + /** + * The identifier of the cache. + * */ + Identifier getName(); + + /** + * Load the contents of the cache. + * */ + void load() throws IOException; + + /** + * Save the contents of the cache. + * */ + void save() throws IOException; + + /** + * Get a cached class entry. + * + * @param original The original class + * + * @return The stored class + * */ + Optional getEntry(byte[] original); + + /** + * Creates an entry in the cache. + * + * @param original The original class + * @param modified The modified class + * */ + void putEntry(byte[] original, byte[] modified); +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/Identifier.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/Identifier.java new file mode 100644 index 0000000..f0a7f28 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/Identifier.java @@ -0,0 +1,65 @@ +package net.gudenau.minecraft.asm.api.v0; + +import java.util.Objects; + +/** + * A simple namespace/path identifier. + * + * Similar to Minecraft's but doesn't load any of it's classes. + * */ +public final class Identifier{ + private final String modId; + private final String name; + + /** + * Creates a new identifier from a mod id and a name. + * + * @param modId The mod id + * @param name The name + * */ + public Identifier(String modId, String name){ + this.modId = modId; + this.name = name; + } + + /** + * Gets the mod id. + * + * @return The mod id + * */ + public String getModId(){ + return modId; + } + + /** + * Gets the name. + * + * @return The name + * */ + public String getName(){ + return name; + } + + @Override + public String toString(){ + return modId + ":" + name; + } + + @Override + public boolean equals(Object o){ + if(this == o){ + return true; + } + if(o == null || getClass() != o.getClass()){ + return false; + } + Identifier that = (Identifier)o; + return Objects.equals(modId, that.modId) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode(){ + return Objects.hash(modId, name); + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/Pair.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/Pair.java new file mode 100644 index 0000000..8d20f2d --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/Pair.java @@ -0,0 +1,83 @@ +package net.gudenau.minecraft.asm.api.v0; + +import java.util.Objects; + +/** + * A pair of things. + * + * Minecraft has one but this is to keep the MC stuff from getting loaded. + * + * Use this one instead of the MC one in transformers! + * */ +public final class Pair{ + /** + * Creates a new pair. + * + * @param a The A value + * @param b The B value + * + * @return The pair + * */ + public static Pair of(A a, B b){ + return new Pair<>(a, b); + } + + /** + * The A value. + * */ + private final A a; + + /** + * The B value. + * */ + private final B b; + + private Pair(A a, B b){ + this.a = a; + this.b = b; + } + + /** + * Get's the A value. + * + * @return The A value + * */ + public A getA(){ + return a; + } + + /** + * Get's the B value. + * + * @return The B value + * */ + public B getB(){ + return b; + } + + @Override + public boolean equals(Object o){ + if(this == o){ + return true; + } + if(o == null || getClass() != o.getClass()){ + return false; + } + Pair pair = (Pair)o; + return Objects.equals(a, pair.a) && + Objects.equals(b, pair.b); + } + + @Override + public int hashCode(){ + return Objects.hash(a, b); + } + + @Override + public String toString(){ + return "Pair{" + + "a=" + a + + ", b=" + b + + '}'; + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/Transformer.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/Transformer.java new file mode 100644 index 0000000..6bc51f7 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/Transformer.java @@ -0,0 +1,50 @@ +package net.gudenau.minecraft.asm.api.v0; + +import org.objectweb.asm.tree.ClassNode; + +/** + * The entry point for bytecode transformers. + * */ +public interface Transformer{ + /** + * The name of this transformer. + * + * @return The identifier of this transformer + * */ + Identifier getName(); + + /** + * A quick check to see if this transformer might handle a class. + * + * @param name The name of the class + * @param transformedName The transformed name of the class + * + * @return true if the class might get transformed + * */ + boolean handlesClass(String name, String transformedName); + + /** + * Transforms a class. + * + * @param classNode The class that is being transformer + * @param flags Various flags that might be useful + * + * @return True if the class was transformer, false otherwise + * */ + boolean transform(ClassNode classNode, Flags flags); + + /** + * Various flags that might be useful. + * */ + interface Flags{ + /** + * Request that ASM calculate maxes when writing the modified class. + * */ + void requestMaxes(); + + /** + * Request that ASM calculate frames when writing the modified class. + * */ + void requestFrames(); + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/TypeCache.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/TypeCache.java new file mode 100644 index 0000000..a43e6ca --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/TypeCache.java @@ -0,0 +1,51 @@ +package net.gudenau.minecraft.asm.api.v0; + +import org.objectweb.asm.Type; + +/** + * Type cache to try and keep less instances floating around. + * */ +public interface TypeCache{ + /** + * Gets a Type from a descriptor. + * + * I.E.: "Ljava/lang/Object;" + * + * @param descriptor The descriptor + * + * @return The type + * */ + Type getType(String descriptor); + + /** + * Gets an Type from an internal name. + * + * I.E.: "java/lang/Object" + * + * @param name The internal name + * + * @return The type + * */ + Type getObjectType(String name); + + /** + * Gets a method type from a descriptor. + * + * I.E.: "(Ljava/lang/Object;)V" + * + * @param descriptor The method descriptor + * + * @return The method type + * */ + Type getMethodType(String descriptor); + + /** + * Gets a method type from individual types. + * + * @param returnType The return type + * @param arguments The argument types + * + * @return The method type + * */ + Type getMethodType(Type returnType, Type... arguments); +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/annotation/ForceBootloader.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/annotation/ForceBootloader.java new file mode 100644 index 0000000..8179ae5 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/annotation/ForceBootloader.java @@ -0,0 +1,17 @@ +package net.gudenau.minecraft.asm.api.v0.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Forces a class to be loaded by the bootloader {@link ClassLoader ClassLoader}, use sparingly. + * + * gudenau disclaims any responsibility for damages caused by the use of {@link ForceBootloader ForceBootloader}. + * + * All Rights Reserved. + * */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface ForceBootloader{} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/annotation/ForceInline.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/annotation/ForceInline.java new file mode 100644 index 0000000..873ecd8 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/annotation/ForceInline.java @@ -0,0 +1,16 @@ +package net.gudenau.minecraft.asm.api.v0.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A custom {@link jdk.internal.vm.annotation.ForceInline ForceInline} annotation that pairs with {@link ForceBootloader ForceBootloader}. + * + * You acknowledge that Licensed Software is not designed or intended for use in the design, construction, operation or maintenance of any nuclear facility. gudenau disclaims any express or implied warranty of fitness for such uses. No right, title or interest in or to any trademark, service mark, logo or trade name of Sun or its licensors is granted under this Agreement. Additional restrictions for developers and/or publishers licenses are set forth in the Supplemental License Terms. + * */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface ForceInline{ +} diff --git a/src/main/java/net/gudenau/minecraft/asm/api/v0/functional/BooleanFunction.java b/src/main/java/net/gudenau/minecraft/asm/api/v0/functional/BooleanFunction.java new file mode 100644 index 0000000..4232fee --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/api/v0/functional/BooleanFunction.java @@ -0,0 +1,11 @@ +package net.gudenau.minecraft.asm.api.v0.functional; + +/** + * A {@link java.util.function.Function} that returns a boolean. + * + * Avoids boxing. + * */ +@FunctionalInterface +public interface BooleanFunction{ + boolean apply(T object); +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/Bootstrap.java b/src/main/java/net/gudenau/minecraft/asm/impl/Bootstrap.java new file mode 100644 index 0000000..7593eef --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/Bootstrap.java @@ -0,0 +1,127 @@ +package net.gudenau.minecraft.asm.impl; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.fabricmc.loader.metadata.EntrypointMetadata; +import net.fabricmc.loader.metadata.LoaderModMetadata; +import net.gudenau.minecraft.asm.api.v0.AsmInitializer; +import net.gudenau.minecraft.asm.api.v0.ClassCache; +import net.gudenau.minecraft.asm.util.FileUtils; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.transformer.FabricMixinTransformerProxy; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; + +// Bootstraps all the mess we make. +public class Bootstrap{ + public static boolean enableCache = false; + + public static void setup(){ + // Load the configuration + try{ + Configuration.load(); + }catch(IOException e){ + e.printStackTrace(); + } + + // Load all the other stuff we will need + FabricLoader loader = FabricLoader.getInstance(); + Set entryClasses = new HashSet<>(); + for(ModContainer mod : loader.getAllMods()){ + ModMetadata rawMeta = mod.getMetadata(); + if(rawMeta instanceof LoaderModMetadata){ + List entrypoints = ((LoaderModMetadata)rawMeta).getEntrypoints("gud_asm"); + if(entrypoints == null || entrypoints.isEmpty()){ + continue; + } + + for(EntrypointMetadata entrypoint : entrypoints){ + entryClasses.add(entrypoint.getValue()); + } + } + } + + // And call them all + if(!entryClasses.isEmpty()){ + for(String entryClass : entryClasses){ + try{ + Class klass = Class.forName(entryClass); + if(AsmInitializer.class.isAssignableFrom(klass)){ + //noinspection unchecked + ((Class)klass).getDeclaredConstructor() + .newInstance() + .onInitializeAsm(); + } + }catch(Throwable t){ + new RuntimeException(entryClass + " encountered an error", t).printStackTrace(); + } + } + } + + RegistryImpl registry = RegistryImpl.INSTANCE; + + // Let the cache load itself + ClassCache cache = registry.getCache().orElse(null); + if(cache != null){ + try{ + cache.load(); + enableCache = true; + }catch(IOException e){ + new RuntimeException("Failed to load class cache " + cache.getName(), e).printStackTrace(); + } + } + + // Clean out the class dump if dumping is enabled + if(Configuration.DUMP.get() != Configuration.DumpMode.OFF){ + try{ + FileUtils.delete(loader.getGameDir().resolve("gud").resolve("asm").resolve("dump")); + }catch(IOException ignored){} + } + + // Hack into knot. + ClassLoader classLoader = Bootstrap.class.getClassLoader(); + MixinTransformer.setClassLoader(classLoader); // Needed for ASM, don't ask + + try{ + // Get classes we can't normally access + Class KnotClassLoader = ReflectionHelper.loadClass(classLoader, "net.fabricmc.loader.launch.knot.KnotClassLoader"); + Class KnotClassDelegate = ReflectionHelper.loadClass(classLoader, "net.fabricmc.loader.launch.knot.KnotClassDelegate"); + + // Get the class delegate + MethodHandle KnotClassLoader$delegate$getter = ReflectionHelper.findGetter(KnotClassLoader, classLoader, "delegate", KnotClassDelegate); + Object KnotClassLoader$delegate = KnotClassLoader$delegate$getter.invoke(); + + // Get the transformer proxy + MethodHandle KnotClassDelegate$mixinTransformer$getter = ReflectionHelper.findGetter(KnotClassDelegate, KnotClassLoader$delegate, "mixinTransformer", FabricMixinTransformerProxy.class); + FabricMixinTransformerProxy KnotClassDelegate$mixinTransformer = (FabricMixinTransformerProxy)KnotClassDelegate$mixinTransformer$getter.invokeExact(); + + // Get the environment's transformer + MethodHandle MixinEnvironment$transformer$getter = ReflectionHelper.findStaticGetter(MixinEnvironment.class, "transformer", IMixinTransformer.class); + IMixinTransformer originalTransformer = (IMixinTransformer)MixinEnvironment$transformer$getter.invokeExact(); + + // Clear it, otherwise it will kill Minecraft + MethodHandle MixinEnvironment$transformer$setter = ReflectionHelper.findStaticSetter(MixinEnvironment.class, "transformer", IMixinTransformer.class); + MixinEnvironment$transformer$setter.invokeExact((IMixinTransformer)null); + + // Create our transformer + MixinTransformer customTransformer = enableCache ? new MixinTransformer.Cache(originalTransformer, cache) : new MixinTransformer(originalTransformer); + + // Restore the original to keep the environment as sane as possible + MixinEnvironment$transformer$setter.invokeExact(originalTransformer); + + // Set our custom transformer so it will be used in future class loads + MethodHandle KnotClassDelegate$mixinTransformer$setter = ReflectionHelper.findSetter(KnotClassDelegate, KnotClassLoader$delegate, "mixinTransformer", FabricMixinTransformerProxy.class); + KnotClassDelegate$mixinTransformer$setter.invokeExact((FabricMixinTransformerProxy)customTransformer); + }catch(Throwable t){ + new RuntimeException("Failed to hook into Knot", t).printStackTrace(); + System.exit(0); + // Unreachable + throw new RuntimeException("Failed to hook into Knot", t); + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/BootstrapTransformer.java b/src/main/java/net/gudenau/minecraft/asm/impl/BootstrapTransformer.java new file mode 100644 index 0000000..f15d619 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/BootstrapTransformer.java @@ -0,0 +1,70 @@ +package net.gudenau.minecraft.asm.impl; + +import java.util.List; +import net.gudenau.minecraft.asm.api.v0.AsmUtils; +import net.gudenau.minecraft.asm.api.v0.Identifier; +import net.gudenau.minecraft.asm.api.v0.Transformer; +import net.gudenau.minecraft.asm.api.v0.TypeCache; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +/** + * A simple transformer to enable some JVM abuse. + * */ +public class BootstrapTransformer implements Transformer{ + // On the off chance that ForceInline is not around, we should not use it. + private static final boolean ENABLED; + + static{ + boolean enable; + try{ + ReflectionHelper.loadClass("jdk.internal.vm.annotation.ForceInline"); + enable = true; + }catch(Throwable ignored){ + enable = false; + } + ENABLED = enable; + } + + private static final AsmUtils ASM_UTILS = AsmUtils.getInstance(); + private static final Type FORCEBOOTLOADER; + private static final Type ASM_FORCEINLINE; + private static final Type JVM_FORCEINLINE; + + static{ + TypeCache cache = ASM_UTILS.getTypeCache(); + FORCEBOOTLOADER = cache.getObjectType("net/gudenau/minecraft/asm/api/v0/annotation/ForceBootloader"); + ASM_FORCEINLINE = cache.getObjectType("net/gudenau/minecraft/asm/api/v0/annotation/ForceInline"); + JVM_FORCEINLINE = cache.getObjectType("jdk/internal/vm/annotation/ForceInline"); + } + + @Override + public Identifier getName(){ + return new Identifier("gud_asm", "bootstrap"); + } + + // Special case, this is always true when called. + @Override + public boolean handlesClass(String name, String transformedName){ + return ENABLED; + } + + @Override + public boolean transform(ClassNode classNode, Flags flags){ + boolean changed = ASM_UTILS.removeAnnotations(classNode, FORCEBOOTLOADER); + + for(MethodNode method : classNode.methods){ + List annotations = ASM_UTILS.getAnnotations(method, ASM_FORCEINLINE); + if(!annotations.isEmpty()){ + for(AnnotationNode annotation : annotations){ + annotation.desc = JVM_FORCEINLINE.getDescriptor(); + changed = true; + } + } + } + + return changed; + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/Configuration.java b/src/main/java/net/gudenau/minecraft/asm/impl/Configuration.java new file mode 100644 index 0000000..4da2d72 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/Configuration.java @@ -0,0 +1,225 @@ +package net.gudenau.minecraft.asm.impl; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import net.fabricmc.loader.api.FabricLoader; + +/** + * Basic config for the mod + */ +public class Configuration{ + /** + * Selects the enable class cache, if available. + * */ + public static final Value ENABLED_CACHE = new ListValue("cache", RegistryImpl.INSTANCE.getCacheNames()); + + /** + * The class dumping mode for debugging. + * */ + public static final Value DUMP = new EnumValue<>("dump", DumpMode.OFF); + + private static final Map> VALUES; + static{ + Map> values = new Object2ObjectOpenHashMap<>(); + values.put(ENABLED_CACHE.getName(), ENABLED_CACHE); + values.put(DUMP.getName(), DUMP); + VALUES = Collections.unmodifiableMap(values); + + Runtime.getRuntime().addShutdownHook(new Thread(()->{ + try{ + save(false); + }catch(IOException e){ + e.printStackTrace(); + } + }, "gudASM Config Saver")); + } + + /** + * The dump mode. + * */ + public enum DumpMode{ + /** + * Don't dump any classes. + * */ + OFF, + /** + * Dump classes we changed. + * */ + ON, + /** + * Dump everything. + * */ + FORCE + } + + /** + * Load the config from disk. + * */ + public static void load() throws IOException{ + Path gudConfig = FabricLoader.getInstance().getConfigDir().resolve("gud"); + if(!Files.exists(gudConfig)){ + Files.createDirectories(gudConfig); + } + + Path configPath = gudConfig.resolve("asm.cfg"); + if(Files.exists(configPath)){ + Map rawConfig = new Object2ObjectOpenHashMap<>(); + try(BufferedReader reader = Files.newBufferedReader(configPath, StandardCharsets.UTF_8)){ + for(String line = reader.readLine(); line != null; line = reader.readLine()){ + if(line.isEmpty()){ + continue; + } + String original = line; + int index = line.indexOf("#"); + if(index != -1){ + line = line.substring(0, index); + } + index = line.indexOf("="); + if(index == -1){ + System.err.printf( + "Invalid line \"%s\" in \"gud/asm.cfg\"", + original + ); + } + + String[] split = line.split("=", 2); + rawConfig.put(split[0], split[1]); + } + } + + Set> valueSet = new HashSet<>(VALUES.values()); + for(Map.Entry entry : rawConfig.entrySet()){ + String key = entry.getKey(); + String value = entry.getValue(); + Value config = VALUES.get(key); + if(config != null){ + config.parse(value); + valueSet.remove(config); + } + } + + if(!valueSet.isEmpty()){ + save(true); + } + }else{ + save(true); + } + } + + /** + * Save the configs. + * + * @param force Skip dirty checks + * */ + private static void save(boolean force) throws IOException{ + Map options = new Object2ObjectOpenHashMap<>(); + List keys = new ArrayList<>(); + + boolean shouldSave = force; + for(Value value : VALUES.values()){ + String key = value.getName(); + keys.add(key); + Object valueValue = value.get(); + options.put(key, valueValue == null ? "" : String.valueOf(valueValue).toLowerCase()); + shouldSave |= value.isDirty(); + value.clean(); + } + + if(!shouldSave){ + return; + } + + keys.sort(String::compareToIgnoreCase); + + Path gudConfig = FabricLoader.getInstance().getConfigDir().resolve("gud"); + if(!Files.exists(gudConfig)){ + Files.createDirectories(gudConfig); + } + + Path configPath = gudConfig.resolve("asm.cfg"); + + try(BufferedWriter writer = Files.newBufferedWriter(configPath, StandardCharsets.UTF_8, StandardOpenOption.CREATE)){ + for(String key : keys){ + writer.write(key); + writer.write('='); + writer.write(options.get(key)); + writer.write('\n'); + } + } + } + + public static class Value{ + private final String name; + private final Function parser; + private T value; + private boolean dirty = false; + + Value(String name, T defaultValue, Function parser){ + this.name = name; + this.value = defaultValue; + this.parser = parser; + } + + void parse(String rawValue){ + T value = parser.apply(rawValue); + if(value != null){ + this.value = value; + } + } + + public String getName(){ + return name; + } + + public T get(){ + return value; + } + + public void set(T value){ + this.value = value; + dirty = true; + } + + boolean isDirty(){ + return dirty; + } + + void clean(){ + dirty = false; + } + } + + private static class ListValue extends Value{ + ListValue(String name, List values){ + super(name, values.isEmpty() ? null : values.get(0), (key)-> + values.contains(key) ? key : null + ); + } + } + + private static class EnumValue> extends Value{ + EnumValue(String name, E defaultValue){ + super(name, defaultValue, (key)->{ + for(E value : defaultValue.getDeclaringClass().getEnumConstants()){ + if(value.name().equalsIgnoreCase(key)){ + return value; + } + } + return null; + }); + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/MixinTransformer.java b/src/main/java/net/gudenau/minecraft/asm/impl/MixinTransformer.java new file mode 100644 index 0000000..bd84a85 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/MixinTransformer.java @@ -0,0 +1,272 @@ +package net.gudenau.minecraft.asm.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import net.fabricmc.loader.api.FabricLoader; +import net.gudenau.minecraft.asm.api.v0.AsmUtils; +import net.gudenau.minecraft.asm.api.v0.ClassCache; +import net.gudenau.minecraft.asm.api.v0.Transformer; +import net.gudenau.minecraft.asm.util.AsmUtilsImpl; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.transformer.FabricMixinTransformerProxy; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; + +/** + * Our custom "mixin" transformer. + * */ +public class MixinTransformer extends FabricMixinTransformerProxy{ + private static final Type ANNOTATION_FORCE_BOOTLOADER = AsmUtils.getInstance().getTypeCache().getObjectType("net/gudenau/minecraft/asm/api/v0/annotation/ForceBootloader"); + + private static final Set BLACKLIST = new HashSet<>(Arrays.asList( + "net.gudenau.minecraft.asm.", + "org.objectweb.asm.", + "com.google.gson.", + "org.lwjgl.", + "it.unimi.dsi.fastutil." + )); + private static final Transformer BOOTSTRAP_TRANSFORMER = new BootstrapTransformer(); + + private static final MethodHandle ClassLoader$defineClass; + static{ + try{ + ClassLoader$defineClass = ReflectionHelper.findStatic( + ClassLoader.class, + "defineClass1", + Class.class, + ClassLoader.class, String.class, byte[].class, int.class, int.class, ProtectionDomain.class, String.class + ); + }catch(ReflectiveOperationException e){ + new RuntimeException("Failed to get ClassLoader.defineClass1", e).printStackTrace(); + System.exit(0); + // Unreachable, makes javac happy + throw new RuntimeException("Failed to get ClassLoader.defineClass1", e); + } + } + + private static ClassLoader classLoader; + + public static void setClassLoader(ClassLoader classLoader){ + MixinTransformer.classLoader = classLoader; + } + + private final IMixinTransformer parent; + private final List transformers; + private final List earlyTransformers; + + private final boolean forceDump = Configuration.DUMP.get() == Configuration.DumpMode.FORCE; + private final boolean dump = Configuration.DUMP.get() == Configuration.DumpMode.ON || forceDump; + + MixinTransformer(IMixinTransformer parent){ + this.parent = parent; + transformers = RegistryImpl.INSTANCE.getTransformers(); + earlyTransformers = RegistryImpl.INSTANCE.getEarlyTransformers(); + } + + @Override + public byte[] transformClassBytes(String name, String transformedName, byte[] basicClass){ + for(String prefix : BLACKLIST){ + if(name.startsWith(prefix)){ + if(name.endsWith("ForceBootloaderTest")){ + break; + } + if(forceDump){ + dump(name, basicClass); + } + return bootstrap(cache(basicClass, ()->parent.transformClassBytes(name, transformedName, basicClass))); + } + } + return cache(basicClass, ()->{ + if(basicClass == null){ + return null; + } + + boolean shouldBootstrap = shouldBootstrap(basicClass); + AtomicBoolean modified = new AtomicBoolean(forceDump); + + byte[] bytecode = basicClass; + if(!earlyTransformers.isEmpty()){ + bytecode = transform(name, transformedName, bytecode, earlyTransformers, modified); + } + + bytecode = parent.transformClassBytes(name, transformedName, bytecode); + + //FIXME, this is stupid + List transformers = this.transformers; + if(shouldBootstrap){ + List newList = new ArrayList<>(); + Collections.copy(newList, this.transformers); + newList.add(BOOTSTRAP_TRANSFORMER); + transformers = newList; + } + if(!transformers.isEmpty()){ + bytecode = transform(name, transformedName, bytecode, transformers, modified); + } + + if(dump && modified.get()){ + dump(name, bytecode); + } + + return bootstrap(bytecode, shouldBootstrap); + }); + } + + private static final ExecutorService DUMP_SERVICE = Executors.newFixedThreadPool(1); + static{ + Runtime.getRuntime().addShutdownHook(new Thread(()->{ + DUMP_SERVICE.shutdown(); + try{ + DUMP_SERVICE.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + }catch(InterruptedException ignored){} + }, "gudASM Dumper Service Cleanup")); + } + private void dump(String name, byte[] bytecode){ + DUMP_SERVICE.submit(()->{ + Path path = FabricLoader.getInstance().getGameDir().resolve("gudASMDump"); + String[] split = name.split("\\."); + for(int i = 0; i < split.length - 1; i++){ + String s = split[i]; + path = path.resolve(s); + } + if(!Files.exists(path)){ + try{ + Files.createDirectories(path); + }catch(IOException ignored){} + } + path = path.resolve(split[split.length - 1] + ".class"); + try(OutputStream stream = Files.newOutputStream(path, StandardOpenOption.CREATE)){ + stream.write(bytecode); + }catch(IOException ignored){} + }); + } + + private byte[] transform(String name, String transformedName, byte[] bytecode, List transformers, AtomicBoolean parentModifier){ + List validTransformers = new ArrayList<>(); + for(Transformer transformer : transformers){ + if(transformer.handlesClass(name, transformedName)){ + validTransformers.add(transformer); + } + } + + if(validTransformers.isEmpty()){ + return bytecode; + } + + ClassNode classNode = new ClassNode(); + new ClassReader(bytecode).accept(classNode, 0); + boolean modified = false; + TransformerFlagsImpl flags = new TransformerFlagsImpl(); + for(Transformer transformer : validTransformers){ + modified |= transformer.transform(classNode, flags); + } + if(!modified){ + return bytecode; + } + + ClassWriter writer = new ClassWriter(flags.getClassWriterFlags()){ + // Fixes an issue with stack calculations + @Override + protected ClassLoader getClassLoader(){ + return classLoader; + } + }; + classNode.accept(writer); + parentModifier.set(true); + return writer.toByteArray(); + } + + byte[] cache(byte[] original, Supplier transformed){ + return transformed.get(); + } + + private boolean shouldBootstrap(byte[] bytecode){ + if(bytecode == null){ + return false; + } + + ClassNode classNode = new ClassNode(); + new ClassReader(bytecode).accept(classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + return AsmUtilsImpl.INSTANCE.hasAnnotation(classNode, ANNOTATION_FORCE_BOOTLOADER); + } + + private byte[] bootstrap(byte[] bytecode){ + return bootstrap(bytecode, shouldBootstrap(bytecode)); + } + + private byte[] bootstrap(byte[] bytecode, boolean shouldBootstrap){ + if(bytecode == null){ + return null; + } + + if(shouldBootstrap){ + /* + static native Class defineClass1(ClassLoader loader, String name, byte[] b, int off, int len, + ProtectionDomain pd, String source); + */ + try{ + ClassLoader$defineClass.invoke( + (ClassLoader)null, // AKA bootstrap ClassLoader + (String)null, // Let the JVM figure it out + bytecode, + 0, + bytecode.length, + (ProtectionDomain)null, + (String)null + ); + }catch(Throwable throwable){ + new RuntimeException("Failed to force a class into the bootstrap ClassLoader", throwable).printStackTrace(); + System.exit(0); + } + + return null; + }else{ + return bytecode; + } + } + + static class Cache extends MixinTransformer{ + private final ClassCache cache; + + Cache(IMixinTransformer parent, ClassCache cache){ + super(parent); + this.cache = cache; + } + + @Override + byte[] cache(byte[] original, Supplier transformer){ + if(original == null){ + return transformer.get(); + } + + Optional result = cache.getEntry(original); + if(result.isPresent()){ + return result.get(); + }else{ + byte[] transformed = transformer.get(); + if(transformed != null){ + cache.putEntry(original, transformed); + } + return transformed; + } + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/ReflectionHelper.java b/src/main/java/net/gudenau/minecraft/asm/impl/ReflectionHelper.java new file mode 100644 index 0000000..b0926b9 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/ReflectionHelper.java @@ -0,0 +1,200 @@ +package net.gudenau.minecraft.asm.impl; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; + +/** + * Please be good and never touch this outside gudASM. + * */ +@SuppressWarnings({"SameParameterValue", "unused", "RedundantSuppression"}) +public class ReflectionHelper{ + private static final long AccessibleObject$override = findOverride(); + + private static final MethodHandles.Lookup IMPL_LOOKUP = forceGetField( + MethodHandles.Lookup.class, + null, + Modifier.STATIC | Modifier.FINAL, + MethodHandles.Lookup.class + ); + + @SuppressWarnings("unchecked") + private static T forceGetField(Class owner, O instance, int mods, Class type){ + for(Field field : owner.getDeclaredFields()){ + if( + field.getModifiers() == mods && + field.getType() == type + ){ + try{ + forceSetAccessible(field, true); + return (T)field.get(instance); + }catch(ReflectiveOperationException ignored){} + } + } + throw new RuntimeException(String.format( + "Failed to get field from %s of type %s", + owner.getName(), + type.getName() + )); + } + + @SuppressWarnings("deprecation") + private static long findOverride(){ + AccessibleObject object = UnsafeHelper.allocateInstance(AccessibleObject.class); + for(long cookie = 0; cookie < 64; cookie += 4){ + int original = UnsafeHelper.getInt(object, cookie); + object.setAccessible(true); + if(original != UnsafeHelper.getInt(object, cookie)){ + UnsafeHelper.putInt(object, cookie, original); + if(!object.isAccessible()){ + return cookie; + } + } + object.setAccessible(false); + } + return -1; + } + + private static void forceSetAccessible(AccessibleObject object, boolean accessible){ + UnsafeHelper.putInt(object, AccessibleObject$override, accessible ? 1 : 0); + } + + public static MethodHandle findGetter(Class owner, String name, Class type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findGetter(owner, name, type); + } + + public static MethodHandle findGetter(Class owner, O instance, String name, Class type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findGetter(owner, name, type).bindTo(instance); + } + + public static MethodHandle findStaticGetter(Class owner, String name, Class type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findStaticGetter(owner, name, type); + } + + public static MethodHandle findSetter(Class owner, String name, Class type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findSetter(owner, name, type); + } + + public static MethodHandle findSetter(Class owner, O instance, String name, Class type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findSetter(owner, name, type).bindTo(instance); + } + + public static MethodHandle findStaticSetter(Class owner, String name, Class type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findStaticSetter(owner, name, type); + } + + public static Class loadClass(String name) throws ReflectiveOperationException{ + return loadClass(ReflectionHelper.class.getClassLoader(), name); + } + + @SuppressWarnings("unchecked") + public static Class loadClass(ClassLoader loader, String name) throws ReflectiveOperationException{ + return (Class)loader.loadClass(name); + } + + public static MethodHandle findVirtual(Class owner, O instance, String name, MethodType type) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findVirtual(owner, name, type).bindTo(instance); + } + + public static MethodHandle findStatic(Class owner, String name, Class returnType, Class... params) throws ReflectiveOperationException{ + return IMPL_LOOKUP.findStatic( + owner, + name, + MethodType.methodType(returnType, params) + ); + } + + private static final class UnsafeHelper{ + private static final Class Unsafe = loadUnsafe(); + private static final Object theUnsafe = getUnsafe(); + + private static Class loadUnsafe(){ + try{ + return loadClass("sun.misc.Unsafe"); + }catch(ReflectiveOperationException e2){ + System.err.println("Failed to load Unsafe class"); + e2.printStackTrace(); + System.exit(0); + return null; + } + } + + private static Object getUnsafe(){ + final int modifiers = Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL; + for(Field field : Unsafe.getDeclaredFields()){ + if(field.getModifiers() == modifiers && field.getType() == Unsafe){ + try{ + field.setAccessible(true); + Object unsafe = field.get(null); + if(unsafe != null){ + return unsafe; + } + }catch(ReflectiveOperationException ignored){} + } + } + System.err.println("Failed to find Unsafe instance"); + System.exit(0); + return null; + } + + private static MethodHandle findMethod(String name, Class... arguments){ + for(Method method : Unsafe.getDeclaredMethods()){ + if( + method.getName().equals(name) && + Arrays.equals(arguments, method.getParameterTypes()) + ){ + try{ + method.setAccessible(true); + MethodHandle handle = MethodHandles.lookup().unreflect(method); + return handle.bindTo(theUnsafe); + }catch(ReflectiveOperationException ignored){} + } + } + + System.err.println("Failed to find Unsafe." + name); + System.exit(0); + return null; + } + + private static final MethodHandle allocateInstance = findMethod("allocateInstance", Class.class); + @SuppressWarnings("unchecked") + static T allocateInstance(Class type){ + try{ + return (T)((Object)allocateInstance.invokeExact(type)); + }catch(Throwable throwable){ + System.err.println("Failed to allocate " + type.getName()); + throwable.printStackTrace(); + System.exit(0); + return null; + } + } + + private static final MethodHandle putInt = findMethod("putInt", Object.class, long.class, int.class); + static void putInt(Object o, long offset, int x){ + try{ + putInt.invokeExact(o, offset, x); + }catch(Throwable throwable){ + System.err.println("Failed to put int"); + throwable.printStackTrace(); + System.exit(0); + } + } + + private static final MethodHandle getInt = findMethod("getInt", Object.class, long.class); + static int getInt(Object o, long offset){ + try{ + return (int)getInt.invokeExact(o, offset); + }catch(Throwable throwable){ + System.err.println("Failed to get int"); + throwable.printStackTrace(); + System.exit(0); + return -1; + } + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/RegistryImpl.java b/src/main/java/net/gudenau/minecraft/asm/impl/RegistryImpl.java new file mode 100644 index 0000000..a744c28 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/RegistryImpl.java @@ -0,0 +1,76 @@ +package net.gudenau.minecraft.asm.impl; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import net.gudenau.minecraft.asm.api.v0.ClassCache; +import net.gudenau.minecraft.asm.api.v0.AsmRegistry; +import net.gudenau.minecraft.asm.api.v0.Identifier; +import net.gudenau.minecraft.asm.api.v0.Transformer; + +// Basic registry implementation +public class RegistryImpl implements AsmRegistry{ + public static final RegistryImpl INSTANCE = new RegistryImpl(); + + private final List earlyTransformers = new LinkedList<>(); + private final List transformers = new LinkedList<>(); + private final List classCaches = new LinkedList<>(); + + private RegistryImpl(){} + + @Override + public void registerEarlyTransformer(Transformer transformer){ + earlyTransformers.add(transformer); + } + + @Override + public void registerTransformer(Transformer transformer){ + transformers.add(transformer); + } + + @Override + public void registerClassCache(ClassCache cache){ + classCaches.add(cache); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private Optional cache; + + @SuppressWarnings("OptionalAssignedToNull") + public Optional getCache(){ + if(cache == null){ + if(classCaches.isEmpty()){ + cache = Optional.empty(); + }else{ + String enabled = Configuration.ENABLED_CACHE.get(); + if(enabled != null){ + Optional existing = classCaches.stream() + .filter((c)->c.getName().toString().equals(enabled)) + .findAny(); + if(existing.isPresent()){ + cache = existing; + return existing; + } + } + + ClassCache newCache = classCaches.get(0); + Configuration.ENABLED_CACHE.set(newCache.getName().toString()); + cache = Optional.of(newCache); + } + } + return cache; + } + + public List getCacheNames(){ + return classCaches.stream().map(ClassCache::getName).map(Identifier::toString).collect(Collectors.toList()); + } + + public List getTransformers(){ + return transformers; + } + + public List getEarlyTransformers(){ + return earlyTransformers; + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/impl/TransformerFlagsImpl.java b/src/main/java/net/gudenau/minecraft/asm/impl/TransformerFlagsImpl.java new file mode 100644 index 0000000..a411c4b --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/impl/TransformerFlagsImpl.java @@ -0,0 +1,22 @@ +package net.gudenau.minecraft.asm.impl; + +import net.gudenau.minecraft.asm.api.v0.Transformer; +import org.objectweb.asm.ClassWriter; + +public class TransformerFlagsImpl implements Transformer.Flags{ + private boolean computeMaxes = false; + private boolean computeFrames = false; + + public void requestMaxes(){ + computeMaxes = true; + } + + public void requestFrames(){ + computeFrames = true; + } + + public int getClassWriterFlags(){ + return (computeFrames ? ClassWriter.COMPUTE_FRAMES : 0) | + (computeMaxes ? ClassWriter.COMPUTE_MAXS : 0); + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/mixin/Plugin.java b/src/main/java/net/gudenau/minecraft/asm/mixin/Plugin.java new file mode 100644 index 0000000..2e967ae --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/mixin/Plugin.java @@ -0,0 +1,44 @@ +package net.gudenau.minecraft.asm.mixin; + +import java.util.List; +import java.util.Set; +import net.gudenau.minecraft.asm.impl.Bootstrap; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +/** + * The part that kicks off the entire mess. + * */ +public class Plugin implements IMixinConfigPlugin{ + static{ + Bootstrap.setup(); + } + + @Override + public void onLoad(String mixinPackage){} + + @Override + public String getRefMapperConfig(){ + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName){ + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets){} + + @Override + public List getMixins(){ + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo){} + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo){} +} diff --git a/src/main/java/net/gudenau/minecraft/asm/util/AsmUtilsImpl.java b/src/main/java/net/gudenau/minecraft/asm/util/AsmUtilsImpl.java new file mode 100644 index 0000000..4f79107 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/util/AsmUtilsImpl.java @@ -0,0 +1,546 @@ +package net.gudenau.minecraft.asm.util; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import net.gudenau.minecraft.asm.api.v0.AsmUtils; +import net.gudenau.minecraft.asm.api.v0.TypeCache; +import net.gudenau.minecraft.asm.api.v0.functional.BooleanFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; + +import static org.objectweb.asm.Opcodes.*; + +public class AsmUtilsImpl implements AsmUtils{ + public static final AsmUtils INSTANCE = new AsmUtilsImpl(); + private static final IntSet METHOD_OPCODES = new IntOpenHashSet(new int[]{ + INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC, INVOKEINTERFACE + }); + private static final IntSet RETURN_OPCODES = new IntOpenHashSet(new int[]{ + IRETURN, LRETURN, FRETURN, DRETURN, ARETURN, RETURN + }); + private final TypeCache typeCache = new TypeCacheImpl(); + + private AsmUtilsImpl(){} + + @Override + public @NotNull TypeCache getTypeCache(){ + return typeCache; + } + + @Override + public List getAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type){ + String desc = type.getDescriptor(); + List annotations = new ArrayList<>(); + if(visibleAnnotations != null && !visibleAnnotations.isEmpty()){ + for(AnnotationNode annotation : visibleAnnotations){ + if(desc.equals(annotation.desc)){ + annotations.add(annotation); + } + } + } + if(invisibleAnnotations != null && !invisibleAnnotations.isEmpty()){ + for(AnnotationNode annotation : invisibleAnnotations){ + if(desc.equals(annotation.desc)){ + annotations.add(annotation); + } + } + } + return annotations; + } + + @Override + public Optional getAnnotation(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type){ + String desc = type.getDescriptor(); + if(visibleAnnotations != null && !visibleAnnotations.isEmpty()){ + for(AnnotationNode annotation : visibleAnnotations){ + if(desc.equals(annotation.desc)){ + return Optional.of(annotation); + } + } + } + if(invisibleAnnotations != null && !invisibleAnnotations.isEmpty()){ + for(AnnotationNode annotation : invisibleAnnotations){ + if(desc.equals(annotation.desc)){ + return Optional.of(annotation); + } + } + } + return Optional.empty(); + } + + @Override + public void addAnnotations(@NotNull MethodNode method, boolean visible, @NotNull Collection annotations){ + List nodes = visible ? method.visibleAnnotations : method.invisibleAnnotations; + if(nodes == null){ + nodes = new ArrayList<>(); + if(visible){ + method.visibleAnnotations = nodes; + }else{ + method.invisibleAnnotations = nodes; + } + } + nodes.addAll(annotations); + } + + @Override + public void addAnnotations(@NotNull ClassNode owner, boolean visible, @NotNull Collection annotations){ + List nodes = visible ? owner.visibleAnnotations : owner.invisibleAnnotations; + if(nodes == null){ + nodes = new ArrayList<>(); + if(visible){ + owner.visibleAnnotations = nodes; + }else{ + owner.invisibleAnnotations = nodes; + } + } + nodes.addAll(annotations); + } + + @Override + public boolean removeAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Collection annotations){ + boolean change = false; + if(visibleAnnotations != null && !visibleAnnotations.isEmpty()){ + change |= visibleAnnotations.removeAll(annotations); + } + if(invisibleAnnotations != null && !invisibleAnnotations.isEmpty()){ + change |= invisibleAnnotations.removeAll(annotations); + } + return change; + } + + @Override + public boolean removeAnnotations(@Nullable List visibleAnnotations, @Nullable List invisibleAnnotations, @NotNull Type type){ + //TODO make this less stupid + return removeAnnotations(visibleAnnotations, invisibleAnnotations, getAnnotations(visibleAnnotations, invisibleAnnotations, type)); + } + + @SuppressWarnings("unchecked") + @Override + public @NotNull List findMatchingNodes(@NotNull InsnList instructions, @NotNull BooleanFunction checker){ + List result = new ArrayList<>(); + for(AbstractInsnNode instruction : instructions){ + if(checker.apply(instruction)){ + result.add((T)instruction); + } + } + return result; + } + + // This is nasty, but should hopefully be somewhat faster than doing more checks in the loop + @Override + public @NotNull List findMethodCalls(@NotNull InsnList instructions, int opcode, @Nullable String owner, @Nullable String name, @Nullable String description){ + BooleanFunction checker; + if(opcode == -1){ + if(owner == null){ + if(name == null){ + if(description == null){ + checker = (node)->METHOD_OPCODES.contains(node.getOpcode()); + }else{ + checker = (node)-> + METHOD_OPCODES.contains(node.getOpcode()) && + description.equals(((MethodInsnNode)node).desc); + } + }else{ + if(description == null){ + checker = (node)-> + METHOD_OPCODES.contains(node.getOpcode()) && + name.equals(((MethodInsnNode)node).name); + }else{ + checker = (node)->{ + if(METHOD_OPCODES.contains(node.getOpcode())){ + MethodInsnNode method = (MethodInsnNode)node; + return name.equals(method.name) && + description.equals(method.desc); + }else{ + return false; + } + }; + } + } + }else{ + if(name == null){ + if(description == null){ + checker = (node)-> + METHOD_OPCODES.contains(node.getOpcode()) && + owner.equals(((MethodInsnNode)node).owner); + }else{ + checker = (node)->{ + if(METHOD_OPCODES.contains(node.getOpcode())){ + MethodInsnNode method = (MethodInsnNode)node; + return description.equals(method.desc) && + owner.equals(method.owner); + }else{ + return false; + } + }; + } + }else{ + if(description == null){ + checker = (node)->{ + if(METHOD_OPCODES.contains(node.getOpcode())){ + MethodInsnNode method = (MethodInsnNode)node; + return name.equals(method.name) && + owner.equals(method.owner); + }else{ + return false; + } + }; + }else{ + checker = (node)->{ + if(METHOD_OPCODES.contains(node.getOpcode())){ + MethodInsnNode method = (MethodInsnNode)node; + return name.equals(method.name) && + owner.equals(method.owner) && + description.equals(method.desc); + }else{ + return false; + } + }; + } + } + } + }else{ + if(owner == null){ + if(name == null){ + if(description == null){ + checker = (node)->opcode == node.getOpcode(); + }else{ + checker = (node)-> + opcode == node.getOpcode() && + description.equals(((MethodInsnNode)node).desc); + } + }else{ + if(description == null){ + checker = (node)-> + opcode == node.getOpcode() && + name.equals(((MethodInsnNode)node).name); + }else{ + checker = (node)->{ + if(opcode == node.getOpcode()){ + MethodInsnNode method = (MethodInsnNode)node; + return name.equals(method.name) && + description.equals(method.desc); + }else{ + return false; + } + }; + } + } + }else{ + if(name == null){ + if(description == null){ + checker = (node)-> + opcode == node.getOpcode() && + owner.equals(((MethodInsnNode)node).owner); + }else{ + checker = (node)->{ + if(opcode == node.getOpcode()){ + MethodInsnNode method = (MethodInsnNode)node; + return description.equals(method.desc) && + owner.equals(method.owner); + }else{ + return false; + } + }; + } + }else{ + if(description == null){ + checker = (node)->{ + if(opcode == node.getOpcode()){ + MethodInsnNode method = (MethodInsnNode)node; + return name.equals(method.name) && + owner.equals(method.owner); + }else{ + return false; + } + }; + }else{ + checker = (node)->{ + if(opcode == node.getOpcode()){ + MethodInsnNode method = (MethodInsnNode)node; + return name.equals(method.name) && + owner.equals(method.owner) && + description.equals(method.desc); + }else{ + return false; + } + }; + } + } + } + } + return findMatchingNodes(instructions, checker); + } + + @Override + public @NotNull List findSurroundingNodes(@NotNull AbstractInsnNode node, int leading, int trailing){ + List nodes = new ArrayList<>(); + AbstractInsnNode current = node.getPrevious(); + if(leading > 0){ + for(int i = 0; i < leading && current != null; i++){ + nodes.add(current); + current = current.getPrevious(); + } + if(!nodes.isEmpty()){ + Collections.reverse(nodes); + } + } + nodes.add(node); + current = node.getNext(); + if(trailing > 0){ + for(int i = 0; i < trailing && current != null; i++){ + nodes.add(current); + current = current.getNext(); + } + } + return nodes; + } + + @Override + public @NotNull List findReturns(@NotNull InsnList instructions){ + return findMatchingNodes(instructions, (node)->RETURN_OPCODES.contains(node.getOpcode())); + } + + @Override + public @Nullable List findInRange(AbstractInsnNode start, AbstractInsnNode end){ + List nodes = new ArrayList<>(); + AbstractInsnNode current = start.getNext(); + while(current != null && current != end){ + nodes.add(current); + current = current.getNext(); + } + return current == null ? null : nodes; + } + + @Override + public int getOpcodeFromHandleTag(int tag){ + switch(tag){ + case H_GETFIELD: return GETFIELD; + case H_GETSTATIC: return GETSTATIC; + case H_PUTFIELD: return PUTFIELD; + case H_PUTSTATIC: return PUTSTATIC; + case H_INVOKEVIRTUAL: return INVOKEVIRTUAL; + case H_INVOKESTATIC: return INVOKESTATIC; + case H_INVOKESPECIAL: return INVOKESPECIAL; + case H_NEWINVOKESPECIAL: return NEW; + case H_INVOKEINTERFACE: return INVOKEINTERFACE; + default: return -1; + } + } + + @Override + public int getHandleTagFromOpcode(int opcode){ + switch(opcode){ + case GETFIELD: return H_GETFIELD; + case GETSTATIC: return H_GETSTATIC; + case PUTFIELD: return H_PUTFIELD; + case PUTSTATIC: return H_PUTSTATIC; + case INVOKEVIRTUAL: return H_INVOKEVIRTUAL; + case INVOKESTATIC: return H_INVOKESTATIC; + case INVOKESPECIAL: return H_INVOKESPECIAL; + case NEW: return H_NEWINVOKESPECIAL; + case INVOKEINTERFACE: return H_INVOKEINTERFACE; + default: return -1; + } + } + + @Override + public @NotNull InsnList createExceptionList(@NotNull Type type, @Nullable String message){ + InsnList instructions = new InsnList(); + String name = type.getInternalName(); + instructions.add(new TypeInsnNode(NEW, name)); + instructions.add(new InsnNode(DUP)); + if(message == null){ + instructions.add(new MethodInsnNode(INVOKESPECIAL, name, "", "()V", false)); + }else{ + instructions.add(new LdcInsnNode(message)); + instructions.add(new MethodInsnNode(INVOKESPECIAL, name, "", "(Ljava/lang/String;)V", false)); + } + instructions.add(new InsnNode(ATHROW)); + return instructions; + } + + @Override + public @NotNull Optional findMethod(@NotNull ClassNode owner, @NotNull String name, @NotNull String desc){ + for(MethodNode method : owner.methods){ + if( + name.equals(method.name) && + desc.equals(method.desc) + ){ + return Optional.of(method); + } + } + return Optional.empty(); + } + + @Override + public @NotNull String getOpcodeName(int opcode){ + switch(opcode){ + case NOP: return "NOP"; + case ACONST_NULL: return "ACONST_NULL"; + case ICONST_M1: return "ICONST_M1"; + case ICONST_0: return "ICONST_0"; + case ICONST_1: return "ICONST_1"; + case ICONST_2: return "ICONST_2"; + case ICONST_3: return "ICONST_3"; + case ICONST_4: return "ICONST_4"; + case ICONST_5: return "ICONST_5"; + case LCONST_0: return "LCONST_0"; + case LCONST_1: return "LCONST_1"; + case FCONST_0: return "FCONST_0"; + case FCONST_1: return "FCONST_1"; + case FCONST_2: return "FCONST_2:"; + case DCONST_0: return "DCONST_0"; + case DCONST_1: return "DCONST_1"; + case BIPUSH: return "BIPUSH"; + case SIPUSH: return "SIPUSH"; + case LDC: return "LDC"; + case ILOAD: return "ILOAD"; + case LLOAD: return "LLOAD"; + case FLOAD: return "FLOAD"; + case DLOAD: return "DLOAD"; + case ALOAD: return "ALOAD"; + case IALOAD: return "IALOAD"; + case LALOAD: return "LALOAD"; + case FALOAD: return "FALOAD"; + case DALOAD: return "DALOAD"; + case AALOAD: return "AALOAD"; + case BALOAD: return "BALOAD"; + case CALOAD: return "CALOAD"; + case SALOAD: return "SALOAD"; + case ISTORE: return "ISTORE"; + case LSTORE: return "LSTORE"; + case FSTORE: return "FSTORE"; + case DSTORE: return "DSTORE"; + case ASTORE: return "ASTORE"; + case IASTORE: return "IASTORE"; + case LASTORE: return "LASTORE"; + case FASTORE: return "FASTORE"; + case DASTORE: return "DASTORE"; + case AASTORE: return "AASTORE"; + case BASTORE: return "BASTORE"; + case CASTORE: return "CASTORE"; + case SASTORE: return "SASTORE"; + case POP: return "POP"; + case POP2: return "POP2"; + case DUP: return "DUP"; + case DUP_X1: return "DUP_X1"; + case DUP_X2: return "DUP_X2"; + case DUP2: return "DUP2:"; + case DUP2_X1: return "DUP2_X1"; + case DUP2_X2: return "DUP2_X2"; + case SWAP: return "SWAP"; + case IADD: return "IADD"; + case LADD: return "LADD:"; + case FADD: return "FADD"; + case DADD: return "DADD"; + case ISUB: return "ISUB"; + case LSUB: return "LSUB"; + case FSUB: return "FSUB"; + case DSUB: return "DSUB"; + case IMUL: return "IMUL"; + case LMUL: return "LMUL"; + case FMUL: return "FMUL"; + case DMUL: return "DMUL"; + case IDIV: return "IDIV"; + case LDIV: return "LDIV"; + case FDIV: return "FDIV"; + case DDIV: return "DDIV"; + case IREM: return "IREM"; + case LREM: return "LREM"; + case FREM: return "FREM"; + case DREM: return "DREM"; + case INEG: return "INEG"; + case LNEG: return "LNEG"; + case FNEG: return "FNEG"; + case DNEG: return "DNEG"; + case ISHL: return "ISHL"; + case LSHL: return "LSHL"; + case ISHR: return "ISHR"; + case LSHR: return "LSHR"; + case IUSHR: return "IUSHR"; + case LUSHR: return "LUSHR"; + case IAND: return "IAND"; + case LAND: return "LAND"; + case IOR: return "IOR"; + case LOR: return "LOR"; + case IXOR: return "IXOR"; + case LXOR: return "LXOR"; + case IINC: return "IINC:"; + case I2L: return "I2L"; + case I2F: return "I2F:"; + case I2D: return "I2D"; + case L2I: return "L2I"; + case L2F: return "L2F"; + case L2D: return "L2D"; + case F2I: return "F2I"; + case F2L: return "F2L"; + case F2D: return "F2D"; + case D2I: return "D2I"; + case D2L: return "D2L"; + case D2F: return "D2F"; + case I2B: return "I2B"; + case I2C: return "I2C"; + case I2S: return "I2S"; + case LCMP: return "LCMP:"; + case FCMPL: return "FCMPL"; + case FCMPG: return "FCMPG"; + case DCMPL: return "DCMPL"; + case DCMPG: return "DCMPG"; + case IFEQ: return "IFEQ:"; + case IFNE: return "IFNE"; + case IFLT: return "IFLT"; + case IFGE: return "IFGE"; + case IFGT: return "IFGT"; + case IFLE: return "IFLE"; + case IF_ICMPEQ: return "IF_ICMPEQ"; + case IF_ICMPNE: return "IF_ICMPNE"; + case IF_ICMPLT: return "IF_ICMPLT"; + case IF_ICMPGE: return "IF_ICMPGE"; + case IF_ICMPGT: return "IF_ICMPGT"; + case IF_ICMPLE: return "IF_ICMPLE"; + case IF_ACMPEQ: return "IF_ACMPEQ"; + case IF_ACMPNE: return "IF_ACMPNE"; + case GOTO: return "GOTO"; + case JSR: return "JSR"; + case RET: return "RET"; + case TABLESWITCH: return "TABLESWITCH"; + case LOOKUPSWITCH: return "LOOKUPSWITCH"; + case IRETURN: return "IRETURN"; + case LRETURN: return "LRETURN"; + case FRETURN: return "FRETURN"; + case DRETURN: return "DRETURN"; + case ARETURN: return "ARETURN"; + case RETURN: return "RETURN"; + case GETSTATIC: return "GETSTATIC"; + case PUTSTATIC: return "PUTSTATIC"; + case GETFIELD: return "GETFIELD"; + case PUTFIELD: return "PUTFIELD"; + case INVOKEVIRTUAL: return "INVOKEVIRTUAL"; + case INVOKESPECIAL: return "INVOKESPECIAL"; + case INVOKESTATIC: return "INVOKESTATIC"; + case INVOKEINTERFACE: return "INVOKEINTERFACE"; + case INVOKEDYNAMIC: return "INVOKEDYNAMIC"; + case NEW: return "NEW:"; + case NEWARRAY: return "NEWARRAY"; + case ANEWARRAY: return "ANEWARRAY"; + case ARRAYLENGTH: return "ARRAYLENGTH"; + case ATHROW: return "ATHROW"; + case CHECKCAST: return "CHECKCAST"; + case INSTANCEOF: return "INSTANCEOF"; + case MONITORENTER: return "MONITORENTER"; + case MONITOREXIT: return "MONITOREXIT"; + case MULTIANEWARRAY: return "MULTIANEWARRAY"; + case IFNULL: return "IFNULL"; + case IFNONNULL: return "IFNONNULL"; + default: return "UNKNOWN"; + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/util/FileUtils.java b/src/main/java/net/gudenau/minecraft/asm/util/FileUtils.java new file mode 100644 index 0000000..062b257 --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/util/FileUtils.java @@ -0,0 +1,37 @@ +package net.gudenau.minecraft.asm.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.Queue; + +public class FileUtils{ + public static void delete(Path root) throws IOException{ + Queue dirs = new LinkedList<>(); + Queue files = new LinkedList<>(); + Queue remaining = new LinkedList<>(); + remaining.add(root); + while(!remaining.isEmpty()){ + Path current = remaining.poll(); + if(Files.isDirectory(current)){ + Files.list(current).forEach((p)->{ + if(Files.isDirectory(p)){ + remaining.add(p); + }else{ + files.add(p); + } + }); + dirs.add(current); + }else{ + files.add(current); + } + } + for(Path file : files){ + Files.deleteIfExists(file); + } + for(Path dir : dirs){ + Files.deleteIfExists(dir); + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/util/Locker.java b/src/main/java/net/gudenau/minecraft/asm/util/Locker.java new file mode 100644 index 0000000..5ecbb8a --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/util/Locker.java @@ -0,0 +1,67 @@ +package net.gudenau.minecraft.asm.util; + +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; + +public class Locker{ + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private final Lock writeLock = lock.writeLock(); + + public V computeIfAbsent(Map map, K key, Function factory){ + V value = readLock(()->map.get(key)); + if(value == null){ + return writeLock(()->map.computeIfAbsent(key, factory)); + }else{ + return value; + } + } + + public V putIfAbsent(Map map, K key, V value){ + V existing = readLock(()->map.get(key)); + if(existing == null){ + return writeLock(()->map.putIfAbsent(key, value)); + } + return existing; + } + + public T readLock(Supplier action){ + readLock.lock(); + try{ + return action.get(); + }finally{ + readLock.unlock(); + } + } + + public T writeLock(Supplier action){ + writeLock.lock(); + try{ + return action.get(); + }finally{ + writeLock.unlock(); + } + } + + public void readLock(Runnable action){ + readLock.lock(); + try{ + action.run(); + }finally{ + readLock.unlock(); + } + } + + public void writeLock(Runnable action){ + writeLock.lock(); + try{ + action.run(); + }finally{ + writeLock.unlock(); + } + } +} diff --git a/src/main/java/net/gudenau/minecraft/asm/util/TypeCacheImpl.java b/src/main/java/net/gudenau/minecraft/asm/util/TypeCacheImpl.java new file mode 100644 index 0000000..5e0c17f --- /dev/null +++ b/src/main/java/net/gudenau/minecraft/asm/util/TypeCacheImpl.java @@ -0,0 +1,54 @@ +package net.gudenau.minecraft.asm.util; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.function.Function; +import net.gudenau.minecraft.asm.api.v0.Pair; +import net.gudenau.minecraft.asm.api.v0.TypeCache; +import org.objectweb.asm.Type; + +public class TypeCacheImpl implements TypeCache{ + private final Locker stringLocker = new Locker(); + private final Map> stringCache = new Object2ObjectOpenHashMap<>(); + + private final Locker methodLocker = new Locker(); + private final Map, WeakReference> methodCache = new Object2ObjectOpenHashMap<>(); + + private Type getString(String name, Function factory){ + WeakReference ref = stringLocker.computeIfAbsent(stringCache, name, (n)->new WeakReference<>(factory.apply(n))); + Type type = ref.get(); + if(type == null){ + type = factory.apply(name); + stringLocker.putIfAbsent(stringCache, name, new WeakReference<>(type)); + } + return type; + } + + @Override + public Type getType(String descriptor){ + return getString(descriptor, Type::getType); + } + + @Override + public Type getObjectType(String name){ + return getString(name, Type::getObjectType); + } + + @Override + public Type getMethodType(String descriptor){ + return getString(descriptor, Type::getMethodType); + } + + @Override + public Type getMethodType(Type returnType, Type... arguments){ + Pair key = Pair.of(returnType, arguments); + WeakReference ref = methodLocker.computeIfAbsent(methodCache, key, (n)->new WeakReference<>(Type.getMethodType(returnType, arguments))); + Type type = ref.get(); + if(type == null){ + type = Type.getMethodType(returnType, arguments); + methodLocker.putIfAbsent(methodCache, key, new WeakReference<>(type)); + } + return type; + } +} diff --git a/src/main/resources/assets/gud_asm/icon.png b/src/main/resources/assets/gud_asm/icon.png new file mode 100644 index 0000000..047b91f Binary files /dev/null and b/src/main/resources/assets/gud_asm/icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..d7d2035 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "id": "gud_asm", + "version": "${version}", + + "name": "gudASM", + "description": "Lets you do weird ASM hacks with relitive ease.", + "authors": [ + "gudenau" + ], + "contact": { + "homepage": "", + "sources": "" + }, + + "license": "", + "icon": "assets/gud_asm/icon.png", + + "environment": "*", + "entrypoints": { + "main": [ + "net.gudenau.minecraft.asm.GudAsm" + ] + }, + "mixins": [ + "gud_asm.mixins.json" + ], + + "depends": { + "fabricloader": ">=0.7.4", + "fabric": "*", + "minecraft": "1.16.x" + }, + "suggests": {} +} diff --git a/src/main/resources/gud_asm.mixins.json b/src/main/resources/gud_asm.mixins.json new file mode 100644 index 0000000..8bd5179 --- /dev/null +++ b/src/main/resources/gud_asm.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.gudenau.minecraft.asm.mixin", + "compatibilityLevel": "JAVA_8", + "plugin": "net.gudenau.minecraft.asm.mixin.Plugin", + "mixins": [], + "client": [], + "injectors": { + "defaultRequire": 1 + } +}