Skip to content

Commit fd3fa25

Browse files
authored
Fix loading package-info classes (#56)
## The issue Currently, SJH's `ModuleClassLoader` is unable to load `package-info` classes, which are placed in the unnamed module when it calls `definePackage`. Java offers 2 ways of defining packages - one with version information and another with a module parameter, which are mutually exclusive. However, our use case requires both package versioning information and the module to be present. `ModuleClassLoader` is designed to only load classes from named modules. When calling `Class.getPackage().getPackageInfo()`, Java will try to load the package-info class using `ClassLoader#findClass(String moduleName, String name)`, passing in `null` for the moduleName parameter as the package is placed in an unnamed module. Here, MCL expects `moduleName` to be non-null at all times. However, that is not true according to java's [documentation](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ClassLoader.html#findResource(java.lang.String,java.lang.String)): > **Parameters:** moduleName - The module name; or null to find a resource in the [unnamed module](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ClassLoader.html#getUnnamedModule()) for this class loader MCL will happily pass this parameter to `Configuration#findModule`, which expects a non-null value, resulting in a NPE. ### Steps to reproduce 1. Invoke `getPackage().getPackageInfo()` on your favourite class, which is loaded by a `ModuleClassLoader` 2. Expect a NPE ### Expected behavior Much like other classes, `package-info` classes should be placed in named modules, too, rather than being loaded in the unnamed module. ## The solution To make java load package-info classes in their module, we must replace the value of the `NamedPackage#module` field. Since java puts no restrictions in place that would prevent us from setting a named module on a `NamedPackage` instance, we reflectively replace the default unnamed module value with the package's actual module. Step 1: Grab the trusted method lookup instance and create a field handle for the package module field ```java var trustedLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); trustedLookupField.setAccessible(true); MethodHandles.Lookup trustedLookup = (MethodHandles.Lookup) trustedLookupField.get(null); Class<?> namedPackage = Class.forName("java.lang.NamedPackage"); PKG_MODULE_HANDLE = trustedLookup.findVarHandle(namedPackage, "module", Module.class); ``` Step 2: After defining each class (necessary for the module to be known), ensure its package is placed in a named module ```java // Get package module Module value = (Module) PKG_MODULE_HANDLE.get(pkg); // Check if the value is not a named module if (value == null || !value.isNamed()) { try { // Replace the value with the loaded class's module PKG_MODULE_HANDLE.set(pkg, module); } catch (Throwable t) { throw new RuntimeException(t); } } ```
1 parent e47246e commit fd3fa25

File tree

6 files changed

+76
-1
lines changed

6 files changed

+76
-1
lines changed

src/main/java/cpw/mods/cl/ModuleClassLoader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ private Class<?> readerToClass(final ModuleReader reader, final ModuleReference
172172
var modroot = this.resolvedRoots.get(ref.descriptor().name());
173173
ProtectionDomainHelper.tryDefinePackage(this, name, modroot.jar().getManifest(), t->modroot.jar().getManifest().getAttributes(t), this::definePackage); // Packages are dirctories, and can't be signed, so use raw attributes instead of signed.
174174
var cs = ProtectionDomainHelper.createCodeSource(toURL(ref.location()), modroot.jar().verifyAndGetSigners(cname, bytes));
175-
return defineClass(name, bytes, 0, bytes.length, ProtectionDomainHelper.createProtectionDomain(cs, this));
175+
var cls = defineClass(name, bytes, 0, bytes.length, ProtectionDomainHelper.createProtectionDomain(cs, this));
176+
ProtectionDomainHelper.trySetPackageModule(cls.getPackage(), cls.getModule());
177+
return cls;
176178
}
177179

178180
protected byte[] maybeTransformClassBytes(final byte[] bytes, final String name, final String context) {

src/main/java/cpw/mods/cl/ProtectionDomainHelper.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cpw.mods.cl;
22

3+
import java.lang.invoke.MethodHandles;
4+
import java.lang.invoke.VarHandle;
35
import java.net.URL;
46
import java.security.*;
57
import java.util.HashMap;
@@ -27,6 +29,34 @@ public static ProtectionDomain createProtectionDomain(CodeSource codeSource, Cla
2729
}
2830
}
2931

32+
private static final VarHandle PKG_MODULE_HANDLE;
33+
static {
34+
try {
35+
// Obtain VarHandle for NamedPackage#module via trusted lookup
36+
var trustedLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
37+
trustedLookupField.setAccessible(true);
38+
MethodHandles.Lookup trustedLookup = (MethodHandles.Lookup) trustedLookupField.get(null);
39+
40+
Class<?> namedPackage = Class.forName("java.lang.NamedPackage");
41+
PKG_MODULE_HANDLE = trustedLookup.findVarHandle(namedPackage, "module", Module.class);
42+
} catch (Throwable t) {
43+
throw new RuntimeException("Error finding package module handle", t);
44+
}
45+
}
46+
47+
static void trySetPackageModule(Package pkg, Module module) {
48+
// Ensure named packages are bound to their module of origin
49+
// Necessary for loading package-info classes
50+
Module value = (Module) PKG_MODULE_HANDLE.get(pkg);
51+
if (value == null || !value.isNamed()) {
52+
try {
53+
PKG_MODULE_HANDLE.set(pkg, module);
54+
} catch (Throwable t) {
55+
throw new RuntimeException("Error setting package module", t);
56+
}
57+
}
58+
}
59+
3060
static Package tryDefinePackage(final ClassLoader classLoader, String name, Manifest man, Function<String, Attributes> trustedEntries, Function<String[], Package> definePackage) throws IllegalArgumentException
3161
{
3262
final var pname = name.substring(0, name.lastIndexOf('.'));
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package cpw.mods.cl.test;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.lang.annotation.Annotation;
7+
import java.util.Arrays;
8+
9+
public class TestPackageInfo {
10+
11+
@Test
12+
public void testPackageInfoAvailability() throws Exception {
13+
// package-info classes can be loaded
14+
TestjarUtil.withTestjar1Setup(cl -> {
15+
String annotationType = "cpw.mods.cl.testjar1.TestAnnotation";
16+
// Reference package through a class to ensure correct behavior of ModuleClassLoader#findClass(String,String)
17+
Class<?> cls = Class.forName("cpw.mods.cl.testjar1.SomeClass", true, cl);
18+
Annotation[] annotations = cls.getPackage().getDeclaredAnnotations();
19+
Assertions.assertTrue(Arrays.stream(annotations).anyMatch(ann -> ann.annotationType().getName().equals(annotationType)), "Expected package to be annotated with " + annotationType);
20+
});
21+
}
22+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package cpw.mods.cl.testjar1;
2+
3+
/**
4+
* Referenced by {@code TestClassLoader}.
5+
*/
6+
public class SomeClass {
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package cpw.mods.cl.testjar1;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.PACKAGE)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface TestAnnotation {
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@TestAnnotation
2+
package cpw.mods.cl.testjar1;
3+

0 commit comments

Comments
 (0)