Skip to content

Commit bbb053a

Browse files
Fix Java 15 (#1025)
- Implemented a fix for the incompatibility with Java 15. This incompatibility was caused by the fact that the lambda generated in the NMS.NetworkManager is a hidden class in J15. Starting in Java 15, final fields in hidden classes can no longer be modified regardless of the 'accessible' flag. (see https://openjdk.java.net/jeps/371 "Using a hidden class", point 3). To circumvent this issue, this retrieves the data from the existing fields in the hidden class (a runnable or a callable) other than the packet. It then retrieves the constructor of the hidden class and instantiates it using the previously-retrieved data and the modified packet instance (this code is only used if the packet instance changed). - Introduced a new ObjectReconstructor class that does all the fields/constructor discovering/accessing etc. The Runnable and Callable methods each get one instance of this class so that we can avoid having to get the fields/constructors and set them accessible every time we want to replace a packet. Co-authored-by: Mark Vainomaa <[email protected]>
1 parent 7bac4ec commit bbb053a

File tree

2 files changed

+144
-15
lines changed

2 files changed

+144
-15
lines changed

src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import com.comphenix.protocol.utility.MinecraftMethods;
3535
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
3636
import com.comphenix.protocol.utility.MinecraftReflection;
37+
import com.comphenix.protocol.utility.ObjectReconstructor;
38+
import com.comphenix.protocol.wrappers.Pair;
3739
import com.comphenix.protocol.wrappers.WrappedGameProfile;
3840
import com.google.common.base.Preconditions;
3941
import io.netty.buffer.ByteBuf;
@@ -48,15 +50,16 @@
4850
import org.bukkit.Bukkit;
4951
import org.bukkit.entity.Player;
5052

53+
import java.lang.reflect.Field;
5154
import java.lang.reflect.InvocationTargetException;
5255
import java.lang.reflect.Method;
5356
import java.net.Socket;
5457
import java.net.SocketAddress;
5558
import java.util.*;
5659
import java.util.Map.Entry;
5760
import java.util.concurrent.Callable;
61+
import java.util.concurrent.ConcurrentHashMap;
5862
import java.util.concurrent.atomic.AtomicInteger;
59-
6063
/**
6164
* Represents a channel injector.
6265
* @author Kristian
@@ -88,8 +91,23 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
8891
} catch (Exception ex) {
8992
throw new RuntimeException("Encountered an error caused by a reload! Please properly restart your server!", ex);
9093
}
94+
95+
Method hiddenClassMethod = null;
96+
try {
97+
if (Float.parseFloat(System.getProperty("java.class.version")) >= 59) {
98+
hiddenClassMethod = Class.class.getMethod("isHidden");
99+
}
100+
} catch (NoSuchMethodException ignored) {
101+
102+
}
103+
IS_HIDDEN_CLASS = hiddenClassMethod;
91104
}
92105

106+
// Starting in Java 15 (59), the lambdas are hidden classes and we cannot use reflection to update
107+
// the values anymore. Instead, the object will have to be reconstructed.
108+
private static final Map<Class<?>, ObjectReconstructor<?>> RECONSTRUCTORS = new ConcurrentHashMap<>();
109+
private static final Method IS_HIDDEN_CLASS;
110+
93111
// Saved accessors
94112
private Method decodeBuffer;
95113
private Method encodeBuffer;
@@ -295,39 +313,39 @@ public ChannelPipeline pipeline() {
295313

296314
@Override
297315
protected <T> Callable<T> onMessageScheduled(final Callable<T> callable, FieldAccessor packetAccessor) {
298-
final PacketEvent event = handleScheduled(callable, packetAccessor);
316+
Pair<Callable<T>, PacketEvent> handled = handleScheduled(callable, packetAccessor);
299317

300318
// Handle cancelled events
301-
if (event != null && event.isCancelled())
319+
if (handled.getSecond() != null && handled.getSecond().isCancelled())
302320
return null;
303321

304322
return () -> {
305323
T result;
306324

307325
// This field must only be updated in the pipeline thread
308-
currentEvent = event;
309-
result = callable.call();
326+
currentEvent = handled.getSecond();
327+
result = handled.getFirst().call();
310328
currentEvent = null;
311329
return result;
312330
};
313331
}
314332

315333
@Override
316334
protected Runnable onMessageScheduled(final Runnable runnable, FieldAccessor packetAccessor) {
317-
final PacketEvent event = handleScheduled(runnable, packetAccessor);
335+
Pair<Runnable, PacketEvent> handled = handleScheduled(runnable, packetAccessor);
318336

319337
// Handle cancelled events
320-
if (event != null && event.isCancelled())
338+
if (handled.getSecond() != null && handled.getSecond().isCancelled())
321339
return null;
322340

323341
return () -> {
324-
currentEvent = event;
325-
runnable.run();
342+
currentEvent = handled.getSecond();
343+
handled.getFirst().run();
326344
currentEvent = null;
327345
};
328346
}
329347

330-
PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
348+
<T> Pair<T, PacketEvent> handleScheduled(T instance, FieldAccessor accessor) {
331349
// Let the filters handle this packet
332350
Object original = accessor.get(instance);
333351

@@ -338,9 +356,9 @@ PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
338356
if (marker != null) {
339357
PacketEvent result = new PacketEvent(ChannelInjector.class);
340358
result.setNetworkMarker(marker);
341-
return result;
359+
return new Pair<>(instance, result);
342360
} else {
343-
return BYPASSED_PACKET;
361+
return new Pair<>(instance, BYPASSED_PACKET);
344362
}
345363
}
346364

@@ -350,11 +368,12 @@ PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
350368

351369
// Change packet to be scheduled
352370
if (original != changed) {
353-
accessor.set(instance, changed);
371+
instance = (T) (isHiddenClass(instance.getClass()) ?
372+
updatePacketMessageReconstruct(instance, changed, accessor) :
373+
updatePacketMessageSetReflection(instance, changed, accessor));
354374
}
355375
}
356-
357-
return event != null ? event : BYPASSED_PACKET;
376+
return new Pair<>(instance, event != null ? event : BYPASSED_PACKET);
358377
}
359378
});
360379

@@ -363,6 +382,30 @@ PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
363382
}
364383
}
365384

385+
/**
386+
* Changes the packet in a packet message using a {@link FieldAccessor}.
387+
*/
388+
private static Object updatePacketMessageSetReflection(Object instance, Object newPacket, FieldAccessor accessor) {
389+
accessor.set(instance, newPacket);
390+
return instance;
391+
}
392+
393+
/**
394+
* Changes the packet in a packet message using a {@link ObjectReconstructor}.
395+
*/
396+
private static Object updatePacketMessageReconstruct(Object instance, Object newPacket, FieldAccessor accessor) {
397+
final ObjectReconstructor<?> objectReconstructor =
398+
RECONSTRUCTORS.computeIfAbsent(instance.getClass(), ObjectReconstructor::new);
399+
400+
final Object[] values = objectReconstructor.getValues(instance);
401+
final Field[] fields = objectReconstructor.getFields();
402+
for (int idx = 0; idx < fields.length; ++idx)
403+
if (fields[idx].equals(accessor.getField()))
404+
values[idx] = newPacket;
405+
406+
return objectReconstructor.reconstruct(values);
407+
}
408+
366409
/**
367410
* Determine if the given object is a compressor or decompressor.
368411
* @param handler - object to test.
@@ -943,4 +986,15 @@ ChannelInjector getChannelInjector() {
943986
public Channel getChannel() {
944987
return originalChannel;
945988
}
989+
990+
private static boolean isHiddenClass(Class<?> clz) {
991+
if (IS_HIDDEN_CLASS == null) {
992+
return false;
993+
}
994+
try {
995+
return (Boolean) IS_HIDDEN_CLASS.invoke(clz);
996+
} catch (Exception e) {
997+
throw new RuntimeException("Failed to determine whether class '" + clz.getName() + "' is hidden or not", e);
998+
}
999+
}
9461000
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.comphenix.protocol.utility;
2+
3+
import java.lang.reflect.Constructor;
4+
import java.lang.reflect.Field;
5+
import java.lang.reflect.InvocationTargetException;
6+
7+
/**
8+
* This class can be used to reconstruct objects.
9+
*
10+
* Note that it is limited to classes where both the order and number of member variables matches the order and number
11+
* of arguments for the first constructor. This means that this class is mostly useful for classes generated by lambdas.
12+
*
13+
* @param <T> The type of the object to reconstruct.
14+
* @author Pim
15+
*/
16+
public class ObjectReconstructor<T> {
17+
18+
private final Class<T> clz;
19+
private final Field[] fields;
20+
private final Constructor<?> ctor;
21+
22+
public ObjectReconstructor(final Class<T> clz) {
23+
this.clz = clz;
24+
this.fields = clz.getDeclaredFields();
25+
for (Field field : fields)
26+
field.setAccessible(true);
27+
this.ctor = clz.getDeclaredConstructors()[0];
28+
this.ctor.setAccessible(true);
29+
}
30+
31+
/**
32+
* Gets the values of all member variables of the provided instance.
33+
* @param instance The instance for which to get all the member variables.
34+
* @return The values of the member variables from the instance.
35+
*/
36+
public Object[] getValues(final Object instance) {
37+
final Object[] values = new Object[fields.length];
38+
for (int idx = 0; idx < fields.length; ++idx)
39+
try {
40+
values[idx] = fields[idx].get(instance);
41+
} catch (IllegalAccessException e) {
42+
throw new RuntimeException("Failed to access field: " + fields[idx].getName() +
43+
" for class: " + clz.getName(), e);
44+
}
45+
return values;
46+
}
47+
48+
/**
49+
* Gets the fields in the class.
50+
* @return The fields.
51+
*/
52+
public Field[] getFields() {
53+
return fields;
54+
}
55+
56+
/**
57+
* Creates a new instance of the class using the new values.
58+
* @param values The new values for the member variables of the class.
59+
* @return The new instance.
60+
*/
61+
public T reconstruct(final Object[] values) {
62+
if (values.length != fields.length)
63+
throw new RuntimeException("Mismatched number of arguments for class: " + clz.getName());
64+
65+
try {
66+
return (T) ctor.newInstance(values);
67+
} catch (InstantiationException e) {
68+
throw new RuntimeException("Failed to reconstruct object of type: " + clz.getName(), e);
69+
} catch (IllegalAccessException e) {
70+
throw new RuntimeException("Failed to access constructor of type: " + clz.getName(), e);
71+
} catch (InvocationTargetException e) {
72+
throw new RuntimeException("Failed to invoke constructor of type: " + clz.getName(), e);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)