Skip to content

Commit 5c85c01

Browse files
authored
feat: conditionally disable netty unsafe to leverage the ffm api (#1664)
### Motivation In netty 4.2.3 a new type of ByteBuffer cleaner (or allocator) will be introduced that uses `malloc` and `free` directly using the ffm api on java 24+. In order to enable this cleaner, unsafe has to be unavailable/disabled. Since this can be a performance issue (as the ByteBuffer lifecycle might be controlled by the garbage collector if unsafe is unavailable) we only disable unsafe when the new cleaner implementation is available. ### Modification Add a transformer that checks if all conditions are met to disable unsafe in order to enable the ffm-based cleaner implementation. If that is the case, the `PlatformDependent0#explicitNoUnsafeCause0()` method will be overridden to always return an UOE to indicate that unsafe is explicitly disabled. ### Result On netty 4.2.3+ unsafe will be disabled in favor of the ffm-linker based cleaner implementation, but only is all necessary preconditions to enable the cleaner are met.
1 parent a38f471 commit 5c85c01

4 files changed

Lines changed: 142 additions & 2 deletions

File tree

wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/ClassTransformer.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.classfile.ClassModel;
2020
import java.lang.classfile.ClassTransform;
2121
import lombok.NonNull;
22+
import org.jetbrains.annotations.Nullable;
2223

2324
/**
2425
* A transformer for a class which gets called before the class is actually put into usage. A transformer can
@@ -38,7 +39,31 @@ public interface ClassTransformer {
3839
* @throws NullPointerException if the given original class model is null.
3940
*/
4041
@NonNull
41-
ClassTransform provideClassTransform(@NonNull ClassModel original);
42+
default ClassTransform provideClassTransform(@NonNull ClassModel original) {
43+
throw new UnsupportedOperationException(
44+
"at least one provideClassTransform() method must be overridden by " + this.getClass().getName());
45+
}
46+
47+
/**
48+
* Provides the class transform that will be used to transform the class data. This method should only be called if a
49+
* prior check to {@link #classTransformWillingness(String)} did not indicate a rejection of the class. This method
50+
* additionally provides the module and class loader of the class being transformed, which should rarely be a concern
51+
* to the transformer.
52+
*
53+
* @param original the original class model that is being transformed.
54+
* @param module the module of the class, null if it is the unnamed module.
55+
* @param loader the class loader of the class, null if it is the bootstrap loader.
56+
* @return the class transform to apply to the target class.
57+
* @throws NullPointerException if the given original class model is null.
58+
*/
59+
@NonNull
60+
default ClassTransform provideClassTransform(
61+
@NonNull ClassModel original,
62+
@Nullable Module module,
63+
@Nullable ClassLoader loader
64+
) {
65+
return this.provideClassTransform(original);
66+
}
4267

4368
/**
4469
* Checks if this class transformer is willing to transform the class with the given internal name. If this method is

wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/DefaultClassTransformerRegistry.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ private record RegisteredClassTransformer(
8080
*/
8181
@Override
8282
public byte[] transform(
83+
@Nullable Module module,
8384
@Nullable ClassLoader loader,
8485
@NonNull String className,
8586
@Nullable Class<?> classBeingRedefined,
@@ -121,7 +122,7 @@ public byte[] transform(
121122
// apply the transformation to the provided class file
122123
var classFile = ClassFile.of(classHierarchyResolverOption);
123124
var classModel = classFile.parse(classfileBuffer);
124-
var classTransform = this.transformer.provideClassTransform(classModel);
125+
var classTransform = this.transformer.provideClassTransform(classModel, module, loader);
125126
return classFile.transformClass(classModel, classTransform);
126127
} catch (Exception exception) {
127128
LOGGER.error("Failed to transform class {} using transformer {}", className, transformerClassName, exception);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2019-2024 CloudNetService team & contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package eu.cloudnetservice.wrapper.impl.transform.netty;
18+
19+
import eu.cloudnetservice.wrapper.impl.transform.ClassTransformer;
20+
import java.lang.classfile.ClassModel;
21+
import java.lang.classfile.ClassTransform;
22+
import java.lang.classfile.MethodModel;
23+
import java.lang.constant.ClassDesc;
24+
import java.lang.constant.ConstantDescs;
25+
import java.lang.constant.MethodTypeDesc;
26+
import lombok.NonNull;
27+
import org.jetbrains.annotations.ApiStatus;
28+
import org.jetbrains.annotations.Nullable;
29+
30+
/**
31+
* Transformer to disable the use of unsafe in netty when the {@code CleanerJava24Linker} (introduced in netty 4.2.3) is
32+
* available. This should result in a similar performance than the use of unsafe, without using the deprecated
33+
* memory-access methods in {@code sun.misc.Unsafe}.
34+
*
35+
* @since 4.0
36+
*/
37+
@ApiStatus.Internal
38+
public final class ConditionalUnsafeDisableTransform implements ClassTransformer {
39+
40+
private static final String SYS_PROP_NO_UNSAFE = "io.netty.noUnsafe";
41+
private static final String CN_PLATFORM_DEPENDENT0 = "PlatformDependent0";
42+
private static final String CN_CLEANER_LINKER_JAVA24 = "CleanerJava24Linker";
43+
44+
private static final String MN_EXPLICIT_NO_UNSAFE_CAUSE = "explicitNoUnsafeCause0";
45+
private static final MethodTypeDesc MTD_EXPLICIT_NO_UNSAFE_CAUSE = MethodTypeDesc.of(ConstantDescs.CD_Throwable);
46+
47+
private static final ClassDesc CD_UNSUPPORTED_OP_EX = ClassDesc.of(UnsupportedOperationException.class.getName());
48+
private static final MethodTypeDesc MTD_UNSUPPORTED_OP_EX_NEW =
49+
MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_String);
50+
51+
/**
52+
* Constructs a new instance of this transformer, usually done via SPI.
53+
*/
54+
public ConditionalUnsafeDisableTransform() {
55+
var explicitNoUnsafeValue = System.getProperty(SYS_PROP_NO_UNSAFE);
56+
if (explicitNoUnsafeValue != null) {
57+
throw new UnsupportedOperationException("transformer disabled as " + SYS_PROP_NO_UNSAFE + " is specified");
58+
}
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
@Override
65+
public @NonNull ClassTransform provideClassTransform(
66+
@NonNull ClassModel original,
67+
@Nullable Module module,
68+
@Nullable ClassLoader loader
69+
) {
70+
// check if native access is enabled for the module, this assumes
71+
// that native access is always enabled for the unnamed module
72+
if (module != null && !module.isNativeAccessEnabled()) {
73+
return ClassTransform.ACCEPT_ALL;
74+
}
75+
76+
// check if the CleanerJava24Linker class is available, in which case we want to prefer
77+
// this cleaner implementation over unsafe, so we disable the use of unsafe in netty. this
78+
// might come with some side effects for other libs. therefore, the transformer can be disabled.
79+
var packageName = original.thisClass().asSymbol().packageName().replace('.', '/');
80+
var cleanerJava24LinkerClassName = String.format("%s/%s.class", packageName, CN_CLEANER_LINKER_JAVA24);
81+
var cleanerLinkerResource = switch (loader) {
82+
case ClassLoader cl -> cl.getResource(cleanerJava24LinkerClassName);
83+
case null -> ClassLoader.getSystemResource(cleanerJava24LinkerClassName);
84+
};
85+
if (cleanerLinkerResource != null) {
86+
return (builder, element) -> {
87+
if (element instanceof MethodModel mm
88+
&& mm.methodName().equalsString(MN_EXPLICIT_NO_UNSAFE_CAUSE)
89+
&& mm.methodTypeSymbol().equals(MTD_EXPLICIT_NO_UNSAFE_CAUSE)) {
90+
builder.withMethodBody(mm.methodName(), mm.methodType(), mm.flags().flagsMask(), code -> code
91+
.new_(CD_UNSUPPORTED_OP_EX)
92+
.dup()
93+
.ldc("Unsafe auto-disabled on J24+ by CloudNet when linker-based cleaner is available")
94+
.invokespecial(CD_UNSUPPORTED_OP_EX, ConstantDescs.INIT_NAME, MTD_UNSUPPORTED_OP_EX_NEW)
95+
.areturn());
96+
} else {
97+
builder.with(element);
98+
}
99+
};
100+
}
101+
102+
return ClassTransform.ACCEPT_ALL;
103+
}
104+
105+
/**
106+
* {@inheritDoc}
107+
*/
108+
@Override
109+
public @NonNull TransformWillingness classTransformWillingness(@NonNull String internalClassName) {
110+
var isPlatformDependent0 = internalClassName.endsWith(CN_PLATFORM_DEPENDENT0);
111+
return isPlatformDependent0 ? TransformWillingness.ACCEPT : TransformWillingness.REJECT;
112+
}
113+
}

wrapper-jvm/impl/src/main/resources/META-INF/services/eu.cloudnetservice.wrapper.impl.transform.ClassTransformer

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ eu.cloudnetservice.wrapper.impl.transform.bukkit.WorldEditJava8DetectorTransform
2323
eu.cloudnetservice.wrapper.impl.transform.bukkit.FAWEWorldEditDownloadURLTransformer
2424
eu.cloudnetservice.wrapper.impl.transform.netty.OldEpollDisableTransformer
2525
eu.cloudnetservice.wrapper.impl.transform.spark.OldAsyncProfilerDisableTransformer
26+
eu.cloudnetservice.wrapper.impl.transform.netty.ConditionalUnsafeDisableTransform

0 commit comments

Comments
 (0)