From 458279d11198ac779cf2c482f6b3649f2d49c809 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Tue, 13 Apr 2021 11:42:32 +0200 Subject: [PATCH] Fix (Bungee): Java 16 compatibility (#2433) This has been tested on the following: - AdoptOpenJDK Java 1.8.0_282 - GraalVM CE 21.0.0 OpenJDK 11.0.10 - AdoptOpenJDK Java 15.0.2 - AdoptOpenJDK Java 16 (also tested with BungeeCord b1556) - Amazon Corretto OpenJDK 16.0.0.36.1 ... with Waterfall b406 on Linux 5.10.28. --- bungee/build.gradle.kts | 7 +++ .../bungee/platform/BungeeViaInjector.java | 58 +++++++++++++------ java-compat/build.gradle.kts | 12 ++++ java-compat/java-compat-16/build.gradle.kts | 10 ++++ .../jre16/Jre16FieldModifierAccessor.java | 35 +++++++++++ java-compat/java-compat-8/build.gradle.kts | 3 + .../jre8/Jre8FieldModifierAccessor.java | 23 ++++++++ java-compat/java-compat-9/build.gradle.kts | 8 +++ .../jre9/Jre9FieldModifierAccessor.java | 24 ++++++++ .../java-compat-common/build.gradle.kts | 0 .../compatibility/FieldModifierAccessor.java | 26 +++++++++ .../compatibility/JavaVersionIdentifier.java | 41 +++++++++++++ settings.gradle.kts | 2 + 13 files changed, 230 insertions(+), 19 deletions(-) create mode 100644 java-compat/build.gradle.kts create mode 100644 java-compat/java-compat-16/build.gradle.kts create mode 100644 java-compat/java-compat-16/src/main/java/us/myles/ViaVersion/compatibility/jre16/Jre16FieldModifierAccessor.java create mode 100644 java-compat/java-compat-8/build.gradle.kts create mode 100644 java-compat/java-compat-8/src/main/java/us/myles/ViaVersion/compatibility/jre8/Jre8FieldModifierAccessor.java create mode 100644 java-compat/java-compat-9/build.gradle.kts create mode 100644 java-compat/java-compat-9/src/main/java/us/myles/ViaVersion/compatibility/jre9/Jre9FieldModifierAccessor.java create mode 100644 java-compat/java-compat-common/build.gradle.kts create mode 100644 java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/FieldModifierAccessor.java create mode 100644 java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/JavaVersionIdentifier.java diff --git a/bungee/build.gradle.kts b/bungee/build.gradle.kts index fbc99a78d..150642d56 100644 --- a/bungee/build.gradle.kts +++ b/bungee/build.gradle.kts @@ -1,4 +1,11 @@ dependencies { implementation(project(":viaversion-common")) + implementation(project(":java-compat")) compileOnly("net.md-5", "bungeecord-api", Versions.bungee) } + +configure { + // This is necessary to allow compilation for Java 8 while still including + // newer Java versions in the code. + disableAutoTargetJvm() +} diff --git a/bungee/src/main/java/us/myles/ViaVersion/bungee/platform/BungeeViaInjector.java b/bungee/src/main/java/us/myles/ViaVersion/bungee/platform/BungeeViaInjector.java index f02913cc2..49e6b85bb 100644 --- a/bungee/src/main/java/us/myles/ViaVersion/bungee/platform/BungeeViaInjector.java +++ b/bungee/src/main/java/us/myles/ViaVersion/bungee/platform/BungeeViaInjector.java @@ -25,15 +25,52 @@ import it.unimi.dsi.fastutil.ints.IntSortedSet; import us.myles.ViaVersion.api.Via; import us.myles.ViaVersion.api.platform.ViaInjector; import us.myles.ViaVersion.bungee.handlers.BungeeChannelInitializer; +import us.myles.ViaVersion.compatibility.FieldModifierAccessor; +import us.myles.ViaVersion.compatibility.JavaVersionIdentifier; +import us.myles.ViaVersion.compatibility.jre16.Jre16FieldModifierAccessor; +import us.myles.ViaVersion.compatibility.jre8.Jre8FieldModifierAccessor; +import us.myles.ViaVersion.compatibility.jre9.Jre9FieldModifierAccessor; import us.myles.ViaVersion.util.ReflectionUtil; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; public class BungeeViaInjector implements ViaInjector { + private final FieldModifierAccessor fieldModifierAccessor; + + public BungeeViaInjector() { + FieldModifierAccessor fieldModifierAccessor = null; + try { + if (JavaVersionIdentifier.IS_JAVA_16) { + fieldModifierAccessor = new Jre16FieldModifierAccessor(); + } else if (JavaVersionIdentifier.IS_JAVA_9) { + fieldModifierAccessor = new Jre9FieldModifierAccessor(); + } + } catch (final ReflectiveOperationException outer) { + try { + fieldModifierAccessor = new Jre8FieldModifierAccessor(); + Via.getPlatform().getLogger().warning("Had to fall back to the Java 8 field modifier accessor."); + outer.printStackTrace(); + } catch (final ReflectiveOperationException inner) { + inner.addSuppressed(outer); + throw new IllegalStateException("Cannot create a modifier accessor", inner); + } + } + + try { + if (fieldModifierAccessor == null) { + fieldModifierAccessor = new Jre8FieldModifierAccessor(); + } + } catch (final ReflectiveOperationException ex) { + throw new IllegalStateException("Cannot create a modifier accessor", ex); + } + + // Must be non-null by now. + this.fieldModifierAccessor = fieldModifierAccessor; + } + @Override public void inject() throws Exception { try { @@ -42,26 +79,9 @@ public class BungeeViaInjector implements ViaInjector { field.setAccessible(true); // Remove the final modifier (unless removed by a fork) - //TODO Fix Java 16 compatibility int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { - try { - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, modifiers & ~Modifier.FINAL); - } catch (NoSuchFieldException e) { - // Java 12 compatibility *this is fine* - Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); - getDeclaredFields0.setAccessible(true); - Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false); - for (Field classField : fields) { - if ("modifiers".equals(classField.getName())) { - classField.setAccessible(true); - classField.set(field, modifiers & ~Modifier.FINAL); - break; - } - } - } + this.fieldModifierAccessor.setModifiers(field, modifiers & ~Modifier.FINAL); } BungeeChannelInitializer newInit = new BungeeChannelInitializer((ChannelInitializer) field.get(null)); diff --git a/java-compat/build.gradle.kts b/java-compat/build.gradle.kts new file mode 100644 index 000000000..ed4e522b5 --- /dev/null +++ b/java-compat/build.gradle.kts @@ -0,0 +1,12 @@ +dependencies { + api(project(":java-compat:java-compat-common")) + api(project(":java-compat:java-compat-8")) + api(project(":java-compat:java-compat-9")) + api(project(":java-compat:java-compat-16")) +} + +configure { + // This is necessary to allow compilation for Java 8 while still including + // newer Java versions in the code. + disableAutoTargetJvm() +} diff --git a/java-compat/java-compat-16/build.gradle.kts b/java-compat/java-compat-16/build.gradle.kts new file mode 100644 index 000000000..cbea07999 --- /dev/null +++ b/java-compat/java-compat-16/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + api(project(":java-compat:java-compat-common")) +} + +configure { + // This is for Java 16, but the minimum required for this + // is actually just Java 9! + sourceCompatibility = JavaVersion.VERSION_1_9 + targetCompatibility = JavaVersion.VERSION_1_9 +} diff --git a/java-compat/java-compat-16/src/main/java/us/myles/ViaVersion/compatibility/jre16/Jre16FieldModifierAccessor.java b/java-compat/java-compat-16/src/main/java/us/myles/ViaVersion/compatibility/jre16/Jre16FieldModifierAccessor.java new file mode 100644 index 000000000..afc4d2c65 --- /dev/null +++ b/java-compat/java-compat-16/src/main/java/us/myles/ViaVersion/compatibility/jre16/Jre16FieldModifierAccessor.java @@ -0,0 +1,35 @@ +package us.myles.ViaVersion.compatibility.jre16; + +import us.myles.ViaVersion.compatibility.FieldModifierAccessor; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.util.Objects; + +@SuppressWarnings({ + "java:S1191", // SonarLint/-Qube/-Cloud: We (sadly) need Unsafe for the Java 16 impl. + "java:S3011", // ^: We need to circumvent the access restrictions of fields. +}) +public final class Jre16FieldModifierAccessor implements FieldModifierAccessor { + private final VarHandle modifiersHandle; + + public Jre16FieldModifierAccessor() throws ReflectiveOperationException { + final Field theUnsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafeField.setAccessible(true); + final sun.misc.Unsafe unsafe = (sun.misc.Unsafe) theUnsafeField.get(null); + + final Field trustedLookup = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + final MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafe.getObject( + unsafe.staticFieldBase(trustedLookup), unsafe.staticFieldOffset(trustedLookup)); + + this.modifiersHandle = lookup.findVarHandle(Field.class, "modifiers", int.class); + } + + @Override + public void setModifiers(final Field field, final int modifiers) { + Objects.requireNonNull(field, "field must not be null"); + + this.modifiersHandle.set(field, modifiers); + } +} diff --git a/java-compat/java-compat-8/build.gradle.kts b/java-compat/java-compat-8/build.gradle.kts new file mode 100644 index 000000000..6b8e88798 --- /dev/null +++ b/java-compat/java-compat-8/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + api(project(":java-compat:java-compat-common")) +} diff --git a/java-compat/java-compat-8/src/main/java/us/myles/ViaVersion/compatibility/jre8/Jre8FieldModifierAccessor.java b/java-compat/java-compat-8/src/main/java/us/myles/ViaVersion/compatibility/jre8/Jre8FieldModifierAccessor.java new file mode 100644 index 000000000..ac50412ab --- /dev/null +++ b/java-compat/java-compat-8/src/main/java/us/myles/ViaVersion/compatibility/jre8/Jre8FieldModifierAccessor.java @@ -0,0 +1,23 @@ +package us.myles.ViaVersion.compatibility.jre8; + +import us.myles.ViaVersion.compatibility.FieldModifierAccessor; + +import java.lang.reflect.Field; +import java.util.Objects; + +@SuppressWarnings("java:S3011") // SonarLint/-Qube/-Cloud: we are intentionally bypassing the setter. +public final class Jre8FieldModifierAccessor implements FieldModifierAccessor { + private final Field modifiersField; + + public Jre8FieldModifierAccessor() throws ReflectiveOperationException { + this.modifiersField = Field.class.getDeclaredField("modifiers"); + this.modifiersField.setAccessible(true); + } + + @Override + public void setModifiers(final Field field, final int modifiers) throws ReflectiveOperationException { + Objects.requireNonNull(field, "field must not be null"); + + this.modifiersField.setInt(field, modifiers); + } +} diff --git a/java-compat/java-compat-9/build.gradle.kts b/java-compat/java-compat-9/build.gradle.kts new file mode 100644 index 000000000..01c8a8b7a --- /dev/null +++ b/java-compat/java-compat-9/build.gradle.kts @@ -0,0 +1,8 @@ +dependencies { + api(project(":java-compat:java-compat-common")) +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_9 + targetCompatibility = JavaVersion.VERSION_1_9 +} diff --git a/java-compat/java-compat-9/src/main/java/us/myles/ViaVersion/compatibility/jre9/Jre9FieldModifierAccessor.java b/java-compat/java-compat-9/src/main/java/us/myles/ViaVersion/compatibility/jre9/Jre9FieldModifierAccessor.java new file mode 100644 index 000000000..8ca782a69 --- /dev/null +++ b/java-compat/java-compat-9/src/main/java/us/myles/ViaVersion/compatibility/jre9/Jre9FieldModifierAccessor.java @@ -0,0 +1,24 @@ +package us.myles.ViaVersion.compatibility.jre9; + +import us.myles.ViaVersion.compatibility.FieldModifierAccessor; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.util.Objects; + +public final class Jre9FieldModifierAccessor implements FieldModifierAccessor { + private final VarHandle modifiersHandle; + + public Jre9FieldModifierAccessor() throws ReflectiveOperationException { + final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup()); + this.modifiersHandle = lookup.findVarHandle(Field.class, "modifiers", int.class); + } + + @Override + public void setModifiers(final Field field, final int modifiers) { + Objects.requireNonNull(field, "field must not be null"); + + this.modifiersHandle.set(field, modifiers); + } +} diff --git a/java-compat/java-compat-common/build.gradle.kts b/java-compat/java-compat-common/build.gradle.kts new file mode 100644 index 000000000..e69de29bb diff --git a/java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/FieldModifierAccessor.java b/java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/FieldModifierAccessor.java new file mode 100644 index 000000000..6c7b4b683 --- /dev/null +++ b/java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/FieldModifierAccessor.java @@ -0,0 +1,26 @@ +package us.myles.ViaVersion.compatibility; + +import java.lang.reflect.Field; + +/** + * Exposes a way to access the modifiers of a {@link Field} mutably. + *

+ * Note: This is explicitly an implementation detail. Do not rely on this within plugins and any + * non-ViaVersion code. + *

+ */ +public interface FieldModifierAccessor { + /** + * Sets the modifiers of a field. + *

+ * Note: This does not set the accessibility of the field. If you need to read or mutate it, you must handle + * that yourself. + *

+ * + * @param field the field to set the modifiers of. Will throw if {@code null}. + * @param modifiers the modifiers to set on the given {@code field}. + * @throws ReflectiveOperationException if the reflective operation fails this method is implemented with fails. + */ + void setModifiers(final Field field, final int modifiers) + throws ReflectiveOperationException; +} diff --git a/java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/JavaVersionIdentifier.java b/java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/JavaVersionIdentifier.java new file mode 100644 index 000000000..43fa5fbbb --- /dev/null +++ b/java-compat/java-compat-common/src/main/java/us/myles/ViaVersion/compatibility/JavaVersionIdentifier.java @@ -0,0 +1,41 @@ +package us.myles.ViaVersion.compatibility; + +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.stream.Stream; + +public enum JavaVersionIdentifier { + ; + + public static final boolean IS_JAVA_9; + public static final boolean IS_JAVA_16; + + static { + // Optional#stream()Stream is marked `@since 9`. + IS_JAVA_9 = doesMethodExist(Optional.class, "stream"); + + // Stream#toList()List is marked `@since 16`. + IS_JAVA_16 = doesMethodExist(Stream.class, "toList"); + } + + /** + * Checks if the given name of a {@link Method} exists on the given {@link Class} without comparing parameters or + * other parts of the descriptor. The method must be public and declared on the given class. + *

+ * Note: This should only check for stable methods that are expected to stay permanently. + *

+ * + * @param clazz the type to get the given {@code method} on. + * @param method the name to find. + * @return whether the given method exists. + */ + private static boolean doesMethodExist(final Class clazz, final String method) { + for (final Method reflect : clazz.getMethods()) { + if (reflect.getName().equals(method)) { + return true; + } + } + + return false; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 54ca742dd..f95032f6c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,8 @@ rootProject.name = "viaversion-parent" include("adventure") +include("java-compat", "java-compat:java-compat-common", "java-compat:java-compat-8", + "java-compat:java-compat-9", "java-compat:java-compat-16") setupViaSubproject("api") setupViaSubproject("common")