├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── codechicken │ │ │ └── mixin │ │ │ ├── package-info.java │ │ │ ├── api │ │ │ ├── package-info.java │ │ │ ├── MixinDebugger.java │ │ │ ├── AsmName.java │ │ │ ├── JavaName.java │ │ │ ├── MixinFactory.java │ │ │ ├── MixinBackend.java │ │ │ ├── MixinLanguageSupport.java │ │ │ └── MixinCompiler.java │ │ │ ├── scala │ │ │ ├── package-info.java │ │ │ ├── ScalaClassInfo.java │ │ │ ├── MixinScalaLanguageSupport.java │ │ │ ├── ByteCodecs.java │ │ │ └── ScalaSignature.java │ │ │ ├── util │ │ │ ├── package-info.java │ │ │ ├── MethodInfo.java │ │ │ ├── FieldMixin.java │ │ │ ├── MixinInfo.java │ │ │ ├── ClassInfo.java │ │ │ ├── ClassNodeInfo.java │ │ │ ├── ReflectionClassInfo.java │ │ │ ├── SimpleDebugger.java │ │ │ ├── FactoryGenerator.java │ │ │ ├── Utils.java │ │ │ ├── SimpleServiceLoader.java │ │ │ └── JavaTraitGenerator.java │ │ │ ├── SidedFactory.java │ │ │ ├── MixinFactoryImpl.java │ │ │ └── MixinCompilerImpl.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── codechicken.mixin.api.MixinLanguageSupport └── test │ └── java │ └── codechicken │ └── mixin │ └── util │ └── FactoryGeneratorTests.java ├── settings.gradle ├── .gitignore ├── README.md ├── LICENSE.txt ├── gradlew.bat └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCBProject/TraitMixinCompiler/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package codechicken.mixin; 3 | 4 | import net.covers1624.quack.annotation.NonNullApi; 5 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package codechicken.mixin.api; 3 | 4 | import net.covers1624.quack.annotation.NonNullApi; 5 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/scala/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package codechicken.mixin.scala; 3 | 4 | import net.covers1624.quack.annotation.NonNullApi; 5 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package codechicken.mixin.util; 3 | 4 | import net.covers1624.quack.annotation.NonNullApi; 5 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/codechicken.mixin.api.MixinLanguageSupport: -------------------------------------------------------------------------------- 1 | codechicken.mixin.api.MixinLanguageSupport$JavaMixinLanguageSupport 2 | codechicken.mixin.scala.MixinScalaLanguageSupport 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | plugins { 9 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' 10 | } 11 | 12 | rootProject.name = 'TraitMixinCompiler' 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # exclude all 2 | /* 3 | 4 | # Include Important Folders 5 | !src/ 6 | 7 | # Gradle stuff 8 | !gradle/ 9 | !gradlew 10 | !gradlew.bat 11 | !build.gradle 12 | !settings.gradle 13 | 14 | # Other Files. 15 | !LICENSE.txt 16 | !README.md 17 | 18 | # Include git important files 19 | !.gitmodules 20 | !.gitignore 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TraitMixinCompiler 2 | ============== 3 | A Java based compiler for mixing scala-like trait classes onto a reference 4 | implementation. 5 | 6 | Features: 7 | - Runs without scala present. 8 | - Capable of converting Java classes to scala trait classes. 9 | - Modular, custom environments, additional JVM languages can be supported with ease. 10 | - Probably terrible. 11 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | /** 4 | * Created by covers1624 on 2/11/20. 5 | */ 6 | public interface MethodInfo { 7 | 8 | ClassInfo getOwner(); 9 | 10 | String getName(); 11 | 12 | String getDesc(); 13 | 14 | String[] getExceptions(); 15 | 16 | boolean isPrivate(); 17 | 18 | boolean isAbstract(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/MixinDebugger.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | /** 4 | * Created by covers1624 on 2/9/20. 5 | */ 6 | public interface MixinDebugger { 7 | 8 | void defineClass(@AsmName String name, byte[] bytes); 9 | 10 | class NullDebugger implements MixinDebugger { 11 | 12 | @Override 13 | public void defineClass(String name, byte[] bytes) { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/FieldMixin.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import static org.objectweb.asm.Opcodes.ACC_PRIVATE; 4 | 5 | /** 6 | * Created by covers1624 on 2/11/20. 7 | */ 8 | public record FieldMixin(String name, String desc, int access) { 9 | 10 | public String getAccessName(String owner) { 11 | if ((access & ACC_PRIVATE) != 0) { 12 | return owner.replace("/", "$") + "$$" + name; 13 | } 14 | return name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/AsmName.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks a member as using a JVM Internal name E.g: 'java/lang/Object' 10 | * Simply a marker for sanity sake. 11 | * Created by covers1624 on 2/17/20. 12 | */ 13 | @Retention (RetentionPolicy.SOURCE) 14 | @Target ({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) 15 | public @interface AsmName { } 16 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/JavaName.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks a member as using a standard java name E.g: 'java.lang.Object' 10 | * Simply a marker for sanity sake. 11 | * Created by covers1624 on 2/17/20. 12 | */ 13 | @Retention (RetentionPolicy.SOURCE) 14 | @Target ({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) 15 | public @interface JavaName { } 16 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/MixinInfo.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import net.covers1624.quack.collection.FastStream; 4 | import org.objectweb.asm.tree.MethodNode; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by covers1624 on 2/11/20. 10 | */ 11 | public record MixinInfo(String name, String parent, List parentTraits, List fields, List methods, List supers) { 12 | 13 | public FastStream linearize() { 14 | return FastStream.concat( 15 | FastStream.of(parentTraits).flatMap(MixinInfo::linearize), 16 | FastStream.of(this) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 covers1624, ChickenBones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/scala/ScalaClassInfo.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.scala; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import codechicken.mixin.util.ClassInfo; 5 | import codechicken.mixin.util.ClassNodeInfo; 6 | import net.covers1624.quack.collection.FastStream; 7 | import org.objectweb.asm.tree.ClassNode; 8 | 9 | /** 10 | * Created by covers1624 on 19/1/24. 11 | */ 12 | public class ScalaClassInfo extends ClassNodeInfo { 13 | 14 | public final ScalaSignature sig; 15 | public final ScalaSignature.ClassSymbolRef cSym; 16 | 17 | public ScalaClassInfo(MixinCompiler mixinCompiler, ClassNode cNode, ScalaSignature sig, ScalaSignature.ClassSymbolRef cSym) { 18 | super(mixinCompiler, cNode); 19 | this.sig = sig; 20 | this.cSym = cSym; 21 | interfaces = FastStream.of(cSym.jInterfaces()).map(mixinCompiler::getClassInfo).toList(); 22 | } 23 | 24 | @Override 25 | public ClassInfo concreteParent() { 26 | ClassInfo info = getSuperClass(); 27 | if (info instanceof ScalaClassInfo sInfo && sInfo.isTrait()) return sInfo.concreteParent(); 28 | 29 | return info; 30 | } 31 | 32 | @Override 33 | public boolean isInterface() { 34 | return cSym.isTrait() || cSym.isInterface(); 35 | } 36 | 37 | @Override 38 | public ClassInfo getSuperClass() { 39 | return mixinCompiler.getClassInfo(cSym.jParent()); 40 | } 41 | 42 | public boolean isTrait() { 43 | return cSym.isTrait(); 44 | } 45 | 46 | public boolean isObject() { 47 | return cSym.isObject(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/ClassInfo.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import net.covers1624.quack.collection.FastStream; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | /** 8 | * Created by covers1624 on 2/11/20. 9 | */ 10 | public abstract class ClassInfo { 11 | 12 | protected MixinCompiler mixinCompiler; 13 | 14 | protected ClassInfo(MixinCompiler mixinCompiler) { 15 | this.mixinCompiler = mixinCompiler; 16 | } 17 | 18 | public abstract String getName(); 19 | 20 | public abstract boolean isInterface(); 21 | 22 | public abstract @Nullable ClassInfo getSuperClass(); 23 | 24 | public abstract Iterable getInterfaces(); 25 | 26 | public abstract Iterable getMethods(); 27 | 28 | public FastStream getParentMethods() { 29 | return FastStream.ofNullable(getSuperClass()) 30 | .concat(getInterfaces()) 31 | .flatMap(ClassInfo::getAllMethods); 32 | } 33 | 34 | public FastStream getAllMethods() { 35 | return FastStream.concat(getMethods(), getParentMethods()); 36 | } 37 | 38 | public @Nullable MethodInfo findPublicImpl(String name, String desc) { 39 | return getAllMethods() 40 | .filter(m -> m.getName().equals(name)) 41 | .filter(m -> m.getDesc().equals(desc)) 42 | .filter(m -> !m.isAbstract() && !m.isPrivate()) 43 | .firstOrDefault(); 44 | } 45 | 46 | public @Nullable MethodInfo findPublicParentImpl(String name, String desc) { 47 | return getParentMethods() 48 | .filter(m -> m.getName().equals(name)) 49 | .filter(m -> m.getDesc().equals(desc)) 50 | .filter(m -> !m.isAbstract() && !m.isPrivate()) 51 | .firstOrDefault(); 52 | } 53 | 54 | public @Nullable ClassInfo concreteParent() { 55 | return getSuperClass(); 56 | } 57 | 58 | public boolean inheritsFrom(String parentName) { 59 | return FastStream.ofNullable(concreteParent()).concat(getInterfaces()) 60 | .anyMatch(e -> e.getName().equals(parentName) || e.inheritsFrom(parentName)); 61 | } 62 | 63 | public String getModuleName() { 64 | return getName(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/MixinFactory.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import org.objectweb.asm.tree.ClassNode; 5 | 6 | /** 7 | * Represents a 'user' facing interface for interacting and caching the {@link MixinCompiler}. 8 | *

9 | * Created by covers1624 on 2/17/20. 10 | */ 11 | public interface MixinFactory { 12 | 13 | /** 14 | * Gets the associated MixinCompiler for this MixinFactory. 15 | * 16 | * @return The MixinCompiler. 17 | */ 18 | MixinCompiler getMixinCompiler(); 19 | 20 | /** 21 | * Registers a trait. 22 | * 23 | * @param tClass The trait class. 24 | * @return a TraitKey for using this registered trait. 25 | */ 26 | TraitKey registerTrait(@AsmName Class tClass); 27 | 28 | /** 29 | * Registers a binary trait. Used for traits which 30 | * are generated at runtime. 31 | * 32 | * @param cNode The {@link ClassNode} for the trait. 33 | * @return a TraitKey for using this registered trait. 34 | */ 35 | TraitKey registerTrait(@AsmName ClassNode cNode); 36 | 37 | /** 38 | * Returns a factory ({@link F}) capable of constructing a new {@link B} with the given set of traits applied. 39 | *

40 | * The MixinFactory will cache constructed classes with the given set of traits, 41 | * subsequent calls with the same traits will not cause a new class to be generated. 42 | *

43 | * It should be noted that, {@link ImmutableSet} is explicitly used here, 44 | * as their hashCode is statically computed, making it favourable for use as a 45 | * key in a Map. 46 | * 47 | * @param traits The traits to apply. 48 | * @return The Factory. 49 | */ 50 | F construct(ImmutableSet traits); 51 | 52 | /** 53 | * Gets the traits that were used in compiling the given class. 54 | * If the given class was not compiled by this factory, simply returns null. 55 | * 56 | * @param clazz The Class to get the traits for. 57 | * @return The classes traits, or null if it was not compiled by this factory. 58 | */ 59 | ImmutableSet getTraitsForClass(Class clazz); 60 | 61 | /** 62 | * Unique key representing a registered trait. 63 | * 64 | * @param tName The trait class name. 65 | */ 66 | record TraitKey(String tName) { 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/ClassNodeInfo.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import net.covers1624.quack.collection.FastStream; 5 | import org.objectweb.asm.tree.ClassNode; 6 | import org.objectweb.asm.tree.MethodNode; 7 | 8 | import java.util.List; 9 | 10 | import static org.objectweb.asm.Opcodes.*; 11 | 12 | /** 13 | * Created by covers1624 on 2/11/20. 14 | */ 15 | public class ClassNodeInfo extends ClassInfo { 16 | 17 | private final ClassNode cNode; 18 | protected List interfaces; 19 | private final List methods; 20 | 21 | public ClassNodeInfo(MixinCompiler mixinCompiler, ClassNode cNode) { 22 | super(mixinCompiler); 23 | this.cNode = cNode; 24 | interfaces = FastStream.of(cNode.interfaces) 25 | .map(mixinCompiler::getClassInfo) 26 | .toList(); 27 | methods = FastStream.of(cNode.methods) 28 | .map(MethodNodeInfo::new) 29 | .toList(FastStream.infer()); 30 | } 31 | 32 | //@formatter:off 33 | @Override public String getName() { return cNode.name; } 34 | @Override public boolean isInterface() { return (cNode.access & ACC_INTERFACE) != 0; } 35 | @Override public ClassInfo getSuperClass() { return cNode.superName != null ? mixinCompiler.getClassInfo(cNode.superName) : null; } 36 | @Override public Iterable getInterfaces() { return interfaces; } 37 | @Override public Iterable getMethods() { return methods; } 38 | public ClassNode getCNode() { return cNode; } 39 | //@formatter:on 40 | 41 | public class MethodNodeInfo implements MethodInfo { 42 | 43 | private final MethodNode mNode; 44 | private final String[] exceptions; 45 | 46 | public MethodNodeInfo(MethodNode mNode) { 47 | this.mNode = mNode; 48 | exceptions = mNode.exceptions.toArray(new String[0]); 49 | } 50 | 51 | //@formatter:off 52 | @Override public ClassInfo getOwner() { return ClassNodeInfo.this; } 53 | @Override public String getName() { return mNode.name; } 54 | @Override public String getDesc() { return mNode.desc; } 55 | @Override public String[] getExceptions() { return exceptions; } 56 | @Override public boolean isPrivate() { return (mNode.access & ACC_PRIVATE) != 0; } 57 | @Override public boolean isAbstract() { return (mNode.access & ACC_ABSTRACT) != 0; } 58 | //@formatter:on 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/ReflectionClassInfo.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import net.covers1624.quack.collection.FastStream; 5 | import org.objectweb.asm.Type; 6 | 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Modifier; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by covers1624 on 2/11/20. 14 | */ 15 | public class ReflectionClassInfo extends ClassInfo { 16 | 17 | private final Class clazz; 18 | private final String name; 19 | private final List interfaces; 20 | private final List methods; 21 | 22 | public ReflectionClassInfo(MixinCompiler mixinCompiler, Class clazz) { 23 | super(mixinCompiler); 24 | this.clazz = clazz; 25 | name = Utils.asmName(clazz.getName()); 26 | interfaces = FastStream.of(clazz.getInterfaces()) 27 | .map(mixinCompiler::getClassInfo) 28 | .toList(); 29 | methods = FastStream.of(clazz.getMethods()) 30 | .map(ReflectionMethodInfo::new) 31 | .toList(FastStream.infer()); 32 | } 33 | 34 | //@formatter:off 35 | @Override public String getName() { return name; } 36 | @Override public boolean isInterface() { return clazz.isInterface(); } 37 | @Override public ClassInfo getSuperClass() { return mixinCompiler.getClassInfo(clazz.getSuperclass()); } 38 | @Override public Iterable getInterfaces() { return interfaces; } 39 | @Override public Iterable getMethods() { return methods; } 40 | //@formatter:on 41 | 42 | public class ReflectionMethodInfo implements MethodInfo { 43 | 44 | private final String name; 45 | private final String desc; 46 | private final String[] exceptions; 47 | private final boolean isPrivate; 48 | private final boolean isAbstract; 49 | 50 | private ReflectionMethodInfo(Method method) { 51 | name = method.getName(); 52 | desc = Type.getType(method).getDescriptor(); 53 | exceptions = FastStream.of(method.getExceptionTypes()) 54 | .map(Class::getName) 55 | .map(Utils::asmName) 56 | .toArray(new String[0]); 57 | isPrivate = Modifier.isPrivate(method.getModifiers()); 58 | isAbstract = Modifier.isAbstract(method.getModifiers()); 59 | } 60 | 61 | //@formatter:off 62 | @Override public ClassInfo getOwner() { return ReflectionClassInfo.this; } 63 | @Override public String getName() { return name; } 64 | @Override public String getDesc() { return desc; } 65 | @Override public String[] getExceptions() { return exceptions; } 66 | @Override public boolean isPrivate() { return isPrivate; } 67 | @Override public boolean isAbstract() { return isAbstract; } 68 | //@formatter:on 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/MixinBackend.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | import net.covers1624.quack.io.IOUtils; 4 | import net.covers1624.quack.reflect.PrivateLookups; 5 | import net.covers1624.quack.util.SneakyUtils; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.lang.invoke.MethodHandle; 11 | import java.lang.invoke.MethodType; 12 | 13 | /** 14 | * Provides an abstracted backend for the {@link MixinCompiler} system. 15 | * This allows different environments, to change the low level 16 | * integration {@link MixinCompiler} requires with the running environment. 17 | * I.e, Running under MinecraftForge requires _some_ tweaks. 18 | *

19 | * Created by covers1624 on 2/11/20. 20 | */ 21 | public interface MixinBackend { 22 | 23 | /** 24 | * The class loader to delegate class loading calls to. 25 | * 26 | * @return The context class loader. 27 | */ 28 | ClassLoader getContextClassLoader(); 29 | 30 | /** 31 | * Gets the bytes for a class. 32 | * 33 | * @param name The class name. 34 | * @return The bytes for the class. 35 | */ 36 | byte @Nullable [] getBytes(@AsmName String name); 37 | 38 | /** 39 | * Allows a MixinBackend to filter a method based on the annotation value for 'value'. 40 | * Used exclusively for {@link codechicken.mixin.scala.MixinScalaLanguageSupport} 41 | * with {@link codechicken.mixin.forge.ForgeMixinBackend} to strip methods from 42 | * {@link codechicken.mixin.scala.ScalaSignature} in a Forge environment. 43 | * 44 | * @param annType The annotation type. 45 | * @param value The annotation value for 'value' 46 | * @return To remove or not. 47 | */ 48 | default boolean filterMethodAnnotations(String annType, String value) { 49 | return true; 50 | } 51 | 52 | /** 53 | * A simple {@link MixinBackend} implementation for standalone use. 54 | */ 55 | class SimpleMixinBackend implements MixinBackend { 56 | 57 | private final ClassLoader classLoader; 58 | 59 | public SimpleMixinBackend() { 60 | this(SimpleMixinBackend.class.getClassLoader()); 61 | } 62 | 63 | public SimpleMixinBackend(ClassLoader classLoader) { 64 | this.classLoader = classLoader; 65 | } 66 | 67 | @Override 68 | public ClassLoader getContextClassLoader() { 69 | return classLoader; 70 | } 71 | 72 | @Override 73 | public byte @Nullable [] getBytes(String name) { 74 | try (InputStream is = classLoader.getResourceAsStream(name + ".class")) { 75 | if (is == null) { 76 | return null; 77 | } 78 | return IOUtils.toBytes(is); 79 | } catch (IOException e) { 80 | SneakyUtils.throwUnchecked(new ClassNotFoundException("Could not load bytes for '" + name + "'.", e)); 81 | return null; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/SimpleDebugger.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.mixin.api.MixinDebugger; 4 | import net.covers1624.quack.io.IOUtils; 5 | import org.objectweb.asm.ClassReader; 6 | import org.objectweb.asm.ClassVisitor; 7 | import org.objectweb.asm.util.Textifier; 8 | import org.objectweb.asm.util.TraceClassVisitor; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.io.BufferedWriter; 13 | import java.io.IOException; 14 | import java.io.OutputStream; 15 | import java.io.PrintWriter; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | 19 | import static codechicken.mixin.MixinCompilerImpl.LOG_LEVEL; 20 | 21 | /** 22 | * Created by covers1624 on 2/9/20. 23 | */ 24 | public class SimpleDebugger implements MixinDebugger { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDebugger.class); 27 | 28 | private final Path folder; 29 | private final DumpType type; 30 | 31 | public SimpleDebugger(Path folder, DumpType type) { 32 | this.folder = folder; 33 | this.type = type; 34 | try { 35 | if (Files.exists(folder)) { 36 | if (!Files.isDirectory(folder)) { 37 | LOGGER.warn("Expected '{}' to be a directory. Overwriting..", folder); 38 | Files.delete(folder); 39 | } else { 40 | LOGGER.atLevel(LOG_LEVEL).log("Clearing debugger output. '{}'", folder.toAbsolutePath()); 41 | Utils.deleteFolder(folder); 42 | } 43 | } 44 | Files.createDirectories(folder); 45 | } catch (IOException e) { 46 | LOGGER.error("Encountered an error setting up SimpleDebugger.", e); 47 | } 48 | } 49 | 50 | @Override 51 | public void defineClass(String name, byte[] bytes) { 52 | name = name.replace("/", "."); 53 | try { 54 | switch (type) { 55 | case TEXT: { 56 | try { 57 | LOGGER.atLevel(LOG_LEVEL).log("Dumping '{}' as text", name); 58 | Path path = folder.resolve(name + ".txt"); 59 | try (BufferedWriter writer = Files.newBufferedWriter(IOUtils.makeParents(path))) { 60 | ClassVisitor cv = new TraceClassVisitor(null, new Textifier(), new PrintWriter(writer)); 61 | ClassReader reader = new ClassReader(bytes); 62 | reader.accept(cv, ClassReader.EXPAND_FRAMES); 63 | } 64 | break; 65 | } catch (IOException e) { 66 | throw e;// Rethrow 67 | } catch (Exception e) { 68 | LOGGER.warn("Fatal exception dumping as text. Dumping as binary.", e); 69 | //Fall through to Binary. 70 | } 71 | } 72 | case BINARY: { 73 | LOGGER.atLevel(LOG_LEVEL).log("Dumping '{}' as binary.", name); 74 | Path path = folder.resolve(name + ".class"); 75 | try (OutputStream os = Files.newOutputStream(IOUtils.makeParents(path))) { 76 | os.write(bytes); 77 | } 78 | break; 79 | } 80 | } 81 | } catch (IOException e) { 82 | LOGGER.error("Unable to dump '{}' to disk.", name, e); 83 | } 84 | } 85 | 86 | public enum DumpType { 87 | TEXT, 88 | BINARY, 89 | ; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/codechicken/mixin/util/FactoryGeneratorTests.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * Created by covers1624 on 3/2/21. 9 | */ 10 | public class FactoryGeneratorTests { 11 | 12 | @Test 13 | public void testVoidFactory() { 14 | MixinCompiler compiler = MixinCompiler.create(); 15 | FactoryGenerator generator = new FactoryGenerator(compiler); 16 | VoidFactory factory = generator.generateFactory(ThingToMake.class, VoidFactory.class); 17 | ThingToMake thing = factory.construct(); 18 | } 19 | 20 | public interface VoidFactory { 21 | 22 | ThingToMake construct(); 23 | } 24 | 25 | @Test 26 | public void testObjectParam() { 27 | MixinCompiler compiler = MixinCompiler.create(); 28 | FactoryGenerator generator = new FactoryGenerator(compiler); 29 | ObjectParamFactory factory = generator.generateFactory(ThingToMake.class, ObjectParamFactory.class); 30 | ThingToMake thing = factory.construct("Hello World"); 31 | 32 | Assertions.assertEquals("Hello World", thing.str); 33 | } 34 | 35 | public interface ObjectParamFactory { 36 | 37 | ThingToMake construct(String str); 38 | } 39 | 40 | @Test 41 | public void testPrimitiveParam() { 42 | MixinCompiler compiler = MixinCompiler.create(); 43 | FactoryGenerator generator = new FactoryGenerator(compiler); 44 | PrimitiveParamFactory factory = generator.generateFactory(ThingToMake.class, PrimitiveParamFactory.class); 45 | ThingToMake thing = factory.construct(69420); 46 | 47 | Assertions.assertEquals(69420, thing.i); 48 | } 49 | 50 | public interface PrimitiveParamFactory { 51 | 52 | ThingToMake construct(int i); 53 | } 54 | 55 | @Test 56 | public void testWidePrimitiveParam() { 57 | MixinCompiler compiler = MixinCompiler.create(); 58 | FactoryGenerator generator = new FactoryGenerator(compiler); 59 | WidePrimitiveParamFactory factory = generator.generateFactory(ThingToMake.class, WidePrimitiveParamFactory.class); 60 | ThingToMake thing = factory.construct(69.420D); 61 | 62 | Assertions.assertEquals(69.420D, thing.d); 63 | } 64 | 65 | public interface WidePrimitiveParamFactory { 66 | 67 | ThingToMake construct(double d); 68 | } 69 | 70 | @Test 71 | public void testMixedParams() { 72 | MixinCompiler compiler = MixinCompiler.create(); 73 | FactoryGenerator generator = new FactoryGenerator(compiler); 74 | MixedParamFactory factory = generator.generateFactory(ThingToMake.class, MixedParamFactory.class); 75 | ThingToMake thing = factory.construct("Hello", 69420, 69.420D, "World"); 76 | 77 | Assertions.assertEquals("Hello", thing.str); 78 | Assertions.assertEquals(69420, thing.i); 79 | Assertions.assertEquals(69.420D, thing.d); 80 | Assertions.assertEquals("World", thing.str2); 81 | } 82 | 83 | public interface MixedParamFactory { 84 | 85 | ThingToMake construct(String str, int i, double d, String str2); 86 | } 87 | 88 | public static class ThingToMake { 89 | 90 | private String str; 91 | private int i; 92 | private double d; 93 | private String str2; 94 | 95 | public ThingToMake() { 96 | } 97 | 98 | public ThingToMake(String str) { 99 | this.str = str; 100 | } 101 | 102 | public ThingToMake(int i) { 103 | this.i = i; 104 | } 105 | 106 | public ThingToMake(double d) { 107 | this.d = d; 108 | } 109 | 110 | public ThingToMake(String str, int i, double d, String str2) { 111 | this.str = str; 112 | this.i = i; 113 | this.d = d; 114 | this.str2 = str2; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/FactoryGenerator.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import net.covers1624.quack.collection.FastStream; 5 | import org.objectweb.asm.ClassWriter; 6 | import org.objectweb.asm.MethodVisitor; 7 | import org.objectweb.asm.Type; 8 | 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.Modifier; 11 | import java.util.Arrays; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | import static codechicken.mixin.util.Utils.asmName; 15 | import static org.objectweb.asm.Opcodes.*; 16 | 17 | /** 18 | * Created by covers1624 on 7/1/21. 19 | */ 20 | public class FactoryGenerator { 21 | 22 | private static final AtomicInteger COUNTER = new AtomicInteger(); 23 | 24 | private final MixinCompiler compiler; 25 | 26 | public FactoryGenerator(MixinCompiler compiler) { 27 | this.compiler = compiler; 28 | } 29 | 30 | public Method findMethod(Class clazz) { 31 | if (!clazz.isInterface()) { 32 | throw new RuntimeException("Class is not an interface."); 33 | } 34 | Method[] methods = FastStream.of(clazz.getMethods()) 35 | .filter(e -> !Modifier.isStatic(e.getModifiers())) 36 | .filter(e -> Modifier.isAbstract(e.getModifiers())) 37 | .toArray(new Method[0]); 38 | if (methods.length == 0) { 39 | throw new IllegalArgumentException("No implementable methods found for class: " + clazz.getName()); 40 | } 41 | if (methods.length > 1) { 42 | String names = FastStream.of(methods) 43 | .map(Method::getName) 44 | .join(","); 45 | throw new IllegalStateException("Multiple implementable methods found. [" + names + "] in " + clazz.getName()); 46 | } 47 | return methods[0]; 48 | } 49 | 50 | public F generateFactory(Class actualClass, Class factoryClazz) { 51 | Method factoryMethod = findMethod(factoryClazz); 52 | if (Utils.findConstructor(actualClass, factoryMethod.getParameterTypes()) == null) { 53 | throw new IllegalArgumentException("Unable to find constructor for " + actualClass.getName() + " that matches Factory method in " + factoryClazz.getName()); 54 | } 55 | 56 | ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 57 | MethodVisitor mv; 58 | 59 | String cName = actualClass.getName().replace('.', '/') + "$$Ctor$$" + COUNTER.getAndIncrement(); 60 | cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER | ACC_FINAL | ACC_SYNTHETIC, cName, null, asmName(Object.class), new String[] { asmName(factoryClazz) }); 61 | 62 | mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); 63 | mv.visitCode(); 64 | mv.visitVarInsn(ALOAD, 0); 65 | mv.visitMethodInsn(INVOKESPECIAL, asmName(Object.class), "", "()V", false); 66 | mv.visitInsn(RETURN); 67 | mv.visitMaxs(-1, -1); 68 | mv.visitEnd(); 69 | 70 | Type factoryType = Type.getType(factoryMethod); 71 | Type[] params = factoryType.getArgumentTypes(); 72 | mv = cw.visitMethod(ACC_PUBLIC, factoryMethod.getName(), factoryType.getDescriptor(), null, null); 73 | mv.visitCode(); 74 | mv.visitTypeInsn(NEW, asmName(actualClass)); 75 | mv.visitInsn(DUP); 76 | int count = 1; 77 | for (Type param : params) { 78 | mv.visitVarInsn(param.getOpcode(ILOAD), count); 79 | count += (param.getSort() == Type.DOUBLE || param.getSort() == Type.LONG) ? 2 : 1; 80 | } 81 | mv.visitMethodInsn(INVOKESPECIAL, asmName(actualClass), "", Type.getMethodDescriptor(Type.VOID_TYPE, params), false); 82 | mv.visitTypeInsn(CHECKCAST, asmName(factoryType.getReturnType().getInternalName())); 83 | mv.visitInsn(ARETURN); 84 | mv.visitMaxs(-1, -1); 85 | mv.visitEnd(); 86 | 87 | cw.visitEnd(); 88 | 89 | byte[] bytes = cw.toByteArray(); 90 | 91 | Class factory = compiler.defineClass(cName, bytes); 92 | try { 93 | return factory.getConstructor().newInstance(); 94 | } catch (Throwable ex) { 95 | throw new RuntimeException("Unable to instantiate new factory.", ex); 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/MixinLanguageSupport.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | import codechicken.asm.ASMHelper; 4 | import codechicken.mixin.util.ClassInfo; 5 | import codechicken.mixin.util.ClassNodeInfo; 6 | import codechicken.mixin.util.JavaTraitGenerator; 7 | import codechicken.mixin.util.MixinInfo; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.objectweb.asm.tree.ClassNode; 10 | 11 | import java.lang.annotation.ElementType; 12 | import java.lang.annotation.Retention; 13 | import java.lang.annotation.RetentionPolicy; 14 | import java.lang.annotation.Target; 15 | import java.util.ServiceLoader; 16 | import java.util.function.BiFunction; 17 | 18 | import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; 19 | import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; 20 | 21 | /** 22 | * Defines abstract logic for loading Mixins for different languages. 23 | * These are loaded using a {@link ServiceLoader} from the classpath. 24 | *

25 | * Created by covers1624 on 2/16/20. 26 | */ 27 | public interface MixinLanguageSupport { 28 | 29 | /** 30 | * Tries to load a {@link ClassInfo} for the given {@link ClassNode}. 31 | * Custom implementations shouldn't be greedy, only load a 32 | * {@link ClassInfo} if you know for certain that you need to. 33 | * I.e: {@link codechicken.mixin.scala.MixinScalaLanguageSupport}, only loads 34 | * a {@link codechicken.mixin.scala.ScalaClassInfo} if the class has a 35 | * ScalaSignature Annotation, and is a Scala trait class. 36 | * 37 | * @param cNode The ClassNode. 38 | * @return The ClassInfo. 39 | */ 40 | @Nullable 41 | ClassInfo obtainInfo(ClassNode cNode); 42 | 43 | /** 44 | * Tries to build a {@link MixinInfo} for the given {@link ClassNode}. 45 | * as with {@link #obtainInfo}, only load MixinInfos if you 46 | * know for certain that you need to. I.e: {@link codechicken.mixin.scala.MixinScalaLanguageSupport}, 47 | * will only load a MixinInfo for the class, if {@link MixinCompiler#getClassInfo(ClassNode)} 48 | * resolves to a {@link codechicken.mixin.scala.ScalaClassInfo}. 49 | * 50 | * @param cNode The ClassNode. 51 | * @return The MixinInfo. 52 | */ 53 | @Nullable 54 | MixinInfo buildMixinTrait(ClassNode cNode); 55 | 56 | /** 57 | * The name for the MixinLanguageSupport, Required. 58 | * Must be unique. 59 | */ 60 | @Target (ElementType.TYPE) 61 | @Retention (RetentionPolicy.RUNTIME) 62 | @interface LanguageName { 63 | 64 | String value(); 65 | } 66 | 67 | /** 68 | * A simple way to sort this {@link MixinLanguageSupport} before others in the list. 69 | * A smaller number will be earlier in the list. E.g: [-3000, -100, 0, 100, 3000] 70 | * If this annotation is not provided, it will default to an index of 1000. 71 | */ 72 | @Target (ElementType.TYPE) 73 | @Retention (RetentionPolicy.RUNTIME) 74 | @interface SortingIndex { 75 | 76 | int value(); 77 | } 78 | 79 | /** 80 | * The default java handling for MixinCompiler. 81 | */ 82 | @LanguageName ("java") 83 | @SortingIndex (Integer.MAX_VALUE) // Always last 84 | class JavaMixinLanguageSupport implements MixinLanguageSupport { 85 | 86 | protected final MixinCompiler mixinCompiler; 87 | private BiFunction traitGeneratorFactory = JavaTraitGenerator::new; 88 | 89 | public JavaMixinLanguageSupport(MixinCompiler mixinCompiler) { 90 | this.mixinCompiler = mixinCompiler; 91 | } 92 | 93 | public void setTraitGeneratorFactory(BiFunction factory) { 94 | traitGeneratorFactory = factory; 95 | } 96 | 97 | @Override 98 | public ClassInfo obtainInfo(ClassNode cNode) { 99 | return new ClassNodeInfo(mixinCompiler, cNode); 100 | } 101 | 102 | @Override 103 | public MixinInfo buildMixinTrait(ClassNode cNode) { 104 | JavaTraitGenerator generator = traitGeneratorFactory.apply(mixinCompiler, cNode); 105 | ClassNode sNode = generator.getStaticNode(); 106 | if (sNode != null) { 107 | mixinCompiler.defineClass(sNode.name, ASMHelper.createBytes(sNode, COMPUTE_FRAMES | COMPUTE_MAXS)); 108 | } 109 | ClassNode tNode = generator.getTraitNode(); 110 | MixinInfo info = generator.getMixinInfo(); 111 | mixinCompiler.defineClass(tNode.name, ASMHelper.createBytes(tNode, COMPUTE_FRAMES | COMPUTE_MAXS)); 112 | return info; 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/SidedFactory.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin; 2 | 3 | import codechicken.mixin.api.AsmName; 4 | import codechicken.mixin.api.JavaName; 5 | import codechicken.mixin.api.MixinCompiler; 6 | import codechicken.mixin.util.Utils; 7 | import com.google.common.collect.ImmutableSet; 8 | import net.covers1624.quack.collection.FastStream; 9 | import org.jetbrains.annotations.Nullable; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | 17 | /** 18 | * Created by covers1624 on 20/1/24. 19 | */ 20 | public abstract class SidedFactory extends MixinFactoryImpl { 21 | 22 | private static final Logger LOGGER = LoggerFactory.getLogger(SidedFactory.class); 23 | 24 | protected final Map, TraitKey> clientTraits = new HashMap<>(); 25 | protected final Map, TraitKey> serverTraits = new HashMap<>(); 26 | 27 | protected final Map, ImmutableSet> clientObjectTraitCache = new HashMap<>(); 28 | protected final Map, ImmutableSet> serverObjectTraitCache = new HashMap<>(); 29 | 30 | protected SidedFactory(MixinCompiler mc, Class baseType, Class factory, String suffix) { 31 | super(mc, baseType, factory, suffix); 32 | } 33 | 34 | /** 35 | * Overload of {@link #registerTrait(Class, Class, Class)}, using the same 36 | * trait impl for client and server. 37 | * 38 | * @param marker The Marker class, to be found in the part instances class hierarchy. 39 | */ 40 | @AsmName 41 | @JavaName 42 | public void registerTrait(Class marker, Class trait) { 43 | registerTrait(marker, trait, trait); 44 | } 45 | 46 | /** 47 | * Registers a trait to be applied to the host tile in the presence of a specific 48 | * marker class existing in the class hierarchy of a part instance. 49 | * 50 | * @param marker The Marker class, to be found in the part instances class hierarchy. 51 | * @param clientTrait The trait class to be applied on the client side. 52 | * @param serverTrait The trait class to be applied on the server side. 53 | */ 54 | @AsmName 55 | @JavaName 56 | public void registerTrait(Class marker, @Nullable Class clientTrait, @Nullable Class serverTrait) { 57 | if (clientTrait != null) { 58 | register(clientTraits, marker, clientTrait); 59 | } 60 | 61 | if (serverTrait != null) { 62 | register(serverTraits, marker, serverTrait); 63 | } 64 | } 65 | 66 | /** 67 | * Gets all the {@link TraitKey}'s this generator knows about from the thing's 68 | * class hierarchy. 69 | * 70 | * @param thing The thing to get all traits from. 71 | * @param client If this is the client side or not. 72 | * @return The {@link TraitKey}s. 73 | */ 74 | public ImmutableSet getTraitsForObject(T thing, boolean client) { 75 | return getObjectTraitCache(client).computeIfAbsent(thing.getClass(), clazz -> { 76 | Map, TraitKey> traits = getTraitMap(client); 77 | return hierarchy(clazz) 78 | .map(traits::get) 79 | .filter(Objects::nonNull) 80 | .toImmutableSet(); 81 | }); 82 | } 83 | 84 | protected FastStream> hierarchy(Class clazz) { 85 | return FastStream.concat( 86 | FastStream.of(clazz), 87 | FastStream.of(clazz.getInterfaces()).flatMap(this::hierarchy), 88 | FastStream.ofNullable(clazz.getSuperclass()).flatMap(this::hierarchy) 89 | ); 90 | } 91 | 92 | protected Map, TraitKey> getTraitMap(boolean client) { 93 | return client ? clientTraits : serverTraits; 94 | } 95 | 96 | protected Map, ImmutableSet> getObjectTraitCache(boolean client) { 97 | return client ? clientObjectTraitCache : serverObjectTraitCache; 98 | } 99 | 100 | protected void register(Map, TraitKey> map, Class marker, Class trait) { 101 | String tName = Utils.asmName(trait); 102 | TraitKey existing = map.get(marker); 103 | if (existing != null) { 104 | if (existing.tName().equals(tName)) { 105 | LOGGER.error("Attempted to re-register trait for '{}' with a different impl. Ignoring. Existing: '{}', New: '{}'", marker, existing.tName(), tName); 106 | } else { 107 | LOGGER.error("Skipping re-register of trait for '{}' and impl '{}'", marker, tName); 108 | } 109 | return; 110 | } 111 | map.put(marker, registerTrait(trait)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/MixinFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import codechicken.mixin.api.MixinFactory; 5 | import codechicken.mixin.util.ClassInfo; 6 | import codechicken.mixin.util.FactoryGenerator; 7 | import codechicken.mixin.util.Utils; 8 | import com.google.common.collect.ImmutableSet; 9 | import net.covers1624.quack.collection.FastStream; 10 | import net.covers1624.quack.util.SneakyUtils; 11 | import org.objectweb.asm.tree.ClassNode; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | import java.util.function.BiConsumer; 16 | 17 | /** 18 | * Created by covers1624 on 2/17/20. 19 | */ 20 | public class MixinFactoryImpl implements MixinFactory { 21 | 22 | protected final AtomicInteger counter = new AtomicInteger(); 23 | protected final Map, Class> classCache = new HashMap<>(); 24 | protected final Map, F> factoryCache = new HashMap<>(); 25 | protected final Map, ImmutableSet> traitLookup = new HashMap<>(); 26 | protected final Map registeredTraits = new HashMap<>(); 27 | 28 | protected final MixinCompiler mixinCompiler; 29 | protected final Class baseType; 30 | protected final Class factoryClass; 31 | protected final String classSuffix; 32 | 33 | protected final FactoryGenerator factoryGenerator; 34 | 35 | public MixinFactoryImpl(MixinCompiler mixinCompiler, Class baseType, Class factoryClass, String classSuffix) { 36 | this.mixinCompiler = mixinCompiler; 37 | this.baseType = baseType; 38 | this.factoryClass = factoryClass; 39 | this.classSuffix = classSuffix; 40 | 41 | factoryGenerator = new FactoryGenerator(mixinCompiler); 42 | //Validate factory. 43 | factoryGenerator.findMethod(factoryClass); 44 | } 45 | 46 | @Override 47 | public MixinCompiler getMixinCompiler() { 48 | return mixinCompiler; 49 | } 50 | 51 | @Override 52 | public synchronized TraitKey registerTrait(Class tClass) { 53 | String tName = Utils.asmName(tClass); 54 | TraitKey trait = registeredTraits.get(tName); 55 | if (trait != null) return trait; 56 | 57 | ClassNode cNode = mixinCompiler.getClassNode(tName); 58 | if (cNode == null) { 59 | SneakyUtils.throwUnchecked(new ClassNotFoundException(tName)); 60 | return null; 61 | } 62 | return registerTrait(cNode); 63 | } 64 | 65 | @Override 66 | public synchronized TraitKey registerTrait(ClassNode cNode) { 67 | String tName = cNode.name; 68 | TraitKey key = registeredTraits.get(tName); 69 | if (key != null) { 70 | return key; 71 | } 72 | ClassInfo info = mixinCompiler.getClassInfo(cNode); 73 | 74 | String parentName = info.concreteParent().getName(); 75 | ClassInfo baseInfo = mixinCompiler.getClassInfo(baseType); 76 | if (!checkParent(parentName, baseInfo)) { 77 | throw new IllegalArgumentException("Trait '" + tName + "' with resolved parent '" + parentName + "' does not extend base type '" + Utils.asmName(baseType) + "'"); 78 | } 79 | mixinCompiler.registerTrait(cNode); 80 | key = new TraitKey(tName); 81 | registeredTraits.put(tName, key); 82 | return key; 83 | } 84 | 85 | @Override 86 | public F construct(ImmutableSet traits) { 87 | return factoryCache.computeIfAbsent(traits, this::compile); 88 | } 89 | 90 | @Override 91 | public ImmutableSet getTraitsForClass(Class clazz) { 92 | return traitLookup.get(clazz); 93 | } 94 | 95 | private boolean checkParent(String parentName, ClassInfo info) { 96 | if (info.getName().equals(parentName)) return true; 97 | 98 | ClassInfo sClass = info.getSuperClass(); 99 | if (sClass == null) return false; 100 | 101 | return checkParent(parentName, sClass); 102 | } 103 | 104 | private synchronized F compile(ImmutableSet traits) { 105 | Class clazz = classCache.computeIfAbsent(traits, e -> { 106 | Set traitNames = FastStream.of(traits).map(TraitKey::tName).toImmutableSet(); 107 | Class compiled = mixinCompiler.compileMixinClass(nextName(), Utils.asmName(baseType), traitNames); 108 | traitLookup.put(compiled, traits); 109 | return compiled; 110 | }); 111 | return factoryGenerator.generateFactory(clazz, factoryClass); 112 | } 113 | 114 | private String nextName() { 115 | return baseType.getSimpleName() + "_" + classSuffix + "$$" + counter.getAndIncrement(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/Utils.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.asm.StackAnalyser; 4 | import net.covers1624.quack.collection.ColUtils; 5 | import net.covers1624.quack.collection.FastStream; 6 | import org.jetbrains.annotations.Nullable; 7 | import org.objectweb.asm.MethodVisitor; 8 | import org.objectweb.asm.Type; 9 | import org.objectweb.asm.tree.ClassNode; 10 | import org.objectweb.asm.tree.MethodNode; 11 | 12 | import java.io.IOException; 13 | import java.io.UncheckedIOException; 14 | import java.lang.reflect.Constructor; 15 | import java.lang.reflect.InvocationTargetException; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.*; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.stream.Stream; 21 | 22 | import static org.objectweb.asm.Opcodes.*; 23 | 24 | /** 25 | * Created by covers1624 on 2/11/20. 26 | */ 27 | public class Utils { 28 | 29 | public static String asmName(Class clazz) { 30 | return asmName(clazz.getName()); 31 | } 32 | 33 | public static String asmName(String name) { 34 | return name.replace(".", "/"); 35 | } 36 | 37 | /** 38 | * Represents this Enumeration as an Iterable. 39 | * 40 | * @param enumeration The Enumeration. 41 | * @param The Type. 42 | * @return The Iterable. 43 | */ 44 | public static Iterable toIterable(Enumeration enumeration) { 45 | return () -> new Iterator() { 46 | //@formatter:off 47 | @Override public boolean hasNext() { return enumeration.hasMoreElements(); } 48 | @Override public E next() { return enumeration.nextElement(); } 49 | //@formatter:on 50 | }; 51 | } 52 | 53 | public static @Nullable Constructor findConstructor(Class clazz, Class... parameters) { 54 | try { 55 | return clazz.getConstructor(parameters); 56 | } catch (NoSuchMethodException e) { 57 | return null; 58 | } 59 | } 60 | 61 | public static T newInstance(Constructor ctor, Object... args) { 62 | try { 63 | return ctor.newInstance(args); 64 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 65 | throw new RuntimeException("Failed to instantiate class.", e); 66 | } 67 | } 68 | 69 | public static void deleteFolder(Path folder) throws IOException { 70 | try (Stream stream = Files.walk(folder)) { 71 | stream.sorted(Comparator.reverseOrder()).forEach(p -> { 72 | try { 73 | Files.delete(p); 74 | } catch (IOException e) { 75 | throw new UncheckedIOException(e); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | public static String staticDesc(String owner, String desc) { 82 | Type descT = Type.getMethodType(desc); 83 | List args = new ArrayList<>(Arrays.asList(descT.getArgumentTypes())); 84 | args.add(0, Type.getType("L" + owner + ";")); 85 | return Type.getMethodDescriptor(descT.getReturnType(), args.toArray(new Type[0])); 86 | } 87 | 88 | public static String timeString(long start, long end) { 89 | long delta = end - start; 90 | long millis = TimeUnit.NANOSECONDS.toMillis(delta); 91 | 92 | String s; 93 | if (millis >= 5) { 94 | s = millis + "ms(" + delta + "ns)"; 95 | } else { 96 | s = delta + "ns"; 97 | } 98 | return s; 99 | 100 | } 101 | 102 | public static FastStream allParents(ClassInfo info) { 103 | return FastStream.concat( 104 | FastStream.of(info), 105 | FastStream.concat( 106 | FastStream.ofNullable(info.getSuperClass()), 107 | info.getInterfaces() 108 | ).flatMap(Utils::allParents) 109 | ); 110 | } 111 | 112 | public static void finishBridgeCall(MethodVisitor mv, String mvDesc, int opcode, String owner, String name, String desc, boolean isInterface) { 113 | Type[] args = Type.getArgumentTypes(mvDesc); 114 | Type returnType = Type.getReturnType(mvDesc); 115 | int localIndex = 1; 116 | for (Type arg : args) { 117 | mv.visitVarInsn(arg.getOpcode(ILOAD), localIndex); 118 | localIndex += arg.getSize(); 119 | } 120 | mv.visitMethodInsn(opcode, owner, name, desc, isInterface); 121 | mv.visitInsn(returnType.getOpcode(IRETURN)); 122 | mv.visitMaxs(-1, -1);//COMPUTE_FRAMES :) 123 | } 124 | 125 | @Deprecated // This should not be used, specify isInterface explicitly. 126 | public static void writeBridge(MethodVisitor mv, String mvDesc, int opcode, String owner, String name, String desc) { 127 | writeBridge(mv, mvDesc, opcode, owner, name, desc, opcode == INVOKEINTERFACE); 128 | } 129 | 130 | public static void writeBridge(MethodVisitor mv, String mvDesc, int opcode, String owner, String name, String desc, boolean isInterface) { 131 | mv.visitVarInsn(ALOAD, 0); 132 | finishBridgeCall(mv, mvDesc, opcode, owner, name, desc, isInterface); 133 | } 134 | 135 | public static void writeStaticBridge(MethodNode mv, String mName, MixinInfo info) { 136 | writeBridge(mv, mv.desc, INVOKESTATIC, info.name(), mName + "$", staticDesc(info.name(), mv.desc), true); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/api/MixinCompiler.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.api; 2 | 3 | import codechicken.mixin.MixinCompilerImpl; 4 | import codechicken.mixin.util.ClassInfo; 5 | import codechicken.mixin.util.JavaTraitGenerator; 6 | import codechicken.mixin.util.MixinInfo; 7 | import org.jetbrains.annotations.Nullable; 8 | import org.objectweb.asm.tree.ClassNode; 9 | 10 | import java.util.Collection; 11 | import java.util.Set; 12 | 13 | /** 14 | * Defines a compiler capable of generating a composite Class, comprised of 15 | * a Set of Scala-like Trait classes on top of a base Class implementation. 16 | *

17 | * Created by covers1624 on 2/17/20. 18 | */ 19 | public interface MixinCompiler { 20 | 21 | /** 22 | * Create a {@link MixinCompiler} instance. 23 | * 24 | * @return The instance. 25 | */ 26 | static MixinCompiler create() { 27 | return new MixinCompilerImpl(); 28 | } 29 | 30 | /** 31 | * Create a {@link MixinCompiler} instance, with the given MixinBackend. 32 | * 33 | * @param backend The MixinBackend. 34 | * @return The instance. 35 | */ 36 | static MixinCompiler create(MixinBackend backend) { 37 | return new MixinCompilerImpl(backend); 38 | } 39 | 40 | /** 41 | * Create a {@link MixinCompiler} instance, with the 42 | * given {@link MixinBackend} and {@link MixinDebugger}. 43 | * 44 | * @param backend The MixinBackend. 45 | * @param debugger The MixinDebugger. 46 | * @return The instance. 47 | */ 48 | static MixinCompiler create(MixinBackend backend, MixinDebugger debugger) { 49 | return new MixinCompilerImpl(backend, debugger); 50 | } 51 | 52 | /** 53 | * Create a {@link MixinCompiler} instance, with the 54 | * given {@link MixinBackend}, {@link MixinDebugger} 55 | * and the specified Collection of {@link MixinLanguageSupport}s. 56 | * 57 | * @param backend The MixinBackend. 58 | * @param debugger The MixinDebugger. 59 | * @param supports The MixinLanguageSupport classes to load. 60 | * @return The instance. 61 | */ 62 | static MixinCompiler create(MixinBackend backend, MixinDebugger debugger, Collection> supports) { 63 | return new MixinCompilerImpl(backend, debugger, () -> supports); 64 | } 65 | 66 | /** 67 | * Gets the {@link MixinBackend} for this MixinCompiler 68 | * 69 | * @return The MixinBackend instance. 70 | */ 71 | MixinBackend getMixinBackend(); 72 | 73 | /** 74 | * Get a {@link MixinLanguageSupport} instance with the given name. 75 | * 76 | * @param name The name. 77 | * @return The {@link MixinLanguageSupport} instance or {@code null} 78 | */ 79 | @Nullable T getLanguageSupport(String name); 80 | 81 | /** 82 | * Gets a {@link ClassInfo} instance for the given class name. 83 | * 84 | * @param name The class name. 85 | * @return The ClassInfo 86 | */ 87 | @Nullable 88 | ClassInfo getClassInfo(@AsmName String name); 89 | 90 | /** 91 | * Overload for {@link #getClassInfo(String)}, taking a {@link ClassNode} instead. 92 | * 93 | * @param node The ClassNode. 94 | * @return The ClassInfo. 95 | */ 96 | @Nullable 97 | ClassInfo getClassInfo(ClassNode node); 98 | 99 | /** 100 | * Overload for {@link #getClassInfo(String)}, taking a {@link Class} instead. 101 | * 102 | * @param clazz The Class. 103 | * @return The ClassInfo. 104 | */ 105 | @Nullable 106 | default ClassInfo getClassInfo(@Nullable Class clazz) { 107 | return clazz == null ? null : getClassInfo(clazz.getName().replace(".", "/")); 108 | } 109 | 110 | /** 111 | * Loads a {@link ClassNode} for the given class name. 112 | * 113 | * @param name The Class name. 114 | * @return The ClassNode. 115 | */ 116 | @Nullable 117 | ClassNode getClassNode(@AsmName String name); 118 | 119 | /** 120 | * Registers a Trait to the {@link MixinCompiler}. 121 | * 122 | * @param cNode The ClassNode for the trait. 123 | * @return The MixinInfo for the trait. 124 | */ 125 | MixinInfo registerTrait(ClassNode cNode); 126 | 127 | /** 128 | * Gets a {@link MixinInfo} for the given Class name. 129 | * 130 | * @param name The Class name 131 | * @return The MixinInfo, Null if it does not exist. 132 | */ 133 | @Nullable 134 | MixinInfo getMixinInfo(@AsmName String name); 135 | 136 | /** 137 | * Defines a class. 138 | * 139 | * @param name The name for the class. 140 | * @param bytes The bytes for the class. 141 | * @return The defined class. 142 | */ 143 | Class defineClass(@AsmName String name, byte[] bytes); 144 | 145 | /** 146 | * Get a previously defined class. 147 | * 148 | * @param name The name of the previously defined class. 149 | * @return The defined class. 150 | * @throws NullPointerException If the class was not found. 151 | */ 152 | Class getDefinedClass(@AsmName @JavaName String name); 153 | 154 | /** 155 | * Compiles a new class with the given name, super Class, and traits. 156 | * 157 | * @param name The name for the class. 158 | * @param superClass The name for the super class.s 159 | * @param traits The Traits to mixin. 160 | * @return The compiled class. 161 | */ 162 | Class compileMixinClass(String name, String superClass, Set traits); 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/SimpleServiceLoader.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStreamReader; 5 | import java.io.LineNumberReader; 6 | import java.net.URL; 7 | import java.util.Collections; 8 | import java.util.HashSet; 9 | import java.util.ServiceConfigurationError; 10 | import java.util.Set; 11 | import java.util.function.Consumer; 12 | 13 | /** 14 | * Basically a re-implementation of ServiceLoader that does things a little differently. 15 | *

16 | * Services are only class loaded and not instantiated, useful for some applications. 17 | * {@link #poll()} must be called to actually load stuff, new services found directly 18 | * after that poll operation can be obtained via {@link #getNewServices()}, this set 19 | * is cleared each time {@link #poll()} is called. All found services can be retrieved 20 | * via {@link #getAllServices()}. 21 | *

22 | *

23 | * Created by covers1624 on 15/11/18. 24 | */ 25 | public class SimpleServiceLoader { 26 | 27 | private static final String PREFIX = "META-INF/services/"; 28 | 29 | private final Class serviceClazz; 30 | private final ClassLoader classLoader; 31 | 32 | private final Set foundClasses = new HashSet<>(); 33 | private final Set> foundServices = new HashSet<>(); 34 | private final Set> newServices = new HashSet<>(); 35 | 36 | public SimpleServiceLoader(Class serviceClazz) { 37 | this.serviceClazz = serviceClazz; 38 | this.classLoader = getContextClassLoader(); 39 | } 40 | 41 | public SimpleServiceLoader(Class serviceClazz, ClassLoader classLoader) { 42 | this.serviceClazz = serviceClazz; 43 | this.classLoader = classLoader; 44 | } 45 | 46 | @SuppressWarnings ("unchecked") 47 | public SimpleServiceLoader poll() { 48 | newServices.clear(); 49 | load(classLoader, serviceClazz, line -> { 50 | if (!foundClasses.contains(line)) { 51 | foundClasses.add(line); 52 | Class clazz = null; 53 | try { 54 | clazz = (Class) Class.forName(line, false, classLoader); 55 | } catch (ClassNotFoundException e) { 56 | fail(serviceClazz, "Provider " + line + " not found."); 57 | } 58 | if (!serviceClazz.isAssignableFrom(clazz)) { 59 | fail(serviceClazz, "Provider " + line + " not a subtype"); 60 | } 61 | foundServices.add(clazz); 62 | newServices.add(clazz); 63 | } 64 | }); 65 | return this; 66 | } 67 | 68 | public Set> getAllServices() { 69 | return Collections.unmodifiableSet(foundServices); 70 | } 71 | 72 | public Set> getNewServices() { 73 | return Collections.unmodifiableSet(newServices); 74 | } 75 | 76 | public static void load(Class clazz, Consumer cons) { 77 | load(getContextClassLoader(), clazz, cons); 78 | } 79 | 80 | public static void load(ClassLoader cl, Class clazz, Consumer cons) { 81 | try { 82 | for (URL url : Utils.toIterable(cl.getResources(PREFIX + clazz.getName()))) { 83 | try (LineNumberReader reader = new LineNumberReader(new InputStreamReader(url.openStream()))) { 84 | String line; 85 | while ((line = reader.readLine()) != null) { 86 | int lc = reader.getLineNumber(); 87 | int cIndex = line.indexOf('#'); 88 | if (cIndex >= 0) { 89 | line = line.substring(0, cIndex); 90 | } 91 | line = line.trim(); 92 | int n = line.length(); 93 | if (n != 0) { 94 | if (line.indexOf(' ') >= 0 || line.indexOf('\t') >= 0) { 95 | fail(clazz, url, lc, "Illegal configuration-file syntax"); 96 | } 97 | int cp = line.codePointAt(0); 98 | if (!Character.isJavaIdentifierStart(cp)) { 99 | fail(clazz, url, lc, "Illegal provider-class name: " + line); 100 | } 101 | for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { 102 | cp = line.codePointAt(i); 103 | if (!Character.isJavaIdentifierPart(cp) && cp != '.') { 104 | fail(clazz, url, lc, "Illegal provider-class name: " + line); 105 | } 106 | } 107 | cons.accept(line); 108 | } 109 | } 110 | } 111 | } 112 | 113 | } catch (IOException e) { 114 | fail(clazz, "Error reading configuration file", e); 115 | } 116 | } 117 | 118 | private static ClassLoader getContextClassLoader() { 119 | ClassLoader cl = Thread.currentThread().getContextClassLoader(); 120 | if (cl == null) { 121 | cl = SimpleServiceLoader.class.getClassLoader(); 122 | } 123 | return cl; 124 | } 125 | 126 | private static void fail(Class service, String msg, Throwable cause) throws ServiceConfigurationError { 127 | throw new ServiceConfigurationError(service.getName() + ": " + msg, cause); 128 | } 129 | 130 | private static void fail(Class service, String msg) throws ServiceConfigurationError { 131 | throw new ServiceConfigurationError(service.getName() + ": " + msg); 132 | } 133 | 134 | private static void fail(Class service, URL u, int line, String msg) throws ServiceConfigurationError { 135 | fail(service, u + ":" + line + ": " + msg); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/scala/MixinScalaLanguageSupport.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.scala; 2 | 3 | import codechicken.mixin.api.MixinCompiler; 4 | import codechicken.mixin.api.MixinLanguageSupport; 5 | import codechicken.mixin.scala.ScalaSignature.ClassSymbolRef; 6 | import codechicken.mixin.util.ClassInfo; 7 | import codechicken.mixin.util.FieldMixin; 8 | import codechicken.mixin.util.MixinInfo; 9 | import net.covers1624.quack.collection.FastStream; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.objectweb.asm.Type; 12 | import org.objectweb.asm.tree.ClassNode; 13 | import org.objectweb.asm.tree.MethodNode; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Set; 20 | 21 | import static codechicken.mixin.api.MixinLanguageSupport.LanguageName; 22 | import static org.objectweb.asm.Opcodes.ACC_PRIVATE; 23 | import static org.objectweb.asm.Opcodes.ACC_PUBLIC; 24 | 25 | /** 26 | * Created by covers1624 on 19/1/24. 27 | */ 28 | @LanguageName ("scala") 29 | public class MixinScalaLanguageSupport implements MixinLanguageSupport { 30 | 31 | private static final Logger LOGGER = LoggerFactory.getLogger(MixinScalaLanguageSupport.class); 32 | 33 | private final MixinCompiler mixinCompiler; 34 | 35 | public MixinScalaLanguageSupport(MixinCompiler mixinCompiler) { 36 | this.mixinCompiler = mixinCompiler; 37 | } 38 | 39 | @Override 40 | public ClassInfo obtainInfo(ClassNode cNode) { 41 | if (cNode.name.endsWith("$")) { 42 | String baseName = cNode.name.substring(0, cNode.name.length() - 1); 43 | ClassNode baseNode = mixinCompiler.getClassNode(baseName); 44 | if (baseNode != null) { 45 | ScalaClassInfo info = scalaInfo(baseNode, true); 46 | if (info != null) { 47 | return info; 48 | } 49 | } 50 | } 51 | 52 | return scalaInfo(cNode, false); 53 | } 54 | 55 | private @Nullable ScalaClassInfo scalaInfo(ClassNode cNode, boolean obj) { 56 | ScalaSignature sig = ScalaSignature.parse(cNode); 57 | if (sig == null) return null; 58 | 59 | String name = cNode.name.replace('/', '.'); 60 | ClassSymbolRef cSym = obj ? sig.findObject(name) : sig.findClass(name); 61 | if (cSym == null) return null; 62 | 63 | return new ScalaClassInfo(mixinCompiler, cNode, sig, cSym); 64 | } 65 | 66 | @Override 67 | public MixinInfo buildMixinTrait(ClassNode cNode) { 68 | if (!(mixinCompiler.getClassInfo(cNode) instanceof ScalaClassInfo info) || !info.isTrait()) return null; 69 | 70 | ScalaSignature sig = info.sig; 71 | Set filtered = listFiltered(sig); 72 | 73 | List parentTraits = getAndRegisterParentTraits(cNode); 74 | List fields = new ArrayList<>(); 75 | List methods = new ArrayList<>(); 76 | List supers = new ArrayList<>(); 77 | 78 | ClassSymbolRef cSym = info.cSym; 79 | for (ScalaSignature.MethodSymbol sym : sig.collect(8)) { 80 | LOGGER.debug(sym.toString()); 81 | if (sym.isParam() || !sym.owner().equals(cSym)) continue; 82 | if (filtered.contains(sym.full())) continue; 83 | 84 | if (sym.isAccessor()) { 85 | if (!sym.name().trim().endsWith("$eq")) { 86 | fields.add(new FieldMixin(sym.name().trim(), Type.getReturnType(sym.jDesc()).getDescriptor(), sym.isPrivate() ? ACC_PRIVATE : ACC_PUBLIC)); // TODO better 87 | } 88 | } else if (sym.isMethod()) { 89 | String desc = sym.jDesc(); 90 | if (sym.name().startsWith("super$")) { 91 | supers.add(sym.name().substring(6) + desc); 92 | } else if (!sym.isPrivate() && !sym.isDeferred() && !sym.name().equals("$init$")) { 93 | // also check if the return type is a scala object 94 | String objectDesc = Type.getMethodDescriptor( 95 | Type.getObjectType(Type.getReturnType(desc).getInternalName() + "$"), 96 | Type.getArgumentTypes(desc) 97 | ); 98 | MethodNode mNode = FastStream.of(cNode.methods) 99 | .filter(e -> e.name.equals(sym.name()) && (e.desc.equals(desc) || e.desc.equals(objectDesc))) 100 | .firstOrDefault(); 101 | if (mNode == null) { 102 | throw new IllegalArgumentException("Unable to add mixin trait " + cNode.name + ": " + sym.name() + desc + " found in scala signature but not in class file. Most likely an obfuscation issue."); 103 | } 104 | methods.add(mNode); 105 | } 106 | } 107 | } 108 | 109 | return new MixinInfo(cNode.name, cSym.jParent(), parentTraits, fields, methods, supers); 110 | } 111 | 112 | private List getAndRegisterParentTraits(ClassNode cNode) { 113 | return FastStream.of(cNode.interfaces) 114 | .map(mixinCompiler::getClassInfo) 115 | .filter(e -> e instanceof ScalaClassInfo info && info.isTrait() && !info.cSym.isInterface()) 116 | .map(e -> mixinCompiler.registerTrait(((ScalaClassInfo) e).getCNode())) 117 | .toList(); 118 | } 119 | 120 | private Set listFiltered(ScalaSignature sig) { 121 | return FastStream.of(sig.collect(40)) 122 | .filter(e -> { 123 | ScalaSignature.Literal value = e.getValue("value"); 124 | return value instanceof ScalaSignature.EnumLiteral lit 125 | && mixinCompiler.getMixinBackend().filterMethodAnnotations(e.annType().name(), lit.value().full()); 126 | }) 127 | .map(e -> e.owner().full()) 128 | .toSet(); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/scala/ByteCodecs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Scala (https://www.scala-lang.org) 3 | * 4 | * Copyright EPFL and Lightbend, Inc. 5 | * 6 | * Licensed under Apache License 2.0 7 | * (http://www.apache.org/licenses/LICENSE-2.0). 8 | * 9 | * See the NOTICE file distributed with this work for 10 | * additional information regarding copyright ownership. 11 | */ 12 | package codechicken.mixin.scala; 13 | 14 | /** 15 | * This class is borrowed from scala-reflect found at {@code scala.reflect.internal.pickling.ByteCodecs}. 16 | *

17 | * It is unmodified, except for its conversion from Scala to Java and minor syntactic cleanups. 18 | *

19 | * Created by covers1624 on 18/1/24. 20 | */ 21 | public class ByteCodecs { 22 | 23 | /** 24 | * Increment each element by 1, then map 0x00 to 0xC0 0x80. Returns a fresh array. 25 | */ 26 | public static byte[] avoidZero(byte[] src) { 27 | int srcLen = src.length; 28 | int count = 0; 29 | for (byte b : src) { 30 | if (b == 0x7F) { 31 | count++; 32 | } 33 | } 34 | byte[] dst = new byte[srcLen + count]; 35 | int j = 0; 36 | for (byte in : src) { 37 | if (in == 0x7F) { 38 | dst[j++] = (byte) 0xC0; 39 | dst[j++] = (byte) 0x80; 40 | } else { 41 | dst[j++] = (byte) (in + 1); 42 | } 43 | } 44 | return dst; 45 | } 46 | 47 | /** 48 | * Map 0xC0 0x80 to 0x00, then subtract 1 from each element. In-place. 49 | */ 50 | public static int regenerateZero(byte[] src) { 51 | int srcLen = src.length; 52 | int j = 0; 53 | for (int i = 0; i < srcLen; i++, j++) { 54 | int in = src[i] & 0xFF; 55 | if (in == 0xC0 && (src[i + 1] & 0xFF) == 0x80) { 56 | src[j] = 0x7F; 57 | i++; 58 | } else if (in == 0) { 59 | src[j] = 0x7F; 60 | } else { 61 | src[j] = (byte) (in - 1); 62 | } 63 | } 64 | return j; 65 | } 66 | 67 | /** 68 | * Returns a new array 69 | */ 70 | public static byte[] encode8to7(byte[] src) { 71 | int srclen = src.length; 72 | int dstlen = (srclen * 8 + 6) / 7; 73 | byte[] dst = new byte[dstlen]; 74 | int i = 0; 75 | int j = 0; 76 | while (i + 6 < srclen) { 77 | int in = src[i] & 0xFF; 78 | dst[j] = (byte) (in & 0x7F); 79 | int out = in >>> 7; 80 | in = src[i + 1] & 0xFF; 81 | dst[j + 1] = (byte) (out | (in << 1) & 0x7F); 82 | out = in >>> 6; 83 | in = src[i + 2] & 0xFF; 84 | dst[j + 2] = (byte) (out | (in << 2) & 0x7F); 85 | out = in >>> 5; 86 | in = src[i + 3] & 0xFF; 87 | dst[j + 3] = (byte) (out | (in << 3) & 0x7F); 88 | out = in >>> 4; 89 | in = src[i + 4] & 0xFF; 90 | dst[j + 4] = (byte) (out | (in << 4) & 0x7F); 91 | out = in >>> 3; 92 | in = src[i + 5] & 0xFF; 93 | dst[j + 5] = (byte) (out | (in << 5) & 0x7F); 94 | out = in >>> 2; 95 | in = src[i + 6] & 0xFF; 96 | dst[j + 6] = (byte) (out | (in << 6) & 0x7F); 97 | out = in >>> 1; 98 | dst[j + 7] = (byte) out; 99 | i += 7; 100 | j += 8; 101 | } 102 | if (i < srclen) { 103 | int in = src[i] & 0xFF; 104 | dst[j] = (byte) (in & 0x7F); 105 | j += 1; 106 | int out = in >>> 7; 107 | if (i + 1 < srclen) { 108 | in = src[i + 1] & 0xFF; 109 | dst[j] = (byte) (out | (in << 1) & 0x7F); 110 | j += 1; 111 | out = in >>> 6; 112 | if (i + 2 < srclen) { 113 | in = src[i + 2] & 0xFF; 114 | dst[j] = (byte) (out | (in << 2) & 0x7F); 115 | j += 1; 116 | out = in >>> 5; 117 | if (i + 3 < srclen) { 118 | in = src[i + 3] & 0xFF; 119 | dst[j] = (byte) (out | (in << 3) & 0x7F); 120 | j += 1; 121 | out = in >>> 4; 122 | if (i + 4 < srclen) { 123 | in = src[i + 4] & 0xFF; 124 | dst[j] = (byte) (out | (in << 4) & 0x7F); 125 | j += 1; 126 | out = in >>> 3; 127 | if (i + 5 < srclen) { 128 | in = src[i + 5] & 0xFF; 129 | dst[j] = (byte) (out | (in << 5) & 0x7F); 130 | j += 1; 131 | out = in >>> 2; 132 | } 133 | } 134 | } 135 | } 136 | } 137 | if (j < dstlen) { 138 | dst[j] = (byte) out; 139 | } 140 | } 141 | return dst; 142 | } 143 | 144 | /** 145 | * In-place 146 | */ 147 | public static int decode7to8(byte[] src, int srclen) { 148 | int i = 0; 149 | int j = 0; 150 | int dstlen = (srclen * 7 + 7) / 8; 151 | while (i + 7 < srclen) { 152 | int out = src[i]; 153 | byte in = src[i + 1]; 154 | src[j] = (byte) (out | (in & 0x01) << 7); 155 | out = in >>> 1; 156 | in = src[i + 2]; 157 | src[j + 1] = (byte) (out | (in & 0x03) << 6); 158 | out = in >>> 2; 159 | in = src[i + 3]; 160 | src[j + 2] = (byte) (out | (in & 0x07) << 5); 161 | out = in >>> 3; 162 | in = src[i + 4]; 163 | src[j + 3] = (byte) (out | (in & 0x0f) << 4); 164 | out = in >>> 4; 165 | in = src[i + 5]; 166 | src[j + 4] = (byte) (out | (in & 0x1f) << 3); 167 | out = in >>> 5; 168 | in = src[i + 6]; 169 | src[j + 5] = (byte) (out | (in & 0x3f) << 2); 170 | out = in >>> 6; 171 | in = src[i + 7]; 172 | src[j + 6] = (byte) (out | in << 1); 173 | i += 8; 174 | j += 7; 175 | } 176 | if (i < srclen) { 177 | int out = src[i]; 178 | if (i + 1 < srclen) { 179 | byte in = src[i + 1]; 180 | src[j] = (byte) (out | (in & 0x01) << 7); 181 | j += 1; 182 | out = in >>> 1; 183 | if (i + 2 < srclen) { 184 | in = src[i + 2]; 185 | src[j] = (byte) (out | (in & 0x03) << 6); 186 | j += 1; 187 | out = in >>> 2; 188 | if (i + 3 < srclen) { 189 | in = src[i + 3]; 190 | src[j] = (byte) (out | (in & 0x07) << 5); 191 | j += 1; 192 | out = in >>> 3; 193 | if (i + 4 < srclen) { 194 | in = src[i + 4]; 195 | src[j] = (byte) (out | (in & 0x0f) << 4); 196 | j += 1; 197 | out = in >>> 4; 198 | if (i + 5 < srclen) { 199 | in = src[i + 5]; 200 | src[j] = (byte) (out | (in & 0x1f) << 3); 201 | j += 1; 202 | out = in >>> 5; 203 | if (i + 6 < srclen) { 204 | in = src[i + 6]; 205 | src[j] = (byte) (out | (in & 0x3f) << 2); 206 | j += 1; 207 | out = in >>> 6; 208 | } 209 | } 210 | } 211 | } 212 | } 213 | } 214 | if (j < dstlen) { 215 | src[j] = (byte) out; 216 | } 217 | } 218 | return dstlen; 219 | } 220 | 221 | public static byte[] encode(byte[] xs) { 222 | return avoidZero(encode8to7(xs)); 223 | } 224 | 225 | /** 226 | * Destructively decodes array xs and returns the length of the decoded array. 227 | *

228 | * Sometimes returns (length+1) of the decoded array. Example: 229 | *

230 | * scala> val enc = scala.reflect.internal.pickling.ByteCodecs.encode(Array(1,2,3)) 231 | * enc: Array[Byte] = Array(2, 5, 13, 1) 232 | *

233 | * scala> scala.reflect.internal.pickling.ByteCodecs.decode(enc) 234 | * res43: Int = 4 235 | *

236 | * scala> enc 237 | * res44: Array[Byte] = Array(1, 2, 3, 0) 238 | *

239 | * However, this does not always happen. 240 | */ 241 | public static int decode(byte[] xs) { 242 | int len = regenerateZero(xs); 243 | return decode7to8(xs, len); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/scala/ScalaSignature.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.scala; 2 | 3 | import net.covers1624.quack.collection.FastStream; 4 | import net.covers1624.quack.util.SneakyUtils; 5 | import org.jetbrains.annotations.Nullable; 6 | import org.objectweb.asm.tree.ClassNode; 7 | 8 | import java.util.*; 9 | import java.util.function.Supplier; 10 | 11 | /** 12 | * Created by covers1624 on 18/1/24. 13 | */ 14 | public class ScalaSignature { 15 | 16 | public final int major; 17 | public final int minor; 18 | public final SigEntry[] table; 19 | 20 | public ScalaSignature(String sig) { 21 | this(parseBytes(sig)); 22 | } 23 | 24 | private ScalaSignature(Bytes bytes) { 25 | Reader reader = bytes.reader(); 26 | major = reader.readByte(); 27 | minor = reader.readByte(); 28 | table = new SigEntry[reader.readNat()]; 29 | for (int i = 0; i < table.length; i++) { 30 | int index = i; 31 | int start = reader.pos; 32 | int tpe = reader.readByte(); 33 | int len = reader.readNat(); 34 | table[index] = reader.advance(len, () -> new SigEntry(index, start, new Bytes(bytes.arr, reader.pos, len))); 35 | } 36 | } 37 | 38 | private static Bytes parseBytes(String str) { 39 | byte[] bytes = str.getBytes(); 40 | int len = ByteCodecs.decode(bytes); 41 | return new Bytes(bytes, 0, len); 42 | } 43 | 44 | public static @Nullable ScalaSignature parse(ClassNode cNode) { 45 | if (cNode.visibleAnnotations == null) return null; 46 | 47 | return FastStream.of(cNode.visibleAnnotations) 48 | .filter(e -> e.desc.equals("Lscala/reflect/ScalaSignature;")) 49 | .map(e -> e.values.get(1).toString()) 50 | .map(ScalaSignature::new) 51 | .firstOrDefault(); 52 | } 53 | 54 | public List collect(int id) { 55 | List list = new ArrayList<>(); 56 | for (int i = 0; i < table.length; i++) { 57 | if (table[i].id() == id) { 58 | list.add(evalT(i)); 59 | } 60 | } 61 | return list; 62 | } 63 | 64 | public @Nullable ObjectSymbol findObject(String name) { 65 | return FastStream.of(this.collect(7)).filter(e -> e.full().equals(name)).first(); 66 | } 67 | 68 | public @Nullable ClassSymbol findClass(String name) { 69 | return FastStream.of(this.collect(6)).filter(e -> !e.isModule() && e.full().equals(name)).first(); 70 | } 71 | 72 | public String evalS(int i) { 73 | SigEntry e = table[i]; 74 | Bytes bc = e.bytes; 75 | Reader bcr = bc.reader(); 76 | return switch (e.id()) { 77 | case 1, 2 -> bcr.readString(bc.len); 78 | case 3 -> ""; 79 | case 9, 10 -> { 80 | String s = evalS(bcr.readNat()); 81 | if (bc.pos + bc.len > bcr.pos) { 82 | s = evalS(bcr.readNat()) + "." + s; 83 | } 84 | yield s; 85 | } 86 | default -> throw new RuntimeException("Switch falloff"); 87 | }; 88 | } 89 | 90 | public T evalT(int i) { 91 | return SneakyUtils.unsafeCast(eval(i)); 92 | } 93 | 94 | private List evalList(Reader reader) { 95 | List list = new ArrayList<>(); 96 | while (reader.more()) { 97 | list.add(evalT(reader.readNat())); 98 | } 99 | return list; 100 | } 101 | 102 | public Object eval(int i) { 103 | SigEntry e = table[i]; 104 | Reader bcr = e.bytes.reader(); 105 | return switch (e.id()) { 106 | case 1, 2 -> evalS(i); 107 | case 3 -> new NoSymbol(); 108 | case 6 -> new ClassSymbol(this, evalS(bcr.readNat()), evalT(bcr.readNat()), bcr.readNat(), bcr.readNat()); 109 | case 7 -> new ObjectSymbol(this, evalS(bcr.readNat()), evalT(bcr.readNat()), bcr.readNat(), bcr.readNat()); 110 | case 8 -> new MethodSymbol(this, evalS(bcr.readNat()), evalT(bcr.readNat()), bcr.readNat(), bcr.readNat()); 111 | case 9, 10 -> new ExternalSymbol(evalS(i)); 112 | case 11, 12 -> new NoType(); // 12 is actually NoPrefixType (no lower bound) 113 | case 13 -> new ThisType(evalT(bcr.readNat())); 114 | case 14 -> new SingleType(evalT(bcr.readNat()), evalT(bcr.readNat())); 115 | case 16 -> new TypeRefType(evalT(bcr.readNat()), evalT(bcr.readNat()), evalList(bcr)); 116 | case 19 -> new ClassType(evalT(bcr.readNat()), evalList(bcr)); 117 | case 20 -> new MethodType(evalT(bcr.readNat()), evalList(bcr)); 118 | case 21, 48 -> new ParameterlessType(evalT(bcr.readNat())); 119 | case 25 -> new BooleanLiteral(bcr.readLong() != 0); 120 | case 26 -> new ByteLiteral((byte) bcr.readLong()); 121 | case 27 -> new ShortLiteral((short) bcr.readLong()); 122 | case 28 -> new CharLiteral((char) bcr.readLong()); 123 | case 29 -> new IntLiteral((int) bcr.readLong()); 124 | case 30 -> new LongLiteral(bcr.readLong()); 125 | case 31 -> new FloatLiteral(Float.intBitsToFloat((int) bcr.readLong())); 126 | case 32 -> new DoubleLiteral(Double.longBitsToDouble(bcr.readLong())); 127 | case 33 -> new StringLiteral(evalS(bcr.readNat())); 128 | case 34 -> new NullLiteral(); 129 | case 35 -> new TypeLiteral(evalT(bcr.readNat())); 130 | case 36 -> new EnumLiteral(evalT(bcr.readNat())); 131 | case 40 -> new AnnotationInfo(evalT(bcr.readNat()), evalT(bcr.readNat()), arrayElements(evalList(bcr))); 132 | case 44 -> new ArrayLiteral(evalList(bcr)); 133 | default -> e; 134 | }; 135 | } 136 | 137 | private static Map arrayElements(List list) { 138 | Map elements = new HashMap<>(); 139 | for (int i = 0; i < list.size(); i += 2) { 140 | elements.put((String) list.get(i), (Literal) list.get(i + 1)); 141 | } 142 | return elements; 143 | } 144 | 145 | // @formatter:off 146 | public record SigEntry(int index, int start, Bytes bytes) { 147 | public byte id() { return bytes.arr[start]; } 148 | @Override public String toString() { return "SigEntry(" + index + ", " + id() + ", " + bytes.len + " bytes)"; } 149 | } 150 | public interface Flags { 151 | boolean hasFlag(int flag); 152 | default boolean isPrivate() { return hasFlag(0x00000004); } 153 | default boolean isProtected() { return hasFlag(0x00000008); } 154 | default boolean isAbstract() { return hasFlag(0x00000080); } 155 | default boolean isDeferred() { return hasFlag(0x00000100); } 156 | default boolean isMethod() { return hasFlag(0x00000200); } 157 | default boolean isModule() { return hasFlag(0x00000400); } 158 | default boolean isInterface() { return hasFlag(0x00000800); } 159 | default boolean isParam() { return hasFlag(0x00002000); } 160 | default boolean isStatic() { return hasFlag(0x00800000); } 161 | default boolean isTrait() { return hasFlag(0x02000000); } 162 | default boolean isAccessor() { return hasFlag(0x08000000); } 163 | } 164 | public interface SymbolRef extends Flags { 165 | String full(); 166 | int flags(); 167 | @Override default boolean hasFlag(int flag) { return (flags() & flag) != 0; } 168 | } 169 | public interface ClassSymbolRef extends SymbolRef { 170 | ScalaSignature sig(); 171 | String name(); 172 | SymbolRef owner(); 173 | int flags(); 174 | int infoId(); 175 | default String full() { return owner().full() + "." + name(); } 176 | default boolean isObject() { return false; } 177 | default ClassType info() { return sig().evalT(infoId() ); } 178 | default String jParent() { return info().parent().jName(); } 179 | default List jInterfaces() { return FastStream.of(info().interfaces()).map(TypeRef::jName).toList(); } 180 | } 181 | public record ClassSymbol(ScalaSignature sig, String name, SymbolRef owner, int flags, int infoId) implements ClassSymbolRef { 182 | @Override public String toString() { return "ClassSymbol(" + name + ", " + owner + ", 0x" + Integer.toHexString(flags) + ", " + infoId + ")"; } 183 | } 184 | public record ObjectSymbol(ScalaSignature sig, String name, SymbolRef owner, int flags, int infoId) implements ClassSymbolRef { 185 | @Override public boolean isObject() { return true; } 186 | @Override public ClassType info() { return ((ClassSymbol) sig.evalT(infoId).sym).info(); } 187 | @Override public String toString() { return "ObjectSymbol(" + name + ", " + owner + ", 0x" + Integer.toHexString(flags) + ", " + infoId + ")"; } 188 | } 189 | public record MethodSymbol(ScalaSignature sig, String name, SymbolRef owner, int flags, int infoId) implements SymbolRef { 190 | @Override public String full() { return owner.full() + "." + name; } 191 | public TMethodType info() { return sig.evalT(infoId ); } 192 | public String jDesc() { return info().jDesc(); } 193 | @Override public String toString() { return "MethodSymbol(" + name + ", " + owner + ", 0x" + Integer.toHexString(flags) + ", " + infoId + ")"; } 194 | } 195 | public record ExternalSymbol(String name) implements SymbolRef { 196 | @Override public String full() { return name; } 197 | @Override public int flags() { return 0; } 198 | } 199 | private record NoSymbol() implements SymbolRef { 200 | @Override public String full() { return ""; } 201 | @Override public int flags() { return 0; } 202 | } 203 | public interface TMethodType { 204 | default String jDesc() { return "(" + FastStream.of(params()).map(e -> e.info().returnType().jDesc()).join("") + ")" + returnType().jDesc(); } 205 | TypeRef returnType(); 206 | List params(); 207 | } 208 | public record ClassType(SymbolRef owner, List parents) { 209 | public TypeRef parent() { return parents.get(0); } 210 | public List interfaces() { return parents.subList(1, parents.size()); } 211 | } 212 | public record MethodType(TypeRef returnType, List params) implements TMethodType { } 213 | public record ParameterlessType(TypeRef returnType) implements TMethodType { 214 | @Override public List params() { return List.of(); } 215 | } 216 | public interface TypeRef { 217 | SymbolRef sym(); 218 | default String name() { return sym().full(); } 219 | default String jName() { 220 | String e = name().replace('.', '/'); 221 | return switch (e) { 222 | case "scala/AnyRef", "scala/Any" -> "java/lang/Object"; 223 | case "scala/Predef/String" -> "java/lang/String"; 224 | default -> e; 225 | }; 226 | } 227 | default String jDesc() { 228 | return switch (name()) { 229 | case "scala.Array" -> null; 230 | case "scala.Long" -> "J"; 231 | case "scala.Int" -> "I"; 232 | case "scala.Short" -> "S"; 233 | case "scala.Byte" -> "B"; 234 | case "scala.Double" -> "D"; 235 | case "scala.Float" -> "F"; 236 | case "scala.Boolean" -> "Z"; 237 | case "scala.Unit" -> "V"; 238 | default -> "L" + jName() + ";"; 239 | }; 240 | } 241 | } 242 | public record TypeRefType(TypeRef owner, SymbolRef sym, List typArgs) implements TMethodType, TypeRef { 243 | @Override public List params() { return List.of(); } 244 | @Override 245 | public TypeRef returnType() { return this; } 246 | @Override 247 | public String jDesc() { 248 | if (name().equals("scala.Array")) return "[" + typArgs.get(0).jDesc(); 249 | return TypeRef.super.jDesc(); 250 | } 251 | } 252 | public record ThisType(SymbolRef sym) implements TypeRef { } 253 | public record SingleType(TypeRef owner, SymbolRef sym) implements TypeRef { 254 | @Override public String jName() { return TypeRef.super.jName() + "$"; } 255 | } 256 | public record NoType() implements TypeRef { 257 | @Override public SymbolRef sym() { return null; } 258 | @Override public String name() { return ""; } 259 | } 260 | public interface Literal { 261 | Object value(); 262 | } 263 | public record BooleanLiteral(Boolean value) implements Literal { } 264 | public record ByteLiteral(Byte value) implements Literal { } 265 | public record ShortLiteral(Short value) implements Literal { } 266 | public record CharLiteral(Character value) implements Literal { } 267 | public record IntLiteral(Integer value) implements Literal { } 268 | public record LongLiteral(Long value) implements Literal { } 269 | public record FloatLiteral(Float value) implements Literal { } 270 | public record DoubleLiteral(Double value) implements Literal { } 271 | public record NullLiteral() implements Literal { 272 | @Override public Object value() { return null; } 273 | } 274 | public record StringLiteral(String value) implements Literal { } 275 | public record TypeLiteral(TypeRef value) implements Literal { } 276 | public record EnumLiteral(ExternalSymbol value) implements Literal { } 277 | public record ArrayLiteral(List value) implements Literal { } 278 | public record AnnotationInfo(SymbolRef owner, TypeRef annType, Map values) { 279 | public T getValue(String name) { 280 | return SneakyUtils.unsafeCast(values.get(name)); 281 | } 282 | } 283 | // @formatter:on 284 | 285 | private static final class Reader { 286 | 287 | private final Bytes bc; 288 | public int pos; 289 | 290 | private Reader(Bytes bc) { 291 | this.bc = bc; 292 | pos = bc.pos; 293 | } 294 | 295 | public boolean more() { 296 | return pos < bc.pos + bc.len; 297 | } 298 | 299 | public String readString(int len) { 300 | return advance(len, () -> new String(Arrays.copyOfRange(bc.arr, pos, pos + len))); 301 | } 302 | 303 | public byte readByte() { 304 | readCheck(1); 305 | return bc.arr[pos++]; 306 | } 307 | 308 | public int readNat() { 309 | var r = 0; 310 | var b = 0; 311 | do { 312 | b = readByte(); 313 | r = r << 7 | b & 0x7F; 314 | } 315 | while ((b & 0x80) != 0); 316 | return r; 317 | } 318 | 319 | public long readLong() { 320 | var l = 0L; 321 | while (more()) { 322 | l <<= 8; 323 | l |= readByte() & 0xFF; 324 | } 325 | return l; 326 | } 327 | 328 | public T advance(int len, Supplier f) { 329 | readCheck(len); 330 | T t = f.get(); 331 | pos += len; 332 | return t; 333 | } 334 | 335 | private void readCheck(int len) { 336 | if (pos + len > bc.pos + bc.len) { 337 | throw new IllegalArgumentException("Ran off the end of bytecode"); 338 | } 339 | } 340 | } 341 | 342 | private record Bytes(byte[] arr, int pos, int len) { 343 | 344 | public Reader reader() { 345 | return new Reader(this); 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/util/JavaTraitGenerator.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin.util; 2 | 3 | import codechicken.asm.*; 4 | import codechicken.mixin.api.MixinCompiler; 5 | import net.covers1624.quack.collection.ColUtils; 6 | import net.covers1624.quack.collection.FastStream; 7 | import org.jetbrains.annotations.Nullable; 8 | import org.objectweb.asm.Handle; 9 | import org.objectweb.asm.MethodVisitor; 10 | import org.objectweb.asm.Type; 11 | import org.objectweb.asm.tree.*; 12 | 13 | import java.util.*; 14 | import java.util.function.Function; 15 | 16 | import static org.objectweb.asm.Opcodes.*; 17 | 18 | /** 19 | * Handles taking a java class, and turning it into a Scala-like trait interface. 20 | *

21 | * Created by covers1624 on 2/11/20. 22 | */ 23 | public class JavaTraitGenerator { 24 | 25 | protected final MixinCompiler mixinCompiler; 26 | protected final ClassNode cNode; 27 | protected final ClassNode sNode; 28 | protected final ClassNode tNode; 29 | 30 | protected List staticFields; 31 | 32 | protected List instanceFields; 33 | 34 | protected List traitFields; 35 | protected List traitMethods = new ArrayList<>(); 36 | protected Set supers = new LinkedHashSet<>(); 37 | protected MixinInfo mixinInfo; 38 | 39 | protected Map fieldNameLookup; 40 | protected Map methodSigLookup; 41 | 42 | public JavaTraitGenerator(MixinCompiler mixinCompiler, ClassNode cNode) { 43 | this.mixinCompiler = mixinCompiler; 44 | this.cNode = cNode; 45 | tNode = new ClassNode(); 46 | sNode = new ClassNode(); 47 | checkNode(); 48 | staticFields = FastStream.of(cNode.fields) 49 | .filter(e -> (e.access & ACC_STATIC) != 0) 50 | .toList(); 51 | 52 | instanceFields = FastStream.of(cNode.fields) 53 | .filter(e -> (e.access & ACC_STATIC) == 0) 54 | .toList(); 55 | 56 | traitFields = FastStream.of(instanceFields) 57 | .map(f -> new FieldMixin(f.name, f.desc, f.access)) 58 | .toList(); 59 | 60 | fieldNameLookup = FastStream.of(traitFields) 61 | .toMap(FieldMixin::name, e -> e.getAccessName(cNode.name)); 62 | 63 | methodSigLookup = FastStream.of(cNode.methods) 64 | .toMap(e -> e.name + e.desc, Function.identity()); 65 | mixinInfo = operate(); 66 | } 67 | 68 | protected void checkNode() { 69 | preCheckNode(); 70 | if ((cNode.access & ACC_INTERFACE) != 0) { 71 | throw new IllegalArgumentException("Cannot register java interface '" + cNode.name + "' as a mixin trait."); 72 | } 73 | if (!cNode.innerClasses.isEmpty() && !ColUtils.anyMatch(cNode.innerClasses, innerNode -> innerNode.outerName != null && !cNode.name.equals(innerNode.outerName) && !innerNode.name.startsWith(cNode.name))) { 74 | throw new IllegalArgumentException("Found illegal inner class for '" + cNode.name + "', use scala."); 75 | } 76 | List invalidFields = FastStream.of(cNode.fields) 77 | .filter(e -> (e.access & ACC_PRIVATE) == 0) 78 | .toList(); 79 | if (!invalidFields.isEmpty()) { 80 | String fields = "[" + FastStream.of(invalidFields) 81 | .map(e -> e.name) 82 | .join(", ") + "]"; 83 | throw new IllegalArgumentException("Illegal fields " + fields + " found in " + cNode.name + ". These fields must be private."); 84 | } 85 | 86 | if ((cNode.access & ACC_ABSTRACT) != 0) { 87 | throw new IllegalArgumentException("Cannot register abstract class " + cNode.name + " as a java mixin trait. Use scala"); 88 | } 89 | } 90 | 91 | protected MixinInfo operate() { 92 | beforeTransform(); 93 | 94 | sNode.visit(V1_8, ACC_PUBLIC, cNode.name + "$", null, "java/lang/Object", new String[0]); 95 | sNode.sourceFile = cNode.sourceFile; 96 | 97 | tNode.visit(V1_8, ACC_INTERFACE | ACC_ABSTRACT | ACC_PUBLIC, cNode.name, null, "java/lang/Object", cNode.interfaces.toArray(new String[0])); 98 | tNode.sourceFile = cNode.sourceFile; 99 | 100 | //Clone static fields to StaticNode 101 | staticFields.forEach(f -> sNode.visitField(ACC_STATIC, f.name, f.desc, f.signature, f.value)); 102 | 103 | traitFields.forEach(f -> { 104 | tNode.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, fieldNameLookup.get(f.name()), "()" + f.desc(), null, null); 105 | tNode.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, fieldNameLookup.get(f.name()) + "_$eq", "(" + f.desc() + ")V", null, null); 106 | }); 107 | cNode.methods.forEach(this::convertMethod); 108 | return new MixinInfo(tNode.name, cNode.superName, Collections.emptyList(), traitFields, traitMethods, List.copyOf(supers)); 109 | } 110 | 111 | protected void preCheckNode() { 112 | } 113 | 114 | protected void beforeTransform() { 115 | } 116 | 117 | public @Nullable ClassNode getStaticNode() { 118 | if (sNode.methods.isEmpty() && sNode.fields.isEmpty()) { 119 | return null; // Pointless. 120 | } 121 | return sNode; 122 | } 123 | 124 | public ClassNode getTraitNode() { 125 | return tNode; 126 | } 127 | 128 | public MixinInfo getMixinInfo() { 129 | return mixinInfo; 130 | } 131 | 132 | private void staticTransform(MethodNode mNode, MethodNode base) { 133 | StackAnalyser stack = new StackAnalyser(Type.getObjectType(cNode.name), base); 134 | InsnList insnList = mNode.instructions; 135 | InsnPointer pointer = new InsnPointer(insnList); 136 | 137 | AbstractInsnNode insn; 138 | while ((insn = pointer.get()) != null) { 139 | if (insn instanceof FieldInsnNode fInsn) { 140 | if (fInsn.owner.equals(cNode.name)) { 141 | if (insn.getOpcode() == GETFIELD) { 142 | pointer.replace(new MethodInsnNode(INVOKEINTERFACE, cNode.name, fieldNameLookup.get(fInsn.name), "()" + fInsn.desc, true)); 143 | } else if (insn.getOpcode() == PUTFIELD) { 144 | pointer.replace(new MethodInsnNode(INVOKEINTERFACE, cNode.name, fieldNameLookup.get(fInsn.name) + "_$eq", "(" + fInsn.desc + ")V", true)); 145 | } else if (insn.getOpcode() == GETSTATIC || insn.getOpcode() == PUTSTATIC) { 146 | fInsn.owner = fInsn.owner + "$"; 147 | } 148 | } 149 | } else if (insn instanceof MethodInsnNode) { 150 | MethodInsnNode mInsn = (MethodInsnNode) insn; 151 | if (mInsn.getOpcode() == INVOKESPECIAL) { 152 | MethodInfo sMethod = getSuper(mInsn, stack); 153 | if (sMethod != null) { 154 | String bridgeName = cNode.name.replace("/", "$") + "$$super$" + mInsn.name; 155 | if (supers.add(mInsn.name + mInsn.desc)) { 156 | tNode.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, bridgeName, mInsn.desc, null, null); 157 | } 158 | pointer.replace(new MethodInsnNode(INVOKEINTERFACE, cNode.name, bridgeName, mInsn.desc, true)); 159 | } else { 160 | MethodNode target = methodSigLookup.get(mInsn.name + mInsn.desc); 161 | if (target != null) { 162 | if ((target.access & ACC_PRIVATE) != 0) { 163 | //Call the static method impl 164 | pointer.replace(new MethodInsnNode(INVOKESTATIC, mInsn.owner, mInsn.name, Utils.staticDesc(mInsn.owner, mInsn.desc), true)); 165 | } 166 | } 167 | } 168 | } else if (mInsn.getOpcode() == INVOKEVIRTUAL) { 169 | if (mInsn.owner.equals(cNode.name)) { 170 | MethodNode target = methodSigLookup.get(mInsn.name + mInsn.desc); 171 | if (target != null) { 172 | if ((target.access & ACC_PRIVATE) != 0) { 173 | // call the static impl method 174 | pointer.replace(new MethodInsnNode(INVOKESTATIC, mInsn.owner, mInsn.name, Utils.staticDesc(mInsn.owner, mInsn.desc), true)); 175 | } else { 176 | // call the interface method 177 | pointer.replace(new MethodInsnNode(INVOKEINTERFACE, mInsn.owner, mInsn.name, mInsn.desc, true)); 178 | } 179 | } else { 180 | //cast to parent class and call 181 | Type mType = Type.getMethodType(mInsn.desc); 182 | StackAnalyser.StackEntry instanceEntry = stack.peek(StackAnalyser.width(mType.getArgumentTypes())); 183 | insnList.insert(instanceEntry.insn, new TypeInsnNode(CHECKCAST, cNode.superName)); 184 | mInsn.owner = cNode.superName; 185 | } 186 | } 187 | //Ensure we cast when we call methods that take our parent's type. 188 | List entries = peekArgs(stack, mInsn.desc); 189 | Type[] argumentTypes = Type.getMethodType(mInsn.desc).getArgumentTypes(); 190 | for (int i = 0; i < argumentTypes.length; i++) { 191 | Type arg = argumentTypes[i]; 192 | StackAnalyser.StackEntry entry = entries.get(i); 193 | if (!arg.getInternalName().equals("java/lang/Object") && ClassHierarchyManager.classExtends(cNode.superName.replace("/", "."), arg.getInternalName().replace("/", "."))) { 194 | insnList.insert(entry.insn, new TypeInsnNode(CHECKCAST, cNode.superName)); 195 | } 196 | } 197 | } else if (mInsn.getOpcode() == INVOKESTATIC) { 198 | if (mInsn.owner.equals(cNode.name) && methodSigLookup.get(mInsn.name + mInsn.desc) != null) { 199 | mInsn.owner = mInsn.owner + "$"; 200 | } 201 | } 202 | } else if (insn instanceof InvokeDynamicInsnNode) { 203 | InvokeDynamicInsnNode mInsn = (InvokeDynamicInsnNode) insn; 204 | Object[] bsmArgs = mInsn.bsmArgs; 205 | for (int i = 0; i < bsmArgs.length; i++) { 206 | Object bsmArg = bsmArgs[i]; 207 | if (!(bsmArg instanceof Handle handle)) continue; 208 | if (handle.getOwner().equals(cNode.name) && handle.getTag() == H_INVOKESTATIC) { 209 | bsmArgs[i] = new Handle(handle.getTag(), handle.getOwner() + "$", handle.getName(), handle.getDesc(), handle.isInterface()); 210 | } 211 | } 212 | } 213 | 214 | stack.visitInsn(pointer.get()); 215 | pointer.advance(); 216 | } 217 | } 218 | 219 | private void convertMethod(MethodNode mNode) { 220 | if (mNode.name.equals("")) { 221 | MethodNode mv = staticClone(mNode, "$init$", ACC_PUBLIC); 222 | 223 | // Strip super constructor call. 224 | InsnListSection insns = new InsnListSection(); 225 | int idx = 0; 226 | insns.add(new VarInsnNode(ALOAD, idx++)); 227 | for (Type arg : Type.getArgumentTypes(mNode.desc)) { 228 | insns.add(new VarInsnNode(arg.getOpcode(ILOAD), idx)); 229 | idx += arg.getSize(); 230 | } 231 | insns.add(new MethodInsnNode(INVOKESPECIAL, cNode.superName, "", mNode.desc, false)); 232 | 233 | InsnListSection mInsns = new InsnListSection(mv.instructions); 234 | InsnListSection found = InsnComparator.matches(mInsns, insns, Collections.emptySet()); 235 | if (found == null) { 236 | throw new IllegalArgumentException("Invalid constructor insn sequence " + cNode.name + "\n" + mInsns); 237 | } 238 | found.trim(Collections.emptySet()).remove(); 239 | 240 | staticTransform(mv, mNode); 241 | return; 242 | } 243 | if ((mNode.access & ACC_STATIC) != 0) { 244 | int mask = ACC_PRIVATE | ACC_PROTECTED; 245 | int access = (mNode.access & ~mask) | ACC_PUBLIC; 246 | MethodNode mv = (MethodNode) sNode.visitMethod(access, mNode.name, mNode.desc, null, null); 247 | ASMHelper.copy(mNode, mv); 248 | staticTransform(mv, mNode); 249 | return; 250 | } 251 | boolean isPrivate = (mNode.access & ACC_PRIVATE) != 0; 252 | int access = !isPrivate ? ACC_PUBLIC : ACC_PRIVATE; 253 | if (!isPrivate) { 254 | MethodVisitor mv = tNode.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, mNode.name, mNode.desc, null, mNode.exceptions.toArray(new String[0])); 255 | traitMethods.add((MethodNode) mv); 256 | } 257 | MethodNode mv = staticClone(mNode, !isPrivate ? mNode.name + "$" : mNode.name, access); 258 | staticTransform(mv, mNode); 259 | } 260 | 261 | private @Nullable MethodInfo getSuper(MethodInsnNode mInsn, StackAnalyser stack) { 262 | if (mInsn.owner.equals(stack.owner.getInternalName())) { 263 | return null;//not a super call 264 | } 265 | 266 | //super calls are either to methods with the same name or contain a pattern 'target$$super$name' from the scala compiler 267 | String methodName = stack.mNode.name.replaceAll(".+\\Q$$super$\\E", ""); 268 | if (!mInsn.name.equals(methodName)) { 269 | return null; 270 | } 271 | 272 | StackAnalyser.StackEntry entry = stack.peek(Type.getType(mInsn.desc).getArgumentTypes().length); 273 | if (!(entry instanceof StackAnalyser.Load load)) { 274 | return null; 275 | } 276 | if (!(load.e instanceof StackAnalyser.This)) { 277 | return null; 278 | } 279 | 280 | return Objects.requireNonNull(mixinCompiler.getClassInfo(stack.owner.getInternalName()), "Failed to load class: " + stack.owner.getInternalName()) 281 | .findPublicParentImpl(methodName, mInsn.desc); 282 | } 283 | 284 | private MethodNode staticClone(MethodNode mNode, String name, int access) { 285 | ClassNode target = mNode.name.equals("") ? sNode : tNode; 286 | String desc = (mNode.access & ACC_STATIC) == 0 ? Utils.staticDesc(cNode.name, mNode.desc) : mNode.desc; 287 | MethodNode mv = (MethodNode) target.visitMethod(access | ACC_STATIC, name, desc, null, mNode.exceptions.toArray(new String[0])); 288 | ASMHelper.copy(mNode, mv); 289 | return mv; 290 | } 291 | 292 | private static List peekArgs(StackAnalyser analyser, String desc) { 293 | int len = Type.getArgumentTypes(desc).length; 294 | StackAnalyser.StackEntry[] args = new StackAnalyser.StackEntry[len]; 295 | 296 | for (int i = 0; i < len; ++i) { 297 | args[len - i - 1] = analyser.peek(i); 298 | } 299 | 300 | return Arrays.asList(args); 301 | } 302 | 303 | public static class InsnPointer { 304 | 305 | public final InsnList insnList; 306 | public @Nullable AbstractInsnNode pointer; 307 | 308 | public InsnPointer(InsnList insnList) { 309 | this.insnList = insnList; 310 | pointer = insnList.getFirst(); 311 | } 312 | 313 | private void replace(AbstractInsnNode newInsn) { 314 | insnList.insert(pointer, newInsn); 315 | insnList.remove(pointer); 316 | pointer = newInsn; 317 | } 318 | 319 | public @Nullable AbstractInsnNode get() { 320 | return pointer; 321 | } 322 | 323 | public AbstractInsnNode advance() { 324 | assert pointer != null; 325 | return pointer = pointer.getNext(); 326 | } 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/codechicken/mixin/MixinCompilerImpl.java: -------------------------------------------------------------------------------- 1 | package codechicken.mixin; 2 | 3 | import codechicken.asm.ASMHelper; 4 | import codechicken.mixin.api.MixinBackend; 5 | import codechicken.mixin.api.MixinCompiler; 6 | import codechicken.mixin.api.MixinDebugger; 7 | import codechicken.mixin.api.MixinLanguageSupport; 8 | import codechicken.mixin.util.*; 9 | import com.google.common.collect.Lists; 10 | import net.covers1624.quack.collection.ColUtils; 11 | import net.covers1624.quack.collection.FastStream; 12 | import net.covers1624.quack.util.SneakyUtils; 13 | import org.jetbrains.annotations.Nullable; 14 | import org.objectweb.asm.MethodVisitor; 15 | import org.objectweb.asm.Type; 16 | import org.objectweb.asm.tree.ClassNode; 17 | import org.objectweb.asm.tree.FieldNode; 18 | import org.objectweb.asm.tree.MethodNode; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.slf4j.event.Level; 22 | 23 | import java.lang.reflect.Constructor; 24 | import java.util.*; 25 | import java.util.function.Supplier; 26 | 27 | import static org.objectweb.asm.ClassReader.EXPAND_FRAMES; 28 | import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; 29 | import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; 30 | import static org.objectweb.asm.Opcodes.*; 31 | 32 | /** 33 | * Generates composite classes similar to the scala compiler with traits. 34 | *

35 | * Created by covers1624 on 2/11/20. 36 | */ 37 | public class MixinCompilerImpl implements MixinCompiler { 38 | 39 | public static final Level LOG_LEVEL = Level.valueOf(System.getProperty("codechicken.mixin.log_level", "DEBUG")); 40 | private static final Logger LOGGER = LoggerFactory.getLogger(MixinCompilerImpl.class); 41 | 42 | private final MixinBackend mixinBackend; 43 | private final MixinDebugger debugger; 44 | private final List languageSupportList; 45 | private final Map languageSupportMap; 46 | 47 | private final Map classBytesCache = Collections.synchronizedMap(new HashMap<>()); 48 | private final Map infoCache = Collections.synchronizedMap(new HashMap<>()); 49 | private final Map mixinMap = Collections.synchronizedMap(new HashMap<>()); 50 | private final MixinClassLoader classLoader; 51 | 52 | public MixinCompilerImpl() { 53 | this(new MixinBackend.SimpleMixinBackend()); 54 | } 55 | 56 | public MixinCompilerImpl(MixinBackend mixinBackend) { 57 | this(mixinBackend, new MixinDebugger.NullDebugger()); 58 | } 59 | 60 | public MixinCompilerImpl(MixinBackend mixinBackend, MixinDebugger debugger) { 61 | this(mixinBackend, debugger, () -> new SimpleServiceLoader<>(MixinLanguageSupport.class).poll().getNewServices()); 62 | } 63 | 64 | public MixinCompilerImpl(MixinBackend mixinBackend, MixinDebugger debugger, Supplier>> supportSupplier) { 65 | this.mixinBackend = mixinBackend; 66 | this.debugger = debugger; 67 | LOGGER.atLevel(LOG_LEVEL).log("Starting CodeChicken MixinCompiler."); 68 | LOGGER.atLevel(LOG_LEVEL).log("Loading MixinLanguageSupport services.."); 69 | long start = System.nanoTime(); 70 | List languageSupportInstances = FastStream.of(supportSupplier.get()) 71 | .map(LanguageSupportInstance::new) 72 | .sorted(Comparator.comparingInt(e -> e.sortIndex)) 73 | .toList(); 74 | languageSupportList = FastStream.of(languageSupportInstances) 75 | .map(e -> e.instance) 76 | .toList(); 77 | Map languageSupportInstanceMap = new HashMap<>(); 78 | for (LanguageSupportInstance instance : languageSupportInstances) { 79 | LanguageSupportInstance other = languageSupportInstanceMap.get(instance.name); 80 | if (other != null) { 81 | throw new RuntimeException(String.format("Duplicate MixinLanguageSupport. '%s' name conflicts with '%s'", instance, other)); 82 | } 83 | languageSupportInstanceMap.put(instance.name, instance); 84 | } 85 | languageSupportMap = FastStream.of(languageSupportInstanceMap.entrySet()) 86 | .toMap(Map.Entry::getKey, e -> e.getValue().instance); 87 | long end = System.nanoTime(); 88 | LOGGER.atLevel(LOG_LEVEL).log("Loaded {} MixinLanguageSupport instances in {}.", languageSupportList.size(), Utils.timeString(start, end)); 89 | 90 | classLoader = new MixinClassLoader(mixinBackend); 91 | } 92 | 93 | @Override 94 | public MixinBackend getMixinBackend() { 95 | return mixinBackend; 96 | } 97 | 98 | @Override 99 | public @Nullable T getLanguageSupport(String name) { 100 | return SneakyUtils.unsafeCast(languageSupportMap.get(name)); 101 | } 102 | 103 | @Override 104 | public @Nullable ClassInfo getClassInfo(String name) { 105 | // Do not compress this down to a computeIfAbsent, obtainInfo can modify the infoCache map. 106 | ClassInfo info = infoCache.get(name); 107 | if (info == null) { 108 | info = obtainInfo(name); 109 | infoCache.put(name, info); 110 | } 111 | return info; 112 | } 113 | 114 | @Override 115 | public @Nullable ClassInfo getClassInfo(ClassNode cNode) { 116 | ClassInfo info = infoCache.get(cNode.name); 117 | if (info == null) { 118 | info = obtainInfo(cNode); 119 | infoCache.put(cNode.name, info); 120 | } 121 | return info; 122 | } 123 | 124 | @Override 125 | public MixinInfo getMixinInfo(String name) { 126 | return mixinMap.get(name); 127 | } 128 | 129 | @Override 130 | @SuppressWarnings ("unchecked") 131 | public Class compileMixinClass(String name, String superClass, Set traits) { 132 | ClassInfo baseInfo = getClassInfo(superClass); 133 | if (baseInfo == null) throw new IllegalArgumentException("Provided super class does not exist."); 134 | if (traits.isEmpty()) { 135 | try { 136 | return (Class) Class.forName(baseInfo.getName().replace('/', '.'), true, mixinBackend.getContextClassLoader()); 137 | } catch (ClassNotFoundException ex) { 138 | throw new RuntimeException("Base class can't be loaded??", ex); 139 | } 140 | } 141 | 142 | long start = System.nanoTime(); 143 | List baseTraits = FastStream.of(traits) 144 | .map(mixinMap::get) 145 | .toList(); 146 | List mixinInfos = FastStream.of(baseTraits) 147 | .flatMap(MixinInfo::linearize) 148 | .distinct() 149 | .toList(); 150 | List traitInfos = FastStream.of(mixinInfos) 151 | .map(MixinInfo::name) 152 | .map(this::getClassInfo) 153 | .toList(); 154 | ClassNode cNode = new ClassNode(); 155 | 156 | cNode.visit(V1_8, ACC_PUBLIC, name, null, superClass, FastStream.of(baseTraits).map(MixinInfo::name).toArray(new String[0])); 157 | 158 | MethodInfo cInit = FastStream.of(baseInfo.getMethods()) 159 | .filter(e -> e.getName().equals("")) 160 | .first(); 161 | MethodNode mInit = (MethodNode) cNode.visitMethod(ACC_PUBLIC, "", cInit.getDesc(), null, null); 162 | Utils.writeBridge(mInit, cInit.getDesc(), INVOKESPECIAL, superClass, "", cInit.getDesc(), false); 163 | mInit.instructions.remove(mInit.instructions.getLast());//remove the RETURN from writeBridge 164 | 165 | List prevInfos = new ArrayList<>(); 166 | 167 | for (MixinInfo t : mixinInfos) { 168 | { 169 | int idx = 0; 170 | mInit.visitVarInsn(ALOAD, idx++); 171 | for (Type arg : Type.getArgumentTypes(cInit.getDesc())) { 172 | mInit.visitVarInsn(arg.getOpcode(ILOAD), idx); 173 | idx += arg.getSize(); 174 | } 175 | mInit.visitMethodInsn(INVOKESTATIC, t.name(), "$init$", Utils.staticDesc(t.name(), cInit.getDesc()), true); 176 | } 177 | 178 | for (FieldMixin f : t.fields()) { 179 | FieldNode fv = (FieldNode) cNode.visitField(ACC_PRIVATE, f.getAccessName(t.name()), f.desc(), null, null); 180 | 181 | Type fType = Type.getType(fv.desc); 182 | MethodVisitor mv; 183 | mv = cNode.visitMethod(ACC_PUBLIC, fv.name, "()" + f.desc(), null, null); 184 | mv.visitVarInsn(ALOAD, 0); 185 | mv.visitFieldInsn(GETFIELD, name, fv.name, fv.desc); 186 | mv.visitInsn(fType.getOpcode(IRETURN)); 187 | mv.visitMaxs(-1, -1); 188 | 189 | mv = cNode.visitMethod(ACC_PUBLIC, fv.name + "_$eq", "(" + f.desc() + ")V", null, null); 190 | mv.visitVarInsn(ALOAD, 0); 191 | mv.visitVarInsn(fType.getOpcode(ILOAD), 1); 192 | mv.visitFieldInsn(PUTFIELD, name, fv.name, fv.desc); 193 | mv.visitInsn(RETURN); 194 | mv.visitMaxs(-1, -1); 195 | } 196 | 197 | for (String s : t.supers()) { 198 | int nIdx = s.indexOf('('); 199 | String sName = s.substring(0, nIdx); 200 | String sDesc = s.substring(nIdx); 201 | MethodNode mv = (MethodNode) cNode.visitMethod(ACC_PUBLIC, t.name().replace("/", "$") + "$$super$" + sName, sDesc, null, null); 202 | 203 | MixinInfo prev = FastStream.of(prevInfos) 204 | .reversed() 205 | .filter(e -> ColUtils.anyMatch(e.methods(), m -> m.name.equals(sName) && m.desc.equals(sDesc))) 206 | .firstOrDefault(); 207 | // each super goes to the one before 208 | if (prev != null) { 209 | Utils.writeStaticBridge(mv, sName, prev); 210 | } else { 211 | MethodInfo mInfo = Objects.requireNonNull(baseInfo.findPublicImpl(sName, sDesc)); 212 | Utils.writeBridge(mv, sDesc, INVOKESPECIAL, mInfo.getOwner().getName(), sName, sDesc, mInfo.getOwner().isInterface()); 213 | } 214 | 215 | } 216 | prevInfos.add(t); 217 | } 218 | mInit.visitInsn(RETURN); 219 | 220 | Set methodSigs = new HashSet<>(); 221 | for (MixinInfo t : Lists.reverse(mixinInfos)) {//last trait gets first pick on methods 222 | for (MethodNode m : t.methods()) { 223 | if (methodSigs.add(m.name + m.desc)) { 224 | MethodNode mv = (MethodNode) cNode.visitMethod(ACC_PUBLIC, m.name, m.desc, null, m.exceptions.toArray(new String[0])); 225 | Utils.writeStaticBridge(mv, m.name, t); 226 | } 227 | } 228 | } 229 | 230 | // generate synthetic bridge methods for covariant return types 231 | Set allParentInfos = FastStream.of(baseInfo) 232 | .concat(traitInfos) 233 | .flatMap(Utils::allParents) 234 | .toSet(); 235 | List allParentMethods = FastStream.of(allParentInfos) 236 | .flatMap(ClassInfo::getMethods) 237 | .toList(); 238 | 239 | for (String nameDesc : new HashSet<>(methodSigs)) { 240 | int nIdx = nameDesc.indexOf('('); 241 | String sName = nameDesc.substring(0, nIdx); 242 | String sDesc = nameDesc.substring(nIdx); 243 | String pDesc = sDesc.substring(0, sDesc.lastIndexOf(")") + 1); 244 | for (MethodInfo m : allParentMethods) { 245 | if (!m.getName().equals(sName) || !m.getDesc().startsWith(pDesc)) continue; 246 | if (!methodSigs.add(m.getName() + m.getDesc())) continue; 247 | 248 | MethodNode mv = (MethodNode) cNode.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC | ACC_BRIDGE, m.getName(), m.getDesc(), null, m.getExceptions()); 249 | Utils.writeBridge(mv, mv.desc, INVOKEVIRTUAL, cNode.name, sName, sDesc, (cNode.access & ACC_INTERFACE) != 0); 250 | } 251 | } 252 | 253 | byte[] bytes = ASMHelper.createBytes(cNode, COMPUTE_FRAMES | COMPUTE_MAXS); 254 | long end = System.nanoTime(); 255 | LOGGER.atLevel(LOG_LEVEL).log("Generation of {} with [{}] took {}", superClass, String.join(", ", traits), Utils.timeString(start, end)); 256 | return defineClass(name, bytes); 257 | } 258 | 259 | @Override 260 | @SuppressWarnings ("unchecked") 261 | public Class defineClass(String name, byte[] bytes) { 262 | debugger.defineClass(name, bytes); 263 | return (Class) classLoader.defineClass(name, bytes); 264 | } 265 | 266 | @Override 267 | @SuppressWarnings ("unchecked") 268 | public Class getDefinedClass(String name) { 269 | return (Class) Objects.requireNonNull(classLoader.getDefinedClass(name), "Class was not defined by MixinCompiler. " + name); 270 | } 271 | 272 | @Override 273 | public MixinInfo registerTrait(ClassNode cNode) { 274 | // Cache hit. 275 | MixinInfo info = mixinMap.get(cNode.name); 276 | if (info != null) { 277 | return info; 278 | } 279 | 280 | for (MixinLanguageSupport languageSupport : languageSupportList) { 281 | info = languageSupport.buildMixinTrait(cNode); 282 | if (info == null) continue; 283 | 284 | if (!cNode.name.equals(info.name())) { 285 | throw new IllegalStateException("Traits must have the same name as their ClassNode. Got: " + info.name() + ", Expected: " + cNode.name); 286 | } 287 | mixinMap.put(info.name(), info); 288 | return info; 289 | } 290 | throw new IllegalStateException("No MixinLanguageSupport wished to handle class '" + cNode.name + "'"); 291 | } 292 | 293 | private ClassInfo obtainInfo(ClassNode cNode) { 294 | for (MixinLanguageSupport languageSupport : languageSupportList) { 295 | ClassInfo info = languageSupport.obtainInfo(cNode); 296 | if (info != null) { 297 | return info; 298 | } 299 | } 300 | // In theory not possible, the Java plugin will always create nodes for 301 | throw new IllegalStateException("Java plugin did not create ClassInfo for existing node: " + cNode.name); 302 | } 303 | 304 | private @Nullable ClassInfo obtainInfo(String cName) { 305 | ClassNode cNode = getClassNode(cName); 306 | if (cNode != null) { 307 | return obtainInfo(cNode); 308 | } 309 | 310 | try { 311 | return new ReflectionClassInfo( 312 | this, 313 | Class.forName(cName.replace('/', '.'), true, mixinBackend.getContextClassLoader()) 314 | ); 315 | } catch (ClassNotFoundException ignored) { 316 | } 317 | return null; 318 | } 319 | 320 | @Override 321 | public ClassNode getClassNode(String name) { 322 | if (name.equals("java/lang/Object")) return null; 323 | 324 | byte[] bytes = classBytesCache.computeIfAbsent(name, mixinBackend::getBytes); 325 | if (bytes == null) return null; 326 | 327 | return ASMHelper.createClassNode(bytes, EXPAND_FRAMES); 328 | } 329 | 330 | private static class MixinClassLoader extends ClassLoader { 331 | 332 | public MixinClassLoader(MixinBackend mixinBackend) { 333 | super(mixinBackend.getContextClassLoader()); 334 | } 335 | 336 | public Class defineClass(String cName, byte[] bytes) { 337 | return defineClass(cName.replace('/', '.'), bytes, 0, bytes.length); 338 | } 339 | 340 | public Class getDefinedClass(String cName) { 341 | return findLoadedClass(cName.replace('/', '.')); 342 | } 343 | } 344 | 345 | private class LanguageSupportInstance { 346 | 347 | private final Class clazz; 348 | private final MixinLanguageSupport instance; 349 | private final String name; 350 | private final int sortIndex; 351 | 352 | public LanguageSupportInstance(Class clazz) { 353 | this.clazz = clazz; 354 | MixinLanguageSupport.LanguageName lName = clazz.getAnnotation(MixinLanguageSupport.LanguageName.class); 355 | MixinLanguageSupport.SortingIndex sIndex = clazz.getAnnotation(MixinLanguageSupport.SortingIndex.class); 356 | if (lName == null) { 357 | throw new RuntimeException("MixinLanguageSupport '" + clazz.getName() + "' is not annotated with MixinLanguageSupport.LanguageName!"); 358 | } 359 | name = lName.value(); 360 | sortIndex = sIndex != null ? sIndex.value() : 1000; 361 | 362 | LOGGER.atLevel(LOG_LEVEL).log("Loading MixinLanguageSupport '{}', Name: '{}', Sorting Index: '{}'", clazz.getName(), name, sortIndex); 363 | Constructor ctor = Utils.findConstructor(clazz, MixinCompiler.class); 364 | Object[] args; 365 | if (ctor != null) { 366 | args = new Object[] { MixinCompilerImpl.this }; 367 | } else { 368 | ctor = Utils.findConstructor(clazz); 369 | args = new Object[0]; 370 | } 371 | if (ctor == null) { 372 | throw new RuntimeException("Expected MixinLanguageSupport to have either no-args ctor or take a MixinCompiler instance."); 373 | } 374 | instance = Utils.newInstance(ctor, args); 375 | } 376 | 377 | @Override 378 | public String toString() { 379 | return new StringJoiner(", ", LanguageSupportInstance.class.getSimpleName() + "[", "]") 380 | .add("class=" + clazz.getName()) 381 | .add("name='" + name + "'") 382 | .add("sortIndex=" + sortIndex) 383 | .toString(); 384 | } 385 | } 386 | } 387 | --------------------------------------------------------------------------------