Skip to content

Commit

Permalink
Feature: @Requires Annotation (#414)
Browse files Browse the repository at this point in the history
* Add Requires Annotation

* Fix

* Add PackageRequiresStrippingVisitor

* Missing Symbol :/

* Blah Blah Blah Usage Conventions

* Blah Blah Blah License Stuff

* `value` instead of `mods`

* moving it to Top API Level

* Compacting Code => So Long, Stripping Data Containers...

* Removing Deprecated Fields

* Removing Deprecated Inner Classes

* I a word
  • Loading branch information
FirstMegaGame4 authored Mar 29, 2024
1 parent 6fe8235 commit 8b7fb2e
Show file tree
Hide file tree
Showing 8 changed files with 550 additions and 254 deletions.
58 changes: 58 additions & 0 deletions src/main/java/org/quiltmc/loader/api/Requires.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2016 FabricMC
* Copyright 2022-2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.quiltmc.loader.api;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Applied to declare that the annotated element requires specific mods to exist.
* <p>
* When applied to mod code this will result in quilt-loader removing that element when running without the specified mods.
* <p>
* When the annotated element is removed, bytecode associated with the element will not be removed. For example, if a
* field is removed, its initializer code will not, and will cause an error on execution.
* <p>
* If an overriding method has this annotation and its overridden method doesn't, unexpected behavior may happen. If an
* overridden method has this annotation while the overriding method doesn't, it is safe, but the method can be used
* from the overridden class only when specified mods are found.
* <p>
* When applied to an implemented interface (via {@link ElementType#TYPE_USE}) it will be removed when running without the specified mods.
* Overridden interface methods need to be annotated separately in order to be removed.
*
* @apiNote Even is this annotation is provided by Quilt Loader, some usages of it are not recommended. In example, when
* you are using code from other mods thanks to their APIs using entrypoints, you should not implement their entrypoint
* interface on the same classes that you are using for vanilla entrypoints. Instead, you should use a separate class in
* a separated package, dedicated to the mod integration that you are making. And to continue this example by a great usage
* of this annotation, you could use it in a new package-info.java file of the current package dedicated for the
* mod integration, in order to trim the complete package, which will provide an additional safety layer by avoiding
* errors when loading and / or calling classes and elements that should not when the required mod is not present. */
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.TYPE, ElementType.TYPE_USE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PACKAGE })
@Documented
public @interface Requires {

/** @return required mods */
String[] value();

/** @return True if lambda methods referenced by this method should also be stripped. Has no effect when used to
* annotate classes, implements declarations, or fields. */
boolean stripLambdas() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package org.quiltmc.loader.impl.launch.knot;

import net.fabricmc.api.EnvType;

import org.quiltmc.loader.impl.transformer.PackageStrippingData;
import org.quiltmc.loader.impl.util.LoaderUtil;
import org.objectweb.asm.ClassReader;
import org.quiltmc.loader.api.ModContainer;
Expand All @@ -26,7 +28,6 @@
import org.quiltmc.loader.impl.launch.common.QuiltCodeSource;
import org.quiltmc.loader.impl.launch.common.QuiltLauncherBase;
import org.quiltmc.loader.impl.patch.PatchLoader;
import org.quiltmc.loader.impl.transformer.PackageEnvironmentStrippingData;
import org.quiltmc.loader.impl.util.FileSystemUtil;
import org.quiltmc.loader.impl.util.FileUtil;
import org.quiltmc.loader.impl.util.ManifestUtil;
Expand All @@ -51,7 +52,6 @@
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -322,9 +322,13 @@ boolean computeCanLoadPackage(String pkgName, boolean allowFromParent) {
// No package-info class file
return true;
}
PackageEnvironmentStrippingData data = new PackageEnvironmentStrippingData(QuiltLoaderImpl.ASM_VERSION, envType);
new ClassReader(bytes).accept(data, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
return !data.stripEntirePackage;

ClassReader reader = new ClassReader(bytes);

PackageStrippingData strippingData = new PackageStrippingData(QuiltLoaderImpl.ASM_VERSION, envType, modCodeSourceMap);
reader.accept(strippingData, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);

return !strippingData.stripEntirePackage();
} catch (IOException e) {
throw new RuntimeException("Unable to load " + fileName, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,265 +16,79 @@

package org.quiltmc.loader.impl.transformer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.TypeReference;
import org.quiltmc.loader.api.minecraft.ClientOnly;
import org.quiltmc.loader.api.minecraft.DedicatedServerOnly;
import org.quiltmc.loader.impl.util.QuiltLoaderInternal;
import org.quiltmc.loader.impl.util.QuiltLoaderInternalType;

import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.api.EnvironmentInterface;
import net.fabricmc.api.EnvironmentInterfaces;

/** Scans a class for Environment and EnvironmentInterface annotations to figure out what needs to be stripped. */
/** Deprecated. All stuff were moved to {@link StrippingData}. */
@Deprecated
@QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED)
public class EnvironmentStrippingData extends ClassVisitor {
// Fabric annotations
private static final String ENVIRONMENT_DESCRIPTOR = Type.getDescriptor(Environment.class);
private static final String ENVIRONMENT_INTERFACE_DESCRIPTOR = Type.getDescriptor(EnvironmentInterface.class);
private static final String ENVIRONMENT_INTERFACES_DESCRIPTOR = Type.getDescriptor(EnvironmentInterfaces.class);

// Quilt annotations
private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class);
private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class);

private final EnvType envType;
private final String envTypeString;

private boolean stripEntireClass = false;
private String[] interfaces;
private final Collection<String> stripInterfaces = new HashSet<>();
private final Collection<String> stripFields = new HashSet<>();
private final Collection<String> stripMethods = new HashSet<>();

/** Every method contained in this will also be contained in {@link #stripMethods}. */
private final Collection<String> stripMethodLambdas = new HashSet<>();

private class FabricEnvironmentAnnotationVisitor extends AnnotationVisitor {
private final Runnable onEnvMismatch;

private FabricEnvironmentAnnotationVisitor(int api, Runnable onEnvMismatch) {
super(api);
this.onEnvMismatch = onEnvMismatch;
}

@Override
public void visitEnum(String name, String descriptor, String value) {
if ("value".equals(name) && !envTypeString.equals(value)) {
onEnvMismatch.run();
}
}
}

private class QuiltEnvironmentAnnotationVisitor extends AnnotationVisitor {
private final Runnable onEnvMismatch;
private final Runnable onEnvMismatchLambdas;

private boolean stripLambdas = true;

private QuiltEnvironmentAnnotationVisitor(int api, Runnable onEnvMismatch, Runnable onEnvMismatchLambda) {
super(api);
this.onEnvMismatch = onEnvMismatch;
this.onEnvMismatchLambdas = onEnvMismatchLambda;
}

@Override
public void visit(String name, Object value) {
if ("stripLambdas".equals(name) && Boolean.FALSE.equals(value)) {
stripLambdas = false;
}
}

@Override
public void visitEnd() {
onEnvMismatch.run();
if (stripLambdas && onEnvMismatchLambdas != null) {
onEnvMismatchLambdas.run();
}
}
}

private class FabricEnvironmentInterfaceAnnotationVisitor extends AnnotationVisitor {
private boolean envMismatch;
private Type itf;

private FabricEnvironmentInterfaceAnnotationVisitor(int api) {
super(api);
}

@Override
public void visitEnum(String name, String descriptor, String value) {
if ("value".equals(name) && !envTypeString.equals(value)) {
envMismatch = true;
}
}

@Override
public void visit(String name, Object value) {
if ("itf".equals(name)) {
itf = (Type) value;
}
}

@Override
public void visitEnd() {
if (envMismatch) {
stripInterfaces.add(itf.getInternalName());
}
}
}

private AnnotationVisitor visitMemberAnnotation(String descriptor, boolean visible, Runnable onEnvMismatch,
Runnable onEnvMismatchLambdas) {
if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) {
return new FabricEnvironmentAnnotationVisitor(api, onEnvMismatch);
}

if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor) && envType == EnvType.SERVER) {
return new QuiltEnvironmentAnnotationVisitor(api, onEnvMismatch, onEnvMismatchLambdas);
}

if (SERVER_ONLY_DESCRIPTOR.equals(descriptor) && envType == EnvType.CLIENT) {
return new QuiltEnvironmentAnnotationVisitor(api, onEnvMismatch, onEnvMismatchLambdas);
}

return null;
}
public class EnvironmentStrippingData extends StrippingData {

public EnvironmentStrippingData(int api, EnvType envType) {
super(api);
this.envType = envType;
this.envTypeString = envType.name();
super(api, envType, new ArrayList<>());
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.interfaces = interfaces;
super.visit(version, access, name, signature, superName, interfaces);
}

@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) {
if (envType == EnvType.SERVER) {
stripEntireClass = true;
}
} else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) {
if (envType == EnvType.CLIENT) {
stripEntireClass = true;
}
} else if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) {
return new FabricEnvironmentAnnotationVisitor(api, () -> stripEntireClass = true);
} else if (ENVIRONMENT_INTERFACE_DESCRIPTOR.equals(descriptor)) {
return new FabricEnvironmentInterfaceAnnotationVisitor(api);
} else if (ENVIRONMENT_INTERFACES_DESCRIPTOR.equals(descriptor)) {
return new AnnotationVisitor(api) {
@Override
public AnnotationVisitor visitArray(String name) {
if ("value".equals(name)) {
return new AnnotationVisitor(api) {
@Override
public AnnotationVisitor visitAnnotation(String name, String descriptor) {
return new FabricEnvironmentInterfaceAnnotationVisitor(api);
}
};
}

return null;
}
};
}

return null;
return super.visitAnnotation(descriptor, visible);
}

@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) {

TypeReference ref = new TypeReference(typeRef);

if (ref.getSort() != TypeReference.CLASS_EXTENDS) {
return null;
}

int interfaceIdx = ref.getSuperTypeIndex();

if (interfaceIdx < 0) {
// Wrongly applied to the super class
return null;
}

final EnvType annotationEnv;

if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) {
annotationEnv = EnvType.CLIENT;
} else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) {
annotationEnv = EnvType.SERVER;
} else {
return null;
}

if (annotationEnv != envType) {
stripInterfaces.add(interfaces[interfaceIdx]);
}

return null;
return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
}

@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
return new FieldVisitor(api) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return visitMemberAnnotation(descriptor, visible, () -> stripFields.add(name + descriptor), null);
}
};
return super.visitField(access, name, descriptor, signature, value);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
String methodId = name + descriptor;
return new MethodVisitor(api) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return visitMemberAnnotation(
descriptor, visible, () -> stripMethods.add(methodId), () -> stripMethodLambdas.add(methodId)
);
}
};
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}

@Override
public boolean stripEntireClass() {
return stripEntireClass;
return super.stripEntireClass();
}

@Override
public Collection<String> getStripInterfaces() {
return stripInterfaces;
return super.getStripInterfaces();
}

@Override
public Collection<String> getStripFields() {
return stripFields;
return super.getStripFields();
}

@Override
public Collection<String> getStripMethods() {
return stripMethods;
return super.getStripMethods();
}

@Override
public Collection<String> getStripMethodLambdas() {
return stripMethodLambdas;
return super.getStripMethodLambdas();
}

@Override
public boolean isEmpty() {
return stripInterfaces.isEmpty() && stripFields.isEmpty() && stripMethods.isEmpty();
return super.isEmpty();
}
}
Loading

0 comments on commit 8b7fb2e

Please sign in to comment.