diff --git a/src/java.se/share/data/jdwp/jdwp.spec b/src/java.se/share/data/jdwp/jdwp.spec index 11e3160ec97..324bd8d67b8 100644 --- a/src/java.se/share/data/jdwp/jdwp.spec +++ b/src/java.se/share/data/jdwp/jdwp.spec @@ -1801,6 +1801,33 @@ JDWP "Java(tm) Debug Wire Protocol" (Error VM_DEAD) ) ) + (Command IsSameObject=11 + "Determines whether two objects refer to the same Java object." + (Out + (object object1 "The object ID") + (object object2 "The object ID") + ) + (Reply + (boolean isSameObject "true if the objects refer to the same Java object; false otherwise") + ) + (ErrorSet + (Error INVALID_OBJECT) + (Error VM_DEAD) + ) + ) + (Command ObjectHashCode=12 + "Returns hash code for an object." + (Out + (object object "The object ID") + ) + (Reply + (int hashCode "hash code value for the object") + ) + (ErrorSet + (Error INVALID_OBJECT) + (Error VM_DEAD) + ) + ) ) (CommandSet StringReference=10 diff --git a/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java b/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java index 1f9024b7cd2..3f6b70597b2 100644 --- a/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java +++ b/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java @@ -147,8 +147,16 @@ public boolean vmNotSuspended(VMAction action) { public boolean equals(Object obj) { if (obj instanceof ObjectReferenceImpl other) { - return (ref() == other.ref()) && - super.equals(obj); + if (ref() == other.ref() && super.equals(obj)) { + return true; + } + // We can get equal value objects with different IDs. + // TODO: do it only for value objects. + try { + return JDWP.ObjectReference.IsSameObject.process(vm, this, other).isSameObject; + } catch (JDWPException exc) { + throw exc.toJDIException(); + } } else { return false; } diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/ObjectReferenceImpl.c b/src/jdk.jdwp.agent/share/native/libjdwp/ObjectReferenceImpl.c index 1c50c20b868..7f1d26d30f1 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/ObjectReferenceImpl.c +++ b/src/jdk.jdwp.agent/share/native/libjdwp/ObjectReferenceImpl.c @@ -357,6 +357,63 @@ referringObjects(PacketInputStream *in, PacketOutputStream *out) return JNI_TRUE; } +static jboolean +isSameObjectImpl(PacketInputStream *in, PacketOutputStream *out) +{ + jlong id1; + jlong id2; + jobject ref1; + jobject ref2; + JNIEnv *env; + + env = getEnv(); + id1 = inStream_readObjectID(in); + id2 = inStream_readObjectID(in); + if (inStream_error(in)) { + return JNI_TRUE; + } + + if (id1 == NULL_OBJECT_ID || id2 == NULL_OBJECT_ID) { + outStream_setError(out, JDWP_ERROR(INVALID_OBJECT)); + return JNI_TRUE; + } + + ref1 = commonRef_idToRef(env, id1); + ref2 = commonRef_idToRef(env, id2); + (void)outStream_writeBoolean(out, isSameObject(env, ref1, ref2)); + + commonRef_idToRef_delete(env, ref1); + commonRef_idToRef_delete(env, ref2); + + return JNI_TRUE; +} + +static jboolean +objectHashCodeImpl(PacketInputStream *in, PacketOutputStream *out) +{ + jlong id; + jobject ref; + JNIEnv *env; + + env = getEnv(); + id = inStream_readObjectID(in); + if (inStream_error(in)) { + return JNI_TRUE; + } + + if (id == NULL_OBJECT_ID) { + outStream_setError(out, JDWP_ERROR(INVALID_OBJECT)); + return JNI_TRUE; + } + + ref = commonRef_idToRef(env, id); + (void)outStream_writeInt(out, objectHashCode(ref)); + + commonRef_idToRef_delete(env, ref); + + return JNI_TRUE; +} + Command ObjectReference_Commands[] = { {referenceType, "ReferenceType"}, {getValues, "GetValues"}, @@ -367,7 +424,9 @@ Command ObjectReference_Commands[] = { {disableCollection, "DisableCollection"}, {enableCollection, "EnableCollection"}, {isCollected, "IsCollected"}, - {referringObjects, "ReferringObjects"} + {referringObjects, "ReferringObjects"}, + {isSameObjectImpl, "IsSameObject"}, + {objectHashCodeImpl, "ObjectHashCode"} }; DEBUG_DISPATCH_DEFINE_CMDSET(ObjectReference) diff --git a/test/jdk/com/sun/jdi/valhalla/CtorDebuggingTest.java b/test/jdk/com/sun/jdi/valhalla/CtorDebuggingTest.java new file mode 100644 index 00000000000..bbcda1918ac --- /dev/null +++ b/test/jdk/com/sun/jdi/valhalla/CtorDebuggingTest.java @@ -0,0 +1,385 @@ +/* + * 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 id=xcomp-core + * @summary Test value class constructor debugging + * @library .. + * @enablePreview + * + * @comment No other references + * @run main CtorDebuggingTest + * + * @comment All references exist + * @run main CtorDebuggingTest 1 2 3 + * + * @comment No reference at step 2 + * @run main CtorDebuggingTest 1 3 + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import com.sun.jdi.ClassType; +import com.sun.jdi.Field; +import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.IntegerValue; +import com.sun.jdi.Location; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.StackFrame; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.event.BreakpointEvent; +import com.sun.jdi.event.ClassPrepareEvent; +import com.sun.jdi.event.Event; +import com.sun.jdi.event.EventSet; +import com.sun.jdi.event.VMDisconnectEvent; +import com.sun.jdi.request.BreakpointRequest; +import com.sun.jdi.request.EventRequest; +import com.sun.jdi.request.EventRequestManager; + +public class CtorDebuggingTest extends TestScaffold { + + static value class Value { + int x; + int y; + Value(int x, int y) { + this.x = x; // @1 breakpoint + this.y = y; // @2 breakpoint + System.out.println("."); // @3 breakpoint + } + } + + static class TargetApp { + public static Value v1; + public static Value v2; + public static Value v3; + + public static void main(String[] args) throws Exception { + // ensure the class is loaded + Class.forName(Value.class.getName()); + List argList = Arrays.asList(args); + if (argList.contains("1")) { + v1 = new Value(0, 0); + } + if (argList.contains("2")) { + v2 = new Value(3, 0); + } + if (argList.contains("3")) { + v3 = new Value(3, 6); + } + System.out.println(">>main"); // @prepared breakpoint + Value v = new Value(3, 6); + System.out.println("< map. + // Example: + // System.out.println("BP is here"); // @my_breakpoint breakpoint + public static Map parseBreakpoints(String filePath) { + return parseTags("breakpoint", filePath); + } + + public static Map parseTags(String tag, String filePath) { + final String regexp = "\\@(.*?) " + tag; + Pattern pattern = Pattern.compile(regexp); + int lineNum = 1; + Map result = new HashMap<>(); + try { + for (String line: Files.readAllLines(Paths.get(filePath))) { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + result.put(matcher.group(1), lineNum); + } + lineNum++; + } + } catch (IOException ex) { + throw new RuntimeException("failed to parse " + filePath, ex); + } + return result; + } + + public static String getTestSourcePath(String fileName) { + return Paths.get(System.getProperty("test.src")).resolve(fileName).toString(); + } + + public static String getThisTestFile() { + return System.getProperty("test.file"); + } + + // TestScaffold is not very good in handling multiple breakpoints. + // this halper class is a listener which resumes debuggee after breakpoints. + class MultiBreakpointHandler extends TargetAdapter { + boolean needToResume = false; + // the map stores "this" in all breakpoints + Map thisObjects = new HashMap<>(); + + @Override + public void eventSetComplete(EventSet set) { + if (needToResume) { + set.resume(); + needToResume = false; + } + } + + BreakpointRequest addBreakpoint(Location loc, ObjectReference filterObject) { + final BreakpointRequest request = eventRequestManager().createBreakpointRequest(loc); + if (filterObject != null) { + request.addInstanceFilter(filterObject); + } + request.enable(); + + TargetAdapter adapter = new TargetAdapter() { + @Override + public void breakpointReached(BreakpointEvent event) { + if (request.equals(event.request())) { + ObjectReference thisObject = getThisObject(event); + System.out.println("BreakpointEvent: " + event + + " (instanceFilter: " + valueString(filterObject) + ")" + + ", this = " + valueString(thisObject)); + thisObjects.put((BreakpointRequest)event.request(), thisObject); + needToResume = true; + removeThisListener(); + } + } + }; + + addListener(adapter); + + System.out.println("Breakpoint added: " + loc + + " (instanceFilter: " + valueString(filterObject) + ")"); + + return request; + } + + // Resumes the debuggee and go through all breackpoints until the location specified is reached. + void resumeTo(Location loc) { + final BreakpointRequest request = eventRequestManager().createBreakpointRequest(loc); + request.enable(); + + class EventNotification { + boolean completed = false; + boolean disconnected = false; + } + final EventNotification en = new EventNotification(); + + TargetAdapter adapter = new TargetAdapter() { + public void eventReceived(Event event) { + if (request.equals(event.request())) { + synchronized (en) { + en.completed = true; + en.notifyAll(); + } + removeThisListener(); + } else if (event instanceof VMDisconnectEvent) { + synchronized (en) { + en.disconnected = true; + en.notifyAll(); + } + removeThisListener(); + } + } + }; + + addListener(adapter); + // this must be the last listener (as it resumes the debuggee) + addListener(this); + + try { + synchronized (en) { + vm().resume(); + while (!en.completed && !en.disconnected) { + en.wait(); + } + } + } catch (InterruptedException e) { + } + + removeListener(this); + + if (en.disconnected) { + throw new RuntimeException("VM Disconnected before requested event occurred"); + } + } + + // check if the breakpoint was hit. + boolean breakpointHit(BreakpointRequest bkpt) { + return thisObjects.containsKey(bkpt); + } + + ObjectReference thisAtBreakpoint(BreakpointRequest bkpt) { + return thisObjects.get(bkpt); + } + } + + @Override + protected void runTests() throws Exception { + BreakpointEvent bpe = startToMain(TargetApp.class.getName()); + ClassType targetClass = (ClassType)bpe.location().declaringType(); + + Map breakpoints = parseBreakpoints(getThisTestFile()); + System.out.println("breakpoints:"); + for (var entry : breakpoints.entrySet()) { + System.out.println(" tag " + entry.getKey() + ", line " + entry.getValue()); + } + + Location locPrepared = findLocation(targetClass, breakpoints.get("prepared")); + Location locDone = findLocation(targetClass, breakpoints.get("done")); + + resumeTo(locPrepared); + System.out.println("PREPARED"); + + ClassType valueClass = (ClassType)findReferenceType(Value.class.getName()); + System.out.println(Value.class.getName() + ": " + valueClass); + xField = valueClass.fieldByName("x"); + yField = valueClass.fieldByName("y"); + + ObjectReference v1 = getStaticFieldObject(targetClass, "v1"); + ObjectReference v2 = getStaticFieldObject(targetClass, "v2"); + ObjectReference v3 = getStaticFieldObject(targetClass, "v3"); + + MultiBreakpointHandler breakpointHandler = new MultiBreakpointHandler(); + + Location loc1 = findLocation(valueClass, breakpoints.get("1")); + BreakpointRequest bkpt1 = breakpointHandler.addBreakpoint(loc1, null); + BreakpointRequest bkpt1_v1 = v1 == null ? null : breakpointHandler.addBreakpoint(loc1, v1); + BreakpointRequest bkpt1_v2 = v2 == null ? null : breakpointHandler.addBreakpoint(loc1, v2); + BreakpointRequest bkpt1_v3 = v3 == null ? null : breakpointHandler.addBreakpoint(loc1, v3); + + Location loc2 = findLocation(valueClass, breakpoints.get("2")); + BreakpointRequest bkpt2 = breakpointHandler.addBreakpoint(loc2, null); + BreakpointRequest bkpt2_v1 = v1 == null ? null : breakpointHandler.addBreakpoint(loc2, v1); + BreakpointRequest bkpt2_v2 = v2 == null ? null : breakpointHandler.addBreakpoint(loc2, v2); + BreakpointRequest bkpt2_v3 = v3 == null ? null : breakpointHandler.addBreakpoint(loc2, v3); + + Location loc3 = findLocation(valueClass, breakpoints.get("3")); + BreakpointRequest bkpt3 = breakpointHandler.addBreakpoint(loc3, null); + BreakpointRequest bkpt3_v1 = v1 == null ? null : breakpointHandler.addBreakpoint(loc3, v1); + BreakpointRequest bkpt3_v2 = v2 == null ? null : breakpointHandler.addBreakpoint(loc3, v2); + BreakpointRequest bkpt3_v3 = v3 == null ? null : breakpointHandler.addBreakpoint(loc3, v3); + + breakpointHandler.resumeTo(locDone); + + System.out.println("DONE"); + + if (v1 == null && v2 == null && v3 == null) { + // No other references. + // ObjectID is generated at the 1st breakpoint (reference to heap object being constructed), + // and later we get the same oop (although it's content is changing). + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1), breakpointHandler.thisAtBreakpoint(bkpt2)); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2), breakpointHandler.thisAtBreakpoint(bkpt3)); + // There is no breakpoints with instance filter. + } else if (v1 != null && v2 != null && v3 != null) { + // Existing references to value objects with the same content as the object being constructed. + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1), v1); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v1), v1); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v2), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v3), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2), v2); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2_v1), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2_v2), v2); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2_v3), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3), v3); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v1), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v2), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v3), v3); + } else if (v1 != null && v2 == null && v3 != null) { + // At 2nd breakpoint new ObjectID is generated. + ObjectReference thisAt2 = breakpointHandler.thisAtBreakpoint(bkpt2); + assertNotEquals(thisAt2, null); + assertNotEquals(thisAt2, v1); + // Now thisAt2 has the same content as v3. + assertEquals(thisAt2, v3); + // At breakpoint 1 this == v1. + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1), v1); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v1), v1); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v3), null); + // At breakpoint 3 this == v3. + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3), v3); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v1), null); + assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v3), v3); + } else { + throw new RuntimeException("Unknown test case"); + } + + resumeToVMDisconnect(); + } +}