-
-
Notifications
You must be signed in to change notification settings - Fork 270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Java 15 #1025
Fix Java 15 #1025
Conversation
- Implemented a proof of concept 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 proof of concept fix 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).
- Fixed the patch breaking on Tuinity (and maybe Paper?) because its generated Runnable had a different number of variables, causing an exception when trying to instantiate a new instance with an incorrect number of variables. This update changes it so that it can use any number / order of variables. The variable of the same type as the packet will be replaced by the changed packet (this applies to all variables of this type, but there's only ever 1).
- 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.
I like it! The ObjectReconstructor seems like a pretty elegant solution. Have you tested with MC 1.8 and Java 8 to make sure it doesn't break backwards compatibility? Also it doesn't seem like there would be much of a performance or memory hit, but have you done any profiling? |
Thanks for your response! This is what I've tested: For every test, I used a simple test plugin that replaces a packet for falling sand with a packet for falling dirt (to ensure the packet-replacement code is fired). This allows me to visually see the result of changing the packet. Additionally, I used LibsDisguises as a more complete plugin. Java 8 and 15. On Minecraft 1.8.8/9 (server 1.8.8, client 1.8.9), this PR worked fine for both plugins using Java 8. However, I was unable to join the server when it ran Java 11+ (I did not test the versions in between). However, this was the case even without any plugins installed, so I guess the server just doesn't work on newer versions of Java. On 1.12.2 and 1.16.4, both Java 8 and Java 15 worked without any issues. I am not particularly experienced with profiling, but as far as I could see, the performance impact is negligible. |
While playing around with it a bit more, I just ran into an issue. I'm not entirely sure what it is yet, but please don't merge it yet. (wish I could mark it as a draft). Update: The issue happens specifically on Tuinity. It happens because is that there is more than 1 type of Runnable being used (the second one being an What would be your preference? Add another field for the specific LazyRunnable (so we have 1 ObjectReconstructor for each of the following: LazyRunnable, Runnable, and Callable) or play it safe and use a map? |
@PimvanderLoos I'm not really fond that you lifted my idea from the commit(s) I linked under #978. Please add appropriate credit. |
@mikroskeem I never actually saw your commit. I'd already written most of the changes, just forgot about it until I saw a message about a post in that thread. Most of the fun in this little project has been figuring out the root cause of the issue, after all ;) I just checked if a PR/commit to master had been made to resolve the issue before I picked this back up. |
- Added hardcoded support for Netty's LazyRunnable as used by Tuinity by adding a specific ObjectReconstructor field for it.
- The new ObjectReconstructor method for changing the packet in a schedule packet message is now only used on java 15+. All older versions of Java will use the old method of modifying the packet using reflection, as that is simpler. Inspired by @mikroskeem's efforts towards this PR.
While going over @mikroskeem's commits, I saw that you use the old reflection method of updating values when the class isn't hidden. I thought this was a good idea, so I added such a system in this PR. Also, I implemented the issue above regarding netty's My interpretation of the JEP is that these lambdas will not get GC'ed and simply get reused for as long as the NetworkManager class is loaded. They are defined with a STRONG relationship with the NetworkManager class. This would mean that using a |
Indeed, I didn't bother making it compatible with Java <15 yet because I run Java 15 in production myself. By the way, I would check if
I would indeed replace hardcoded fields with a map - you'll never know what changes are people making to their servers, sooner or later would someone open a PR to add a new case for a new class.
I haven't read JEP myself yet, so I wasn't sure when will lambdas get GC-ed. Hence I went with WeakHashMap & synchronizing on it. But if that interpretation is correct, then ConcurrentHashMap is suitable there. |
- Switched to using a ConcurrentHashmap for storing the different ObjectReconstructors instead of hardcoding the supported classes.
Why would it be more fragile? Surely all versions going forward will have support for hidden classes and the versions before didn't. That's why I ended up doing it this way, as it's shorter and cleaner than checking if the method exists.
Yeah, I agree. I've switched to a ConcurrentHashMap now.
The relevant part in the JEP is here: And as you can see here, lambdas are indeed created using the STRONG option. |
That works by assuming that the class where packet should be replaced is always a lambda. Some Spigot forks replace lambda with explicit inner class (or do other weird things), thus breaking the workaround right now. Calling Previously I stated that simply checking the method presence is sufficient, but it's not. As a side note, I should really keep myself away from issue trackers when tired. |
Well, as long as the number+type+order of member variables of a class match the number+type+order of arguments of the first constructor, the ObjectReconstructor is perfectly capable of reconstructing the object. This also includes explicit inner classes (as was the case in all versions before 1.13.0, which was the first version to use a lambda) so that's not a problem. Regarding some fork doing some weird stuff here, I'd argue that we'll cross that bridge when we get to it. However, if the class isn't hidden, it would be preferable to use the simple update method instead of recreating the object. So how about something like this: private static final Function<Class<?>, Boolean> IS_HIDDEN;
static {
Function<Class<?>, Boolean> isHidden;
try {
isHidden = Class::isHidden;
} catch (NoSuchMethodError e) {
isHidden = (c) -> false;
}
IS_HIDDEN = isHidden;
}
...
if (original != changed)
instance = (T) (IS_HIDDEN.apply(instance.getClass()) ?
updatePacketMessageReconstruct(instance, changed, accessor) :
updatePacketMessageSetReflection(instance, changed, accessor)); This way we ensure that only hidden classes are reconstructed and older versions of Java and any forks and versions of MC that use explicit inner classes use the simple update method. However, it means that ProtocolLib would have to be compiled on Java 15+, which might not be desirable and up to @dmulloy2 to decide. Also, I ran a quick benchmark using JMH to see the actual differences between updating a value using reflection and reconstructing the object: The
As you can see, using the |
It's as easy as compiling ProtocolLib with newer JDK, so I don't see a problem in that. If that's not viable for dmulloy2, then a different solution should be found. Alternative to compiling with Java 15 is unsurprisingly using reflection. I threw quick solution together yesterday to fix the object construction issue I ran into yesterday. @PimvanderLoos since you already have working JMH benchmark, then could you throw my changes in and see how it performs? As a side note, I would use Predicate instead of Function, as that'll avoid using Boolean object. |
Yeah, I wanted to avoid using reflection, but it's only fair if I benchmark that as well. What I did for the benchmarks:
Results:
As you can see, accessing the For completeness' sake: I'm using JMH's default values with the exception of the OutputTimeUnit, which was set to nanoseconds. All benchmarks were run on a 3900x. |
This fixes the assumption that target class is always a lambda, while it might not be, thus causing ObjectReconstructor to fail.
I applied @mikroskeem's commit that switches to using reflection to check if a class is hidden or not, as based on the benchmarks, this seems like the way to go. Just to make sure everything still works as expected, I ran several manual tests. For 1.8, I only ran the test on J8, as J11+ doesn't work (I think J9+ doesn't work either, but I don't have either J9 or J10). For every test, I used a simple test plugin that replaces packets of falling sand with packets for falling dirt. On MC 1.12+, I also used LibsDisguises. For MC 1.16, I used both Spigot and Tuinity (and for Tuinity, only J11+, as it doesn't run lower versions). In total, I tested this PR on 9 different server/Java combinations and found no issues. In conclusion, I think this PR is now ready. |
Been running this on our 1.16.4 paper servers running OpenJDK 15.0.1. Seems to work great, no problems so far. |
Is it possible for you to insert a link to download your ProtocolLib.jar here? |
This comment has been minimized.
This comment has been minimized.
Perfect, thank you for being so thorough! |
Available in the latest dev build: https://ci.dmulloy2.net/job/ProtocolLib/lastSuccessfulBuild/ |
Thanks for merging it! And @mikroskeem, it has been a pleasure working with you on this. :) |
This PR fixes ProtocolLib's incompatibility with Java 15 on all versions of Minecraft after (and including) 1.13.0.
This incompatibility was caused by the fact that starting in Minecraft 1.13.0, the NetworkManager uses a lambda for Runnables/Callables that are used to send packets. Starting in Java 15, the classes generated by these lambdas are hidden.
Unlike for regular classes, you cannot use reflection to update final fields (regardless of Field#setAccessible(true)), as described in the JEP (see "Using a hidden class", point 3).
However, while changing final fields is not possible, reading them and accessing the class's constructor still works. Therefore, this PR introduces an ObjectReconstructor class that stores all the fields and the constructor of a class (at most once for a Callable and for a Runnable each). When a packet in a Callable/Runnable needs to be modified, this class is used to read the values of all the member variables, update the packet value, and construct a new Callable/Runnable instance. This new instance is then used for the remainder of the message handling process.
This PR fixes #978, fixes #1028.