8294962: Convert java.base/jdk.internal.module package to use the Classfile API to modify and write module-info.class

Reviewed-by: alanb, mchung
This commit is contained in:
Adam Sotona 2023-03-15 07:09:55 +00:00
parent 065d3e0d58
commit 714b5f036f
8 changed files with 185 additions and 234 deletions

View File

@ -250,44 +250,20 @@ public class Classfile {
* @return the classfile bytes
*/
public static byte[] buildModule(ModuleAttribute moduleAttribute) {
return buildModule(moduleAttribute, List.of(), clb -> {});
return buildModule(moduleAttribute, clb -> {});
}
/**
* Build a module descriptor into a byte array.
* @param moduleAttribute the {@code Module} attribute
* @param packages additional module packages
* @return the classfile bytes
*/
public static byte[] buildModule(ModuleAttribute moduleAttribute,
List<PackageDesc> packages) {
return buildModule(moduleAttribute, packages, clb -> {});
}
/**
* Build a module descriptor into a byte array.
* @param moduleAttribute the {@code Module} attribute
* @param packages additional module packages
* @param handler a handler that receives a {@link ClassBuilder}
* @return the classfile bytes
*/
public static byte[] buildModule(ModuleAttribute moduleAttribute,
List<PackageDesc> packages,
Consumer<? super ClassBuilder> handler) {
return build(ClassDesc.of("module-info"), clb -> {
clb.withFlags(AccessFlag.MODULE);
clb.with(moduleAttribute);
if (!packages.isEmpty()) {
var cp = clb.constantPool();
var allPackages = new LinkedHashSet<PackageEntry>();
for (var exp : moduleAttribute.exports()) allPackages.add(AbstractPoolEntry.maybeClone(cp, exp.exportedPackage()));
for (var opn : moduleAttribute.opens()) allPackages.add(AbstractPoolEntry.maybeClone(cp, opn.openedPackage()));
boolean emitMPA = false;
for (var p : packages)
emitMPA |= allPackages.add(cp.packageEntry(p));
if(emitMPA)
clb.with(new UnboundAttribute.UnboundModulePackagesAttribute(allPackages));
}
handler.accept(clb);
});
}
@ -299,33 +275,19 @@ public class Classfile {
*/
public static void buildModuleTo(Path path,
ModuleAttribute moduleAttribute) throws IOException {
buildModuleTo(path, moduleAttribute, List.of(), clb -> {});
buildModuleTo(path, moduleAttribute, clb -> {});
}
/**
* Build a module descriptor into a file.
* @param path the file to write
* @param moduleAttribute the {@code Module} attribute
* @param packages additional module packages
*/
public static void buildModuleTo(Path path,
ModuleAttribute moduleAttribute,
List<PackageDesc> packages) throws IOException {
buildModuleTo(path, moduleAttribute, packages, clb -> {});
}
/**
* Build a module descriptor into a file.
* @param path the file to write
* @param moduleAttribute the {@code Module} attribute
* @param packages additional module packages
* @param handler a handler that receives a {@link ClassBuilder}
*/
public static void buildModuleTo(Path path,
ModuleAttribute moduleAttribute,
List<PackageDesc> packages,
Consumer<? super ClassBuilder> handler) throws IOException {
Files.write(path, buildModule(moduleAttribute, packages, handler));
Files.write(path, buildModule(moduleAttribute, handler));
}
public static final int MAGIC_NUMBER = 0xCAFEBABE;

View File

@ -142,6 +142,19 @@ public sealed interface ModuleAttribute
return mb.build();
}
/**
* {@return a {@code Module} attribute}
*
* @param moduleName the module name
* @param attrHandler a handler that receives a {@link ModuleAttributeBuilder}
*/
static ModuleAttribute of(ModuleEntry moduleName,
Consumer<ModuleAttributeBuilder> attrHandler) {
var mb = new ModuleAttributeBuilderImpl(moduleName);
attrHandler.accept(mb);
return mb.build();
}
public sealed interface ModuleAttributeBuilder
permits ModuleAttributeBuilderImpl {

View File

@ -49,11 +49,15 @@ public final class ModuleAttributeBuilderImpl
private final Set<ClassEntry> uses = new LinkedHashSet<>();
private final Set<ModuleProvideInfo> provides = new LinkedHashSet<>();
public ModuleAttributeBuilderImpl(ModuleDesc moduleName) {
this.moduleEntry = TemporaryConstantPool.INSTANCE.moduleEntry(TemporaryConstantPool.INSTANCE.utf8Entry(moduleName.moduleName()));
public ModuleAttributeBuilderImpl(ModuleEntry moduleName) {
this.moduleEntry = moduleName;
this.moduleFlags = 0;
}
public ModuleAttributeBuilderImpl(ModuleDesc moduleName) {
this(TemporaryConstantPool.INSTANCE.moduleEntry(TemporaryConstantPool.INSTANCE.utf8Entry(moduleName.moduleName())));
}
@Override
public ModuleAttribute build() {
return new UnboundAttribute.UnboundModuleAttribute(moduleEntry, moduleFlags, moduleVersion,

View File

@ -28,21 +28,23 @@ package jdk.internal.module;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.constant.ClassDesc;
import java.lang.module.ModuleDescriptor.Version;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import jdk.internal.classfile.Classfile;
import jdk.internal.classfile.ClassTransform;
import jdk.internal.classfile.attribute.ModuleAttribute;
import jdk.internal.classfile.attribute.ModuleHashInfo;
import jdk.internal.classfile.attribute.ModuleHashesAttribute;
import jdk.internal.classfile.attribute.ModuleMainClassAttribute;
import jdk.internal.classfile.attribute.ModulePackagesAttribute;
import jdk.internal.classfile.attribute.ModuleResolutionAttribute;
import jdk.internal.classfile.attribute.ModuleTargetAttribute;
import jdk.internal.classfile.java.lang.constant.ModuleDesc;
import jdk.internal.classfile.java.lang.constant.PackageDesc;
import jdk.internal.org.objectweb.asm.Attribute;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.ModuleVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.commons.ModuleHashesAttribute;
import jdk.internal.org.objectweb.asm.commons.ModuleResolutionAttribute;
import jdk.internal.org.objectweb.asm.commons.ModuleTargetAttribute;
/**
* Utility class to extend a module-info.class with additional attributes.
@ -148,90 +150,53 @@ public final class ModuleInfoExtender {
* be discarded.
*/
public byte[] toByteArray() throws IOException {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS
+ ClassWriter.COMPUTE_FRAMES);
ClassReader cr = new ClassReader(in);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM7, cw) {
@Override
public ModuleVisitor visitModule(String name, int flags, String version) {
Version v = ModuleInfoExtender.this.version;
String vs = (v != null) ? v.toString() : version;
ModuleVisitor mv = super.visitModule(name, flags, vs);
// ModuleMainClass attribute
if (mainClass != null) {
mv.visitMainClass(mainClass.replace('.', '/'));
}
// ModulePackages attribute
if (packages != null) {
packages.stream()
.sorted()
.forEach(pn -> mv.visitPackage(pn.replace('.', '/')));
}
return new ModuleVisitor(Opcodes.ASM7, mv) {
public void visitMainClass(String existingMainClass) {
// skip main class if there is a new value
if (mainClass == null) {
super.visitMainClass(existingMainClass);
}
}
public void visitPackage(String existingPackage) {
// skip packages if there is a new set of packages
if (packages == null) {
super.visitPackage(existingPackage);
}
}
};
var cm = Classfile.parse(in.readAllBytes());
Version v = ModuleInfoExtender.this.version;
return cm.transform(ClassTransform.endHandler(clb -> {
// ModuleMainClass attribute
if (mainClass != null) {
clb.with(ModuleMainClassAttribute.of(ClassDesc.of(mainClass)));
}
@Override
public void visitAttribute(Attribute attr) {
String name = attr.type;
// drop existing attributes if there are replacements
if (name.equals(ClassFileConstants.MODULE_TARGET)
&& targetPlatform != null)
return;
if (name.equals(ClassFileConstants.MODULE_RESOLUTION)
&& moduleResolution != null)
return;
if (name.equals(ClassFileConstants.MODULE_HASHES)
&& hashes != null)
return;
super.visitAttribute(attr);
// ModulePackages attribute
if (packages != null) {
List<PackageDesc> packageNames = packages.stream()
.sorted()
.map(PackageDesc::of)
.toList();
clb.with(ModulePackagesAttribute.ofNames(packageNames));
}
};
List<Attribute> attrs = new ArrayList<>();
attrs.add(new ModuleTargetAttribute());
attrs.add(new ModuleResolutionAttribute());
attrs.add(new ModuleHashesAttribute());
cr.accept(cv, attrs.toArray(new Attribute[0]), 0);
// add ModuleTarget, ModuleResolution and ModuleHashes attributes
if (targetPlatform != null) {
cw.visitAttribute(new ModuleTargetAttribute(targetPlatform));
}
if (moduleResolution != null) {
int flags = moduleResolution.value();
cw.visitAttribute(new ModuleResolutionAttribute(flags));
}
if (hashes != null) {
String algorithm = hashes.algorithm();
List<String> names = new ArrayList<>();
List<byte[]> values = new ArrayList<>();
for (String name : hashes.names()) {
names.add(name);
values.add(hashes.hashFor(name));
// ModuleTarget, ModuleResolution and ModuleHashes attributes
if (targetPlatform != null) {
clb.with(ModuleTargetAttribute.of(targetPlatform));
}
cw.visitAttribute(new ModuleHashesAttribute(algorithm, names, values));
}
return cw.toByteArray();
if (moduleResolution != null) {
clb.with(ModuleResolutionAttribute.of(moduleResolution.value()));
}
if (hashes != null) {
clb.with(ModuleHashesAttribute.of(
hashes.algorithm(),
hashes.hashes().entrySet().stream().map(he ->
ModuleHashInfo.of(ModuleDesc.of(
he.getKey()),
he.getValue())).toList()));
}
}).andThen((clb, cle) -> {
if (v != null && cle instanceof ModuleAttribute ma) {
clb.with(ModuleAttribute.of(
ma.moduleName(),
ma.moduleFlagsMask(),
clb.constantPool().utf8Entry(v.toString()),
ma.requires(),
ma.exports(),
ma.opens(),
ma.uses(),
ma.provides()));
} else {
clb.accept(cle);
}
}));
}
/**

View File

@ -26,17 +26,22 @@ package jdk.internal.module;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.constant.ClassDesc;
import java.lang.module.ModuleDescriptor;
import java.lang.reflect.AccessFlag;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.stream.Stream;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.ModuleVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.commons.ModuleResolutionAttribute;
import jdk.internal.org.objectweb.asm.commons.ModuleTargetAttribute;
import static jdk.internal.org.objectweb.asm.Opcodes.*;
import jdk.internal.classfile.Classfile;
import jdk.internal.classfile.java.lang.constant.PackageDesc;
import jdk.internal.classfile.attribute.ModuleAttribute;
import jdk.internal.classfile.attribute.ModuleExportInfo;
import jdk.internal.classfile.attribute.ModuleMainClassAttribute;
import jdk.internal.classfile.attribute.ModuleOpenInfo;
import jdk.internal.classfile.attribute.ModulePackagesAttribute;
import jdk.internal.classfile.attribute.ModuleResolutionAttribute;
import jdk.internal.classfile.attribute.ModuleRequireInfo;
import jdk.internal.classfile.attribute.ModuleTargetAttribute;
import jdk.internal.classfile.constantpool.ModuleEntry;
/**
* Utility class to write a ModuleDescriptor as a module-info.class.
@ -46,33 +51,31 @@ public final class ModuleInfoWriter {
private static final Map<ModuleDescriptor.Modifier, Integer>
MODULE_MODS_TO_FLAGS = Map.of(
ModuleDescriptor.Modifier.OPEN, ACC_OPEN,
ModuleDescriptor.Modifier.SYNTHETIC, ACC_SYNTHETIC,
ModuleDescriptor.Modifier.MANDATED, ACC_MANDATED
ModuleDescriptor.Modifier.OPEN, Classfile.ACC_OPEN,
ModuleDescriptor.Modifier.SYNTHETIC, Classfile.ACC_SYNTHETIC,
ModuleDescriptor.Modifier.MANDATED, Classfile.ACC_MANDATED
);
private static final Map<ModuleDescriptor.Requires.Modifier, Integer>
REQUIRES_MODS_TO_FLAGS = Map.of(
ModuleDescriptor.Requires.Modifier.TRANSITIVE, ACC_TRANSITIVE,
ModuleDescriptor.Requires.Modifier.STATIC, ACC_STATIC_PHASE,
ModuleDescriptor.Requires.Modifier.SYNTHETIC, ACC_SYNTHETIC,
ModuleDescriptor.Requires.Modifier.MANDATED, ACC_MANDATED
ModuleDescriptor.Requires.Modifier.TRANSITIVE, Classfile.ACC_TRANSITIVE,
ModuleDescriptor.Requires.Modifier.STATIC, Classfile.ACC_STATIC_PHASE,
ModuleDescriptor.Requires.Modifier.SYNTHETIC, Classfile.ACC_SYNTHETIC,
ModuleDescriptor.Requires.Modifier.MANDATED, Classfile.ACC_MANDATED
);
private static final Map<ModuleDescriptor.Exports.Modifier, Integer>
EXPORTS_MODS_TO_FLAGS = Map.of(
ModuleDescriptor.Exports.Modifier.SYNTHETIC, ACC_SYNTHETIC,
ModuleDescriptor.Exports.Modifier.MANDATED, ACC_MANDATED
ModuleDescriptor.Exports.Modifier.SYNTHETIC, Classfile.ACC_SYNTHETIC,
ModuleDescriptor.Exports.Modifier.MANDATED, Classfile.ACC_MANDATED
);
private static final Map<ModuleDescriptor.Opens.Modifier, Integer>
OPENS_MODS_TO_FLAGS = Map.of(
ModuleDescriptor.Opens.Modifier.SYNTHETIC, ACC_SYNTHETIC,
ModuleDescriptor.Opens.Modifier.MANDATED, ACC_MANDATED
ModuleDescriptor.Opens.Modifier.SYNTHETIC, Classfile.ACC_SYNTHETIC,
ModuleDescriptor.Opens.Modifier.MANDATED, Classfile.ACC_MANDATED
);
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private ModuleInfoWriter() { }
/**
@ -82,86 +85,86 @@ public final class ModuleInfoWriter {
private static byte[] toModuleInfo(ModuleDescriptor md,
ModuleResolution mres,
ModuleTarget target) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V10, ACC_MODULE, "module-info", null, null, null);
//using low-level module building to avoid validation in ModuleDesc and allow invalid names
return Classfile.build(ClassDesc.of("module-info"), clb -> {
clb.withFlags(AccessFlag.MODULE);
var cp = clb.constantPool();
clb.with(ModuleAttribute.of(cp.moduleEntry(cp.utf8Entry(md.name())), mb -> {
mb.moduleFlags(md.modifiers().stream()
.mapToInt(mm -> MODULE_MODS_TO_FLAGS.getOrDefault(mm, 0))
.reduce(0, (x, y) -> (x | y)));
int moduleFlags = md.modifiers().stream()
.map(MODULE_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
String vs = md.rawVersion().orElse(null);
ModuleVisitor mv = cw.visitModule(md.name(), moduleFlags, vs);
md.rawVersion().ifPresent(vs -> mb.moduleVersion(vs));
// requires
for (ModuleDescriptor.Requires r : md.requires()) {
int flags = r.modifiers().stream()
.map(REQUIRES_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
vs = r.rawCompiledVersion().orElse(null);
mv.visitRequire(r.name(), flags, vs);
}
// requires
for (ModuleDescriptor.Requires r : md.requires()) {
int flags = r.modifiers().stream()
.mapToInt(REQUIRES_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
mb.requires(ModuleRequireInfo.of(
cp.moduleEntry(cp.utf8Entry(r.name())),
flags,
r.rawCompiledVersion().map(cp::utf8Entry).orElse(null)));
}
// exports
for (ModuleDescriptor.Exports e : md.exports()) {
int flags = e.modifiers().stream()
.map(EXPORTS_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
String[] targets = e.targets().toArray(EMPTY_STRING_ARRAY);
mv.visitExport(e.source().replace('.', '/'), flags, targets);
}
// exports
for (ModuleDescriptor.Exports e : md.exports()) {
int flags = e.modifiers().stream()
.mapToInt(EXPORTS_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
var targets = e.targets().stream().map(mn -> cp.moduleEntry(cp.utf8Entry(mn)))
.toArray(ModuleEntry[]::new);
mb.exports(ModuleExportInfo.of(
cp.packageEntry(cp.utf8Entry(e.source())),
flags,
targets));
}
// opens
for (ModuleDescriptor.Opens opens : md.opens()) {
int flags = opens.modifiers().stream()
.map(OPENS_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
String[] targets = opens.targets().toArray(EMPTY_STRING_ARRAY);
mv.visitOpen(opens.source().replace('.', '/'), flags, targets);
}
// opens
for (ModuleDescriptor.Opens opens : md.opens()) {
int flags = opens.modifiers().stream()
.mapToInt(OPENS_MODS_TO_FLAGS::get)
.reduce(0, (x, y) -> (x | y));
var targets = opens.targets().stream().map(mn -> cp.moduleEntry(cp.utf8Entry(mn)))
.toArray(ModuleEntry[]::new);
mb.opens(ModuleOpenInfo.of(
cp.packageEntry(cp.utf8Entry(opens.source())),
flags,
targets));
}
// uses
md.uses().stream().map(sn -> sn.replace('.', '/')).forEach(mv::visitUse);
// uses
md.uses().stream().map(ClassDesc::of).forEach(mb::uses);
// provides
for (ModuleDescriptor.Provides p : md.provides()) {
mv.visitProvide(p.service().replace('.', '/'),
p.providers()
.stream()
.map(pn -> pn.replace('.', '/'))
.toArray(String[]::new));
}
// provides
for (ModuleDescriptor.Provides p : md.provides()) {
mb.provides(ClassDesc.of(p.service()),
p.providers().stream()
.map(ClassDesc::of)
.toArray(ClassDesc[]::new));
}
}));
// add the ModulePackages attribute when there are packages that aren't
// exported or open
Stream<String> exported = md.exports().stream()
.map(ModuleDescriptor.Exports::source);
Stream<String> open = md.opens().stream()
.map(ModuleDescriptor.Opens::source);
long exportedOrOpen = Stream.concat(exported, open).distinct().count();
if (md.packages().size() > exportedOrOpen) {
md.packages().stream()
.map(pn -> pn.replace('.', '/'))
.forEach(mv::visitPackage);
}
// packages
var packages = md.packages().stream().sorted().map(PackageDesc::of).toList();
if (!packages.isEmpty()) {
clb.with(ModulePackagesAttribute.ofNames(packages));
}
// ModuleMainClass attribute
md.mainClass()
.map(mc -> mc.replace('.', '/'))
.ifPresent(mv::visitMainClass);
// ModuleMainClass attribute
md.mainClass().ifPresent(mc ->
clb.with(ModuleMainClassAttribute.of(ClassDesc.of(mc))));
mv.visitEnd();
// write ModuleResolution attribute if specified
if (mres != null) {
clb.with(ModuleResolutionAttribute.of(mres.value()));
}
// write ModuleResolution attribute if specified
if (mres != null) {
cw.visitAttribute(new ModuleResolutionAttribute(mres.value()));
}
// write ModuleTarget attribute if there is a target platform
if (target != null && target.targetPlatform().length() > 0) {
cw.visitAttribute(new ModuleTargetAttribute(target.targetPlatform()));
}
cw.visitEnd();
return cw.toByteArray();
// write ModuleTarget attribute if there is a target platform
if (target != null && !target.targetPlatform().isEmpty()) {
clb.with(ModuleTargetAttribute.of(target.targetPlatform()));
}
});
}
/**

View File

@ -26,6 +26,9 @@
* @bug 8142968 8158456 8298875
* @modules java.base/jdk.internal.access
* java.base/jdk.internal.module
* java.base/jdk.internal.classfile
* java.base/jdk.internal.classfile.attribute
* java.base/jdk.internal.classfile.java.lang.constant
* @run testng ModuleDescriptorTest
* @summary Basic test for java.lang.module.ModuleDescriptor and its builder
*/
@ -57,6 +60,10 @@ import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
import jdk.internal.access.JavaLangModuleAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.classfile.Classfile;
import jdk.internal.classfile.attribute.ModuleAttribute;
import jdk.internal.classfile.java.lang.constant.PackageDesc;
import jdk.internal.classfile.java.lang.constant.ModuleDesc;
import jdk.internal.module.ModuleInfoWriter;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@ -1362,14 +1369,11 @@ public class ModuleDescriptorTest {
* complete set of packages.
*/
public void testReadsWithBadPackageFinder() throws Exception {
ModuleDescriptor descriptor = ModuleDescriptor.newModule("foo")
.requires("java.base")
.exports("p")
.build();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ModuleInfoWriter.write(descriptor, baos);
ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());
ByteBuffer bb = ByteBuffer.wrap(Classfile.buildModule(
ModuleAttribute.of(
ModuleDesc.of("foo"),
mb -> mb.requires(ModuleDesc.of("java.base"), 0, null)
.exports(PackageDesc.of("p"), 0))));
// package finder returns a set that doesn't include p
assertThrows(InvalidModuleDescriptorException.class,

View File

@ -85,8 +85,8 @@ class ModuleBuilderTest {
.uses(ClassDesc.of("another.Service"))
.provides(ClassDesc.of("some.nice.Feature"), ClassDesc.of("impl"), ClassDesc.of("another.impl"))),
List.of(PackageDesc.of("foo.bar.baz"), PackageDesc.of("quux"), PackageDesc.of("foo.bar.baz"), PackageDesc.of("quux")),
clb -> clb.with(ModuleMainClassAttribute.of(ClassDesc.of("main.Class")))
.with(ModulePackagesAttribute.ofNames(PackageDesc.of("foo.bar.baz"), PackageDesc.of("quux")))
.with(ModuleMainClassAttribute.of(ClassDesc.of("overwritten.main.Class"))));
moduleModel = Classfile.parse(modInfo);
attr = ((ModuleAttribute) moduleModel.attributes().stream()
@ -182,7 +182,7 @@ class ModuleBuilderTest {
@Test
void verifyPackages() {
ModulePackagesAttribute a = moduleModel.findAttribute(Attributes.MODULE_PACKAGES).orElseThrow();
assertEquals(a.packages().stream().map(pe -> pe.asSymbol().packageName()).toList(), List.of("0", "1", "2", "3", "4", "o0", "o1", "o2", "foo.bar.baz", "quux"));
assertEquals(a.packages().stream().map(pe -> pe.asSymbol().packageName()).toList(), List.of("foo.bar.baz", "quux"));
}
@Test

View File

@ -78,7 +78,7 @@ public class ModuleExamples {
});
// Build it
byte[] moduleInfo = Classfile.buildModule(ModuleAttribute.of(moduleName, handler), List.of(), clb -> {
byte[] moduleInfo = Classfile.buildModule(ModuleAttribute.of(moduleName, handler), clb -> {
// Add an annotation to the module
clb.with(RuntimeVisibleAnnotationsAttribute.of(Annotation.of(ClassDesc.ofDescriptor("Ljava/lang/Deprecated;"),