From 5ba2a91f8a8c4f92d54497b31846448d6384ef07 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 18 Dec 2025 19:21:00 +0100 Subject: [PATCH 1/2] When loading the help class for watches, use MethodHandles.Lookup.defineHiddenClass where available. --- .../debugger/jpda/expr/TreeEvaluator.java | 6 +- .../debugger/jpda/remote/RemoteServices.java | 167 +++++++++++------- .../api/debugger/jpda/EvaluationTest.java | 8 + .../api/debugger/jpda/testapps/EvalApp.java | 4 + 4 files changed, 117 insertions(+), 68 deletions(-) diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/TreeEvaluator.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/TreeEvaluator.java index b512256d8877..6147b320adc2 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/TreeEvaluator.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/TreeEvaluator.java @@ -380,7 +380,7 @@ private static Value invokeVirtual ( private boolean uploadClass(Pair namedClass, Map classes, EvaluationContext evaluationContext) { - ObjectVariable clazz = uploadClass(namedClass); + ObjectVariable clazz = uploadClass(namedClass, evaluationContext.getFrame().location().declaringType().classObject()); if (clazz != null) { String className = namedClass.first(); int simpleNameIndex = className.replace('$', '.').lastIndexOf('.'); @@ -395,14 +395,14 @@ private boolean uploadClass(Pair namedClass, } } - private ObjectVariable uploadClass(Pair namedClass) { + private ObjectVariable uploadClass(Pair namedClass, ClassObjectReference context) { if (!evaluationContext.canInvokeMethods()) { return null; } evaluationContext.methodToBeInvoked(); try { ClassObjectReference newClass = RemoteServices.uploadClass( - evaluationContext.getThread().getThreadReference(), + evaluationContext.getThread().getThreadReference(), context, new RemoteClass(namedClass.first(), namedClass.second())); if (newClass != null) { evaluationContext.registerDisabledCollectionOf(newClass); diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java index 31af1e408266..1ecf41a4d8a5 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java @@ -24,6 +24,7 @@ import com.sun.jdi.ClassNotLoadedException; import com.sun.jdi.ClassObjectReference; import com.sun.jdi.ClassType; +import com.sun.jdi.Field; import com.sun.jdi.IncompatibleThreadStateException; import com.sun.jdi.InvalidTypeException; import com.sun.jdi.InvocationException; @@ -82,7 +83,7 @@ private RemoteServices() {} * @throws UnsupportedOperationExceptionWrapper * @throws ClassNotPreparedExceptionWrapper */ - public static ClassObjectReference uploadClass(ThreadReference tr, RemoteClass rc) throws InvalidTypeException, + public static ClassObjectReference uploadClass(ThreadReference tr, ClassObjectReference context, RemoteClass rc) throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, InvocationException, @@ -93,49 +94,43 @@ public static ClassObjectReference uploadClass(ThreadReference tr, RemoteClass r ObjectCollectedExceptionWrapper, UnsupportedOperationExceptionWrapper, ClassNotPreparedExceptionWrapper { + List reenableCollection = new ArrayList<>(); VirtualMachine vm = MirrorWrapper.virtualMachine(tr); ObjectReference classLoader = getContextClassLoader(tr, vm); - ClassType classLoaderClass = (ClassType) ObjectReferenceWrapper.referenceType(classLoader); - String className = rc.name; - ClassObjectReference theUploadedClass = null; - ArrayReference byteArray = createTargetBytes(vm, rc.bytes, new ByteValue[256]); - StringReference nameMirror = null; + ClassObjectReference classOption = loadClass(tr, "java.lang.invoke.MethodHandles$Lookup$ClassOption", reenableCollection); + try { - Method defineClass = ClassTypeWrapper.concreteMethodByName(classLoaderClass, - "defineClass", - "(Ljava/lang/String;[BII)Ljava/lang/Class;"); - boolean uploaded = false; - while (!uploaded) { - nameMirror = VirtualMachineWrapper.mirrorOf(vm, className); - try { - ObjectReferenceWrapper.disableCollection(nameMirror); - uploaded = true; - } catch (ObjectCollectedExceptionWrapper ocex) { - // Just collected, try again... - } catch (UnsupportedOperationExceptionWrapper uex) { - // Hope it will not be GC'ed... - uploaded = true; - } - } - uploaded = false; - while (!uploaded) { - theUploadedClass = (ClassObjectReference) ObjectReferenceWrapper.invokeMethod( + if (classOption != null) { + //target has support for defineHiddenClass: + ReferenceType lookup = vm.classesByName("java.lang.invoke.MethodHandles$Lookup").get(0); + ObjectReference nestmate = (ObjectReference) classOption.reflectedType().getValue(classOption.reflectedType().fieldByName("NESTMATE")); + Field allMightyLookup = lookup.fieldByName("IMPL_LOOKUP"); + Method inMethod = lookup.methodsByName("in", "(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandles$Lookup;").get(0); + ObjectReference allPowerContextLookup = ((ObjectReference) ((ObjectReference) lookup.getValue(allMightyLookup)).invokeMethod(tr, inMethod, List.of(context), ObjectReference.INVOKE_SINGLE_THREADED)); + Method defineHiddenClass = lookup.methodsByName("defineHiddenClass", "([BZ[Ljava/lang/invoke/MethodHandles$Lookup$ClassOption;)Ljava/lang/invoke/MethodHandles$Lookup;").get(0); + ArrayReference byteArray = createTargetBytes(vm, rc.bytes, new ByteValue[256], reenableCollection); + ArrayType classOptionsArrayClass = getArrayClass(vm, "java.lang.invoke.MethodHandles$Lookup$ClassOption[]"); + ArrayReference classOptionsArray = ArrayTypeWrapper.newInstance(classOptionsArrayClass, 1); + classOptionsArray.setValue(0, nestmate); + + ObjectReference newLookup = (ObjectReference) allPowerContextLookup.invokeMethod(tr, defineHiddenClass, List.of(byteArray, vm.mirrorOf(true), classOptionsArray), ObjectReference.INVOKE_SINGLE_THREADED); + Method lookupGetClass = lookup.methodsByName("lookupClass", "()Ljava/lang/Class;").get(0); + + return (ClassObjectReference) newLookup.invokeMethod(tr, lookupGetClass, List.of(), ObjectReference.INVOKE_SINGLE_THREADED); + } else { + ClassType classLoaderClass = (ClassType) ObjectReferenceWrapper.referenceType(classLoader); + + String className = rc.name; + ArrayReference byteArray = createTargetBytes(vm, rc.bytes, new ByteValue[256], reenableCollection); + StringReference nameMirror = objectWithDisabledCollection(() -> VirtualMachineWrapper.mirrorOf(vm, className), reenableCollection); + Method defineClass = ClassTypeWrapper.concreteMethodByName(classLoaderClass, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;"); + ClassObjectReference theUploadedClass = objectWithDisabledCollection(() -> (ClassObjectReference) ObjectReferenceWrapper.invokeMethod( classLoader, tr, defineClass, Arrays.asList(nameMirror, byteArray, vm.mirrorOf(0), vm.mirrorOf(rc.bytes.length)), - ObjectReference.INVOKE_SINGLE_THREADED); - try { - // Disable collection only of the basic class - ObjectReferenceWrapper.disableCollection(theUploadedClass); - uploaded = true; - } catch (ObjectCollectedExceptionWrapper ocex) { - // Just collected, try again... - } catch (UnsupportedOperationExceptionWrapper uex) { - // Hope it will not be GC'ed... - uploaded = true; - } - } - if (uploaded) { + ObjectReference.INVOKE_SINGLE_THREADED), reenableCollection); // Initialize the class: ClassType bc = ((ClassType) theUploadedClass.reflectedType()); if (!bc.isInitialized()) { @@ -145,16 +140,15 @@ public static ClassObjectReference uploadClass(ThreadReference tr, RemoteClass r Method aMethod = ClassTypeWrapper.concreteMethodByName(theClass, "getConstructors", "()[Ljava/lang/reflect/Constructor;"); ObjectReferenceWrapper.invokeMethod(theUploadedClass, tr, aMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); } + return theUploadedClass; } } finally { - try { - ObjectReferenceWrapper.enableCollection(byteArray); // We can dispose it now - if (nameMirror != null) { - ObjectReferenceWrapper.enableCollection(nameMirror); - } - } catch (UnsupportedOperationExceptionWrapper uex) {} + for (ObjectReference toReenable : reenableCollection) { + try { + ObjectReferenceWrapper.enableCollection(toReenable); // We can dispose it now + } catch (UnsupportedOperationExceptionWrapper uex) {} + } } - return theUploadedClass; } private static ObjectReference getContextClassLoader(ThreadReference tr, VirtualMachine vm) throws InternalExceptionWrapper, @@ -209,26 +203,17 @@ private static ArrayType getArrayClass(VirtualMachine vm, String name) throws In } private static ArrayReference createTargetBytes(VirtualMachine vm, byte[] bytes, - ByteValue[] mirrorBytesCache) throws InvalidTypeException, - ClassNotLoadedException, - InternalExceptionWrapper, - VMDisconnectedExceptionWrapper, - ObjectCollectedExceptionWrapper { + ByteValue[] mirrorBytesCache, + List reenableCollection) throws InvalidTypeException, + ClassNotLoadedException, + InternalExceptionWrapper, + VMDisconnectedExceptionWrapper, + ObjectCollectedExceptionWrapper, + IncompatibleThreadStateException, + InvocationException, + UnsupportedOperationExceptionWrapper { ArrayType bytesArrayClass = getArrayClass(vm, "byte[]"); - ArrayReference array = null; - boolean disabledCollection = false; - while (!disabledCollection) { - array = ArrayTypeWrapper.newInstance(bytesArrayClass, bytes.length); - try { - ObjectReferenceWrapper.disableCollection(array); - disabledCollection = true; - } catch (ObjectCollectedExceptionWrapper ocex) { - // Collected too soon, try again... - } catch (UnsupportedOperationExceptionWrapper uex) { - // Hope it will not be GC'ed... - disabledCollection = true; - } - } + ArrayReference array = objectWithDisabledCollection(() -> ArrayTypeWrapper.newInstance(bytesArrayClass, bytes.length), reenableCollection); List values = new ArrayList(bytes.length); for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; @@ -242,5 +227,57 @@ private static ArrayReference createTargetBytes(VirtualMachine vm, byte[] bytes, ArrayReferenceWrapper.setValues(array, values); return array; } - + + private static T objectWithDisabledCollection(CreateReference provider, + List reenableCollection) throws InternalExceptionWrapper, + VMDisconnectedExceptionWrapper, + ClassNotLoadedException, + IncompatibleThreadStateException, + InvalidTypeException, + InvocationException, + UnsupportedOperationExceptionWrapper, + ObjectCollectedExceptionWrapper { + while (true) { + T value = provider.create(); + try { + ObjectReferenceWrapper.disableCollection(value); + reenableCollection.add(value); + return value; + } catch (ObjectCollectedExceptionWrapper ocex) { + // Collected too soon, try again... + } catch (UnsupportedOperationExceptionWrapper uex) { + // Hope it will not be GC'ed... + return value; + } + } + } + + private static ClassObjectReference loadClass(ThreadReference tr, + String className, + List reenableCollection) throws InternalExceptionWrapper, + VMDisconnectedExceptionWrapper, + ClassNotPreparedExceptionWrapper, + InvalidTypeException, + ClassNotLoadedException, + IncompatibleThreadStateException, + InvocationException, + UnsupportedOperationExceptionWrapper, + ObjectCollectedExceptionWrapper { + VirtualMachine vm = MirrorWrapper.virtualMachine(tr); + ReferenceType jlClass = vm.classesByName("java.lang.Class").get(0); + Method loadClass = jlClass.methodsByName("forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;").get(0); + ObjectReference classLoader = getContextClassLoader(tr, vm); + ObjectReference classNameMirror = objectWithDisabledCollection(() -> vm.mirrorOf(className), reenableCollection); + + try { + return objectWithDisabledCollection(() -> (ClassObjectReference) jlClass.classObject().invokeMethod(tr, loadClass, List.of(classNameMirror, vm.mirrorOf(true), classLoader), ObjectReference.INVOKE_SINGLE_THREADED), + reenableCollection); + } catch (ClassNotLoadedException | IncompatibleThreadStateException | InvalidTypeException | InvocationException ex) { + return null; + } + } + + interface CreateReference { + public T create() throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException, InvocationException, UnsupportedOperationExceptionWrapper, ObjectCollectedExceptionWrapper; + } } diff --git a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/EvaluationTest.java b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/EvaluationTest.java index 45812827cbc0..fbf94ef4fe9a 100644 --- a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/EvaluationTest.java +++ b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/EvaluationTest.java @@ -78,6 +78,14 @@ public void testStaticExpressions () throws Exception { } } + public void testMemberRefToPrivate () throws Exception { + try { + checkEval ("java.util.List.of(\"A\", \"B\").stream().map(EvalApp::toLen).count()", "long", "2"); + } finally { + support.doFinish (); + } + } + private void checkEval (String expression, int value) { try { Variable var = support.getDebugger ().evaluate (expression); diff --git a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/EvalApp.java b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/EvalApp.java index 92a433373959..4843f1b26043 100644 --- a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/EvalApp.java +++ b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/EvalApp.java @@ -56,6 +56,10 @@ private float m3() { return 4.3f; } + private static int toLen(String str) { + return str.length(); + } + private static int ix = 74; private static float fx = 10.0f; private static double dx = 10.0; From 39a0044e961b9c00c1fd4962d75b35b54caaa2ed Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 8 Jan 2026 12:04:16 +0100 Subject: [PATCH 2/2] Cleanup. --- .../debugger/jpda/remote/RemoteServices.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java index 1ecf41a4d8a5..c1f629adbbc8 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/remote/RemoteServices.java @@ -102,12 +102,21 @@ public static ClassObjectReference uploadClass(ThreadReference tr, ClassObjectRe try { if (classOption != null) { - //target has support for defineHiddenClass: - ReferenceType lookup = vm.classesByName("java.lang.invoke.MethodHandles$Lookup").get(0); + //target has support for MethodHandles.Lookup.defineHiddenClass: + ClassType lookup = getClass(vm, "java.lang.invoke.MethodHandles$Lookup"); ObjectReference nestmate = (ObjectReference) classOption.reflectedType().getValue(classOption.reflectedType().fieldByName("NESTMATE")); - Field allMightyLookup = lookup.fieldByName("IMPL_LOOKUP"); - Method inMethod = lookup.methodsByName("in", "(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandles$Lookup;").get(0); - ObjectReference allPowerContextLookup = ((ObjectReference) ((ObjectReference) lookup.getValue(allMightyLookup)).invokeMethod(tr, inMethod, List.of(context), ObjectReference.INVOKE_SINGLE_THREADED)); + //need to get all powers lookup for the context class: + ObjectReference allPowerContextLookup; + if (false) { + //use Lookup.IMPL_LOOKUP: + Field allMightyLookup = lookup.fieldByName("IMPL_LOOKUP"); + Method inMethod = lookup.methodsByName("in", "(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandles$Lookup;").get(0); + allPowerContextLookup = ((ObjectReference) ((ObjectReference) lookup.getValue(allMightyLookup)).invokeMethod(tr, inMethod, List.of(context), ObjectReference.INVOKE_SINGLE_THREADED)); + } else { + //use Lookup(Class): + Method lookupConstructor = lookup.methodsByName("", "(Ljava/lang/Class;)V").get(0); + allPowerContextLookup = (ObjectReference) lookup.newInstance(tr, lookupConstructor, List.of(context), ObjectReference.INVOKE_SINGLE_THREADED); + } Method defineHiddenClass = lookup.methodsByName("defineHiddenClass", "([BZ[Ljava/lang/invoke/MethodHandles$Lookup$ClassOption;)Ljava/lang/invoke/MethodHandles$Lookup;").get(0); ArrayReference byteArray = createTargetBytes(vm, rc.bytes, new ByteValue[256], reenableCollection); ArrayType classOptionsArrayClass = getArrayClass(vm, "java.lang.invoke.MethodHandles$Lookup$ClassOption[]");