diff --git a/src/java.base/share/classes/jdk/internal/vm/annotation/Strict.java b/src/java.base/share/classes/jdk/internal/vm/annotation/Strict.java index e5f29d85bbc..ca22ba8adab 100644 --- a/src/java.base/share/classes/jdk/internal/vm/annotation/Strict.java +++ b/src/java.base/share/classes/jdk/internal/vm/annotation/Strict.java @@ -32,6 +32,6 @@ * Internal and experimental use only */ @Target(ElementType.FIELD) -@Retention(RetentionPolicy.SOURCE) +@Retention(RetentionPolicy.RUNTIME) public @interface Strict { } diff --git a/test/hotspot/jtreg/runtime/valhalla/inlinetypes/verifier/StrictFinalInstanceFieldsTest.java b/test/hotspot/jtreg/runtime/valhalla/inlinetypes/verifier/StrictFinalInstanceFieldsTest.java index 68c52f3bd0d..914c3d31dd1 100644 --- a/test/hotspot/jtreg/runtime/valhalla/inlinetypes/verifier/StrictFinalInstanceFieldsTest.java +++ b/test/hotspot/jtreg/runtime/valhalla/inlinetypes/verifier/StrictFinalInstanceFieldsTest.java @@ -24,7 +24,9 @@ /* * @test * @enablePreview - * @compile --add-exports=java.base/jdk.internal.vm.annotation=ALL-UNNAMED -XDgenerateAssertUnsetFieldsFrame -XDnoLocalProxyVars StrictFinalInstanceFieldsTest.java + * @library /test/lib + * @modules java.base/jdk.internal.vm.annotation + * @run main/othervm jdk.test.lib.value.StrictCompiler StrictFinalInstanceFieldsTest.java -- -XDnoLocalProxyVars * @run main/othervm -Xlog:verification StrictFinalInstanceFieldsTest */ diff --git a/test/lib-test/jdk/test/lib/StrictCompilerSuperTest.java b/test/lib-test/jdk/test/lib/StrictCompilerSuperTest.java new file mode 100644 index 00000000000..1e3ad861789 --- /dev/null +++ b/test/lib-test/jdk/test/lib/StrictCompilerSuperTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* @test + * @bug 8351362 + * @summary Unit Test for StrictCompiler super rewrite + * @enablePreview + * @library /test/lib + * @modules java.base/jdk.internal.vm.annotation + * @run main/othervm jdk.test.lib.value.StrictCompiler --deferSuperCall StrictCompilerSuperTest.java + * @run junit StrictCompilerSuperTest + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.classfile.Attributes; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.Instruction; +import java.lang.classfile.Opcode; +import java.lang.reflect.AccessFlag; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.stream.Stream; + +import jdk.internal.vm.annotation.Strict; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static java.lang.classfile.ClassFile.*; +import static java.lang.constant.ConstantDescs.INIT_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class StrictCompilerSuperTest { + static Stream> testClasses() { + return Stream.of(Rec.class, Exp.class, Inner.class); + } + + static Stream testClassModels() { + return testClasses().map(cls -> { + try (var in = StrictCompilerSuperTest.class.getResourceAsStream("/" + cls.getName() + ".class")) { + return ClassFile.of().parse(in.readAllBytes()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + } + + @MethodSource("testClasses") + @ParameterizedTest + void testReflectRewrittenRecord(Class cls) throws Throwable { + for (var field : cls.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) + continue; + assertEquals(ACC_PRIVATE | ACC_STRICT | ACC_FINAL, field.getModifiers(), () -> "For field: " + field.getName()); + } + } + + @MethodSource("testClassModels") + @ParameterizedTest + void testRewrittenStrictAccessInClassFile(ClassModel cm) throws Throwable { + for (var f : cm.fields()) { + if (f.flags().has(AccessFlag.STATIC) || f.flags().has(AccessFlag.SYNTHETIC)) + continue; + assertEquals(ACC_PRIVATE | ACC_STRICT | ACC_FINAL, f.flags().flagsMask(), () -> "Field " + f); + } + } + + @MethodSource("testClassModels") + @ParameterizedTest + void testRewrittenCtorBytecode(ClassModel cm) throws Throwable { + var ctor = cm.methods().stream().filter(m -> m.methodName().equalsString(INIT_NAME)).findFirst().orElseThrow(); + var insts = new ArrayList(); + ctor.findAttribute(Attributes.code()).orElseThrow().forEach(ce -> { + if (ce instanceof Instruction inst) { + insts.add(inst); + } + }); + assertSame(Opcode.RETURN, insts.getLast().opcode()); + assertSame(Opcode.INVOKESPECIAL, insts.get(insts.size() - 2).opcode()); + } + + record Rec(@Strict int a, @Strict long b) { + static final String NOISE = "noise"; + } + + static class Exp { + private @Strict final int a; + private @Strict final long b; + + Exp(int a, long b) { + this.a = a; + this.b = b; + } + } + + class Inner { + private @Strict final int a; + private @Strict final long b; + + Inner(int a, long b) { + this.a = a; + this.b = b; + } + + @Override + public String toString() { + return a + " " + StrictCompilerSuperTest.this + " " + b; + } + } +} diff --git a/test/lib-test/jdk/test/lib/StrictCompilerTest.java b/test/lib-test/jdk/test/lib/StrictCompilerTest.java new file mode 100644 index 00000000000..2233360169a --- /dev/null +++ b/test/lib-test/jdk/test/lib/StrictCompilerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* @test + * @bug 8351362 + * @summary Unit Test for StrictCompiler + * @enablePreview + * @library /test/lib + * @modules java.base/jdk.internal.vm.annotation + * @run main/othervm jdk.test.lib.value.StrictCompiler StrictCompilerTest.java + * @run junit StrictCompilerTest + */ + +import jdk.internal.vm.annotation.Strict; +import org.junit.jupiter.api.Test; + +import static java.lang.classfile.ClassFile.ACC_FINAL; +import static java.lang.classfile.ClassFile.ACC_STRICT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StrictCompilerTest { + @Test + void testReflectMyself() throws Throwable { + for (var field : StrictTarget.class.getDeclaredFields()) { + assertEquals(ACC_STRICT | ACC_FINAL, field.getModifiers(), () -> field.getName()); + } + } + + static final class StrictTarget { + @Strict + final int a; + @Strict + final Object b; + + StrictTarget() { + this.a = 1; + this.b = 2392352234L; + super(); + } + } +} diff --git a/test/lib/jdk/test/lib/compiler/InMemoryJavaCompiler.java b/test/lib/jdk/test/lib/compiler/InMemoryJavaCompiler.java index 4722ef3b67a..befc235d529 100644 --- a/test/lib/jdk/test/lib/compiler/InMemoryJavaCompiler.java +++ b/test/lib/jdk/test/lib/compiler/InMemoryJavaCompiler.java @@ -216,7 +216,11 @@ public String getClassName() { * @return The resulting byte code from the compilation */ public static Map compile(Map inputMap) { - Collection sourceFiles = new LinkedList(); + return compile(inputMap, new String[0]); + } + + public static Map compile(Map inputMap, String... options) { + Collection sourceFiles = new ArrayList<>(); for (Entry entry : inputMap.entrySet()) { sourceFiles.add(new SourceFile(entry.getKey(), entry.getValue())); } @@ -225,7 +229,7 @@ public static Map compile(Map in FileManager fileManager = new FileManager(compiler.getStandardFileManager(null, null, null)); Writer writer = new StringWriter(); - Boolean exitCode = compiler.getTask(writer, fileManager, null, null, null, sourceFiles).call(); + Boolean exitCode = compiler.getTask(writer, fileManager, null, Arrays.asList(options), null, sourceFiles).call(); if (!exitCode) { System.out.println("*********** javac output begin ***********"); System.out.println(writer.toString()); diff --git a/test/lib/jdk/test/lib/value/StrictCompiler.java b/test/lib/jdk/test/lib/value/StrictCompiler.java new file mode 100644 index 00000000000..0c9ceb01fec --- /dev/null +++ b/test/lib/jdk/test/lib/value/StrictCompiler.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.test.lib.value; + +import java.io.IOException; +import java.lang.classfile.*; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.classfile.constantpool.Utf8Entry; +import java.lang.classfile.instruction.FieldInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.ReturnInstruction; +import java.lang.constant.ClassDesc; +import java.lang.reflect.AccessFlag; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jdk.test.lib.compiler.InMemoryJavaCompiler; + +import static java.lang.classfile.ClassFile.*; +import static java.lang.constant.ConstantDescs.INIT_NAME; + +/** + * Compile a java file with InMemoryJavaCompiler, and then modify the resulting + * class file to include strict modifier and null restriction attributes. + */ +public final class StrictCompiler { + public static final String TEST_SRC = System.getProperty("test.src", "").trim(); + public static final String TEST_CLASSES = System.getProperty("test.classes", "").trim(); + private static final ClassDesc CD_Strict = ClassDesc.of("jdk.internal.vm.annotation.Strict"); + // NR will stay in jdk.internal for now until we expose as a more formal feature + private static final ClassDesc CD_NullRestricted = ClassDesc.of("jdk.internal.vm.annotation.NullRestricted"); + + /** + * @param args source and destination + * @throws IOException if an I/O error occurs + */ + public static void main(String[] args) throws IOException { + Map ins = new HashMap<>(); + List javacOpts = new ArrayList<>(); + boolean encounteredSeparator = false; + boolean deferSuperCall = false; + for (var a : args) { + if (encounteredSeparator) { + javacOpts.add(a); + continue; + } + if (a.endsWith(".java")) { + String className = a.substring(0, a.length() - 5); + Path src = Path.of(TEST_SRC, a); + ins.put(className, Files.readString(src)); + continue; + } + switch (a) { + case "--" -> encounteredSeparator = true; + case "--deferSuperCall" -> deferSuperCall = true; + default -> throw new IllegalArgumentException("Unknown option " + a); + } + } + if (!javacOpts.contains("--source")) { + javacOpts.add("--source"); + javacOpts.add(String.valueOf(Runtime.version().feature())); + } + if (!javacOpts.contains("--enable-preview")) { + javacOpts.add("--enable-preview"); + } + if (!javacOpts.contains("java.base/jdk.internal.vm.annotation=ALL-UNNAMED")) { + javacOpts.add("--add-exports"); + javacOpts.add("java.base/jdk.internal.vm.annotation=ALL-UNNAMED"); + } + System.out.println(javacOpts); + var classes = InMemoryJavaCompiler.compile(ins, javacOpts.toArray(String[]::new)); + Files.createDirectories(Path.of(TEST_CLASSES)); + for (var entry : classes.entrySet()) { + if (deferSuperCall) { + fixSuperAndDumpClass(entry.getKey(), entry.getValue()); + } else { + dumpClass(entry.getKey(), entry.getValue()); + } + } + } + + private static void fixSuperAndDumpClass(String name, byte[] rawBytes) throws IOException { + var cm = ClassFile.of().parse(rawBytes); + record FieldKey(Utf8Entry name, Utf8Entry type) {} + Set strictInstances = new HashSet<>(); + for (var f : cm.fields()) { + if (f.flags().has(AccessFlag.STATIC)) + continue; + var rvaa = f.findAttribute(Attributes.runtimeVisibleAnnotations()); + if (rvaa.isPresent()) { + for (var anno : rvaa.get().annotations()) { + var descString = anno.className(); + if (descString.equalsString(CD_Strict.descriptorString())) { + strictInstances.add(new FieldKey(f.fieldName(), f.fieldType())); + } + } + } + } + + var thisClass = cm.thisClass(); + var superName = cm.superclass().orElseThrow().name(); + + var rewritten = ClassFile.of().transformClass(cm, (clb, cle) -> { + cond: + if (cle instanceof MethodModel mm + && mm.methodName().equalsString(INIT_NAME)) { + var code = mm.findAttribute(Attributes.code()).orElseThrow(); + var elements = code.elementList(); + int len = elements.size(); + int superCallPos = -1; + int returnPos = -1; + boolean deferSuperCall = false; + for (int i = 0; i < len; i++) { + var e = elements.get(i); + if (superCallPos == -1) { + if (e instanceof InvokeInstruction inv && + inv.opcode() == Opcode.INVOKESPECIAL && + inv.method().name().equalsString(INIT_NAME) && + inv.method().type().equalsString("()V") && + inv.owner().name().equals(superName)) { + // Assume we are calling on uninitializedThis... + superCallPos = i; + } + } else if (!deferSuperCall) { + if (e instanceof FieldInstruction ins && + ins.opcode() == Opcode.PUTFIELD && + ins.owner().equals(thisClass) && + strictInstances.contains(new FieldKey(ins.name(), ins.type()))) { + deferSuperCall = true; + } + } + if (e instanceof ReturnInstruction inst && inst.opcode() == Opcode.RETURN) { + if (returnPos != -1) { + throw new IllegalArgumentException("Control flow too complex"); + } else { + returnPos = i; + } + } + } + if (elements.reversed().stream() + .mapMulti((e, sink) -> { + if (e instanceof Instruction i) { + sink.accept(i); + } + }) + .findFirst() + .orElseThrow() + .opcode() != Opcode.RETURN) { + throw new IllegalArgumentException("Control flow too complex"); + } + if (!deferSuperCall) { + break cond; + } + var suppliedElements = new ArrayList<>(elements); + var foundLoad = suppliedElements.remove(superCallPos - 1); + var foundSuperCall = suppliedElements.remove(superCallPos - 1); + var foundReturnInst = suppliedElements.remove(returnPos - 2); + suppliedElements.add(foundLoad); + suppliedElements.add(foundSuperCall); + suppliedElements.add(foundReturnInst); + clb.withMethod(INIT_NAME, mm.methodTypeSymbol(), mm.flags().flagsMask(), mb -> mb + .transform(mm, MethodTransform.dropping(ce -> ce instanceof CodeModel)) + .withCode(suppliedElements::forEach)); + return; + } + clb.with(cle); + }); + + dumpClass(name, rewritten); + } + + private static void dumpClass(String name, byte[] rawBytes) throws IOException { + var cm = ClassFile.of().parse(rawBytes); + var transformed = ClassFile.of().transformClass(cm, ClassTransform.transformingFields(FieldTransform.ofStateful(() -> new FieldTransform() { + int oldAccessFlags; + boolean nullRestricted; + boolean strict; + + @Override + public void accept(FieldBuilder builder, FieldElement element) { + if (element instanceof AccessFlags af) { + oldAccessFlags = af.flagsMask(); + return; + } + builder.with(element); + if (element instanceof RuntimeVisibleAnnotationsAttribute rvaa) { + for (var anno : rvaa.annotations()) { + var descString = anno.className(); + if (descString.equalsString(CD_Strict.descriptorString())) { + strict = true; + } else if (descString.equalsString(CD_NullRestricted.descriptorString())) { + nullRestricted = true; + } + } + } + } + + @Override + public void atEnd(FieldBuilder builder) { + if (strict) { + oldAccessFlags |= ACC_STRICT; + } + builder.withFlags(oldAccessFlags); + assert !nullRestricted || strict : name; + } + }))); + + // Force preview + transformed[4] = (byte) 0xFF; + transformed[5] = (byte) 0xFF; + Path dst = Path.of(TEST_CLASSES, name + ".class"); + Files.write(dst, transformed); + } +}