diff --git a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp index 28bebee7d9a5f..2fa0245b56f4b 100644 --- a/src/hotspot/share/classfile/javaClasses.cpp +++ b/src/hotspot/share/classfile/javaClasses.cpp @@ -3056,21 +3056,33 @@ Handle java_lang_Throwable::create_initialization_error(JavaThread* current, Han return init_error; } -bool java_lang_Throwable::get_top_method_and_bci(oop throwable, Method** method, int* bci) { +bool java_lang_Throwable::get_method_and_bci(oop throwable, Method** method, int* bci, int depth, bool allow_hidden) { JavaThread* current = JavaThread::current(); objArrayHandle result(current, objArrayOop(backtrace(throwable))); - BacktraceIterator iter(result, current); - // No backtrace available. - if (!iter.repeat()) return false; // If the exception happened in a frame that has been hidden, i.e., // omitted from the back trace, we can not compute the message. - oop hidden = ((objArrayOop)backtrace(throwable))->obj_at(trace_hidden_offset); - if (hidden != nullptr) { - return false; + // Restriction only exists for 1 depth; Objects.requireNonNull uses a hidden frame + if (!allow_hidden) { + oop hidden = ((objArrayOop)backtrace(throwable))->obj_at(trace_hidden_offset); + if (hidden != nullptr) { + return false; + } } - // Get first backtrace element. + BacktraceIterator iter(result, current); + // Get the backtrace element. + do { + // No backtrace available. + if (!iter.repeat()) { + return false; + } + + + if (--depth > 0) { + iter.next(current); + } + } while (depth > 0); BacktraceElement bte = iter.next(current); InstanceKlass* holder = InstanceKlass::cast(java_lang_Class::as_Klass(bte._mirror())); diff --git a/src/hotspot/share/classfile/javaClasses.hpp b/src/hotspot/share/classfile/javaClasses.hpp index faa325d55dd5d..c063c3045a206 100644 --- a/src/hotspot/share/classfile/javaClasses.hpp +++ b/src/hotspot/share/classfile/javaClasses.hpp @@ -694,8 +694,9 @@ class java_lang_Throwable: AllStatic { static void java_printStackTrace(Handle throwable, TRAPS); // Debugging friend class JavaClasses; - // Gets the method and bci of the top frame (TOS). Returns false if this failed. - static bool get_top_method_and_bci(oop throwable, Method** method, int* bci); + // Gets the method and bci of a particular frame (TOS). Returns false if this failed. + // allow_hidden allows the caller of the NPE constructor to be a hidden frame. + static bool get_method_and_bci(oop throwable, Method** method, int* bci, int depth, bool allow_hidden); }; diff --git a/src/hotspot/share/include/jvm.h b/src/hotspot/share/include/jvm.h index f97374553cab8..3c2451fe7e5eb 100644 --- a/src/hotspot/share/include/jvm.h +++ b/src/hotspot/share/include/jvm.h @@ -241,7 +241,7 @@ JVM_InitStackTraceElement(JNIEnv* env, jobject element, jobject stackFrameInfo); */ JNIEXPORT jstring JNICALL -JVM_GetExtendedNPEMessage(JNIEnv *env, jthrowable throwable); +JVM_GetExtendedNPEMessage(JNIEnv *env, jthrowable throwable, jint stack_offset, jint search_slot); /* * java.lang.StackWalker diff --git a/src/hotspot/share/interpreter/bytecodeUtils.cpp b/src/hotspot/share/interpreter/bytecodeUtils.cpp index eb4557d6ba09d..d6b492304327e 100644 --- a/src/hotspot/share/interpreter/bytecodeUtils.cpp +++ b/src/hotspot/share/interpreter/bytecodeUtils.cpp @@ -192,9 +192,17 @@ class ExceptionMessageBuilder : public StackObj { int do_instruction(int bci); bool print_NPE_cause0(outputStream *os, int bci, int slot, int max_detail, - bool inner_expr = false, const char *prefix = nullptr); + bool inner_expr, bool because_clause = false); public: + enum class BacktrackNullSlot : int { + // This NullPointerException is explicitly constructed + NPE_EXPLICIT_CONSTRUCTED = -2, + // There cannot be a NullPointerException at the given BCI + INVALID_BYTECODE_ENCOUNTERED = -1, + // A slot is found + FOUND + }; // Creates an ExceptionMessageBuilder object and runs the analysis // building SimulatedOperandStacks for each bytecode in the given @@ -214,7 +222,7 @@ class ExceptionMessageBuilder : public StackObj { // we return the nr of the slot holding the null reference. If this // NPE is created by hand, we return -2 as the slot. If there // cannot be a NullPointerException at the bci, -1 is returned. - int get_NPE_null_slot(int bci); + BacktrackNullSlot get_NPE_null_slot(int bci); // Prints a java-like expression for the bytecode that pushed // the value to the given slot being live at the given bci. @@ -226,9 +234,10 @@ class ExceptionMessageBuilder : public StackObj { // slot: The slot on the operand stack that contains null. // The slots are numbered from TOS downwards, i.e., // TOS has the slot number 0, that below 1 and so on. + // because_clause: Whether to prefix with " because ..." // // Returns false if nothing was printed, else true. - bool print_NPE_cause(outputStream *os, int bci, int slot); + bool print_NPE_cause(outputStream *os, int bci, int slot, bool because_clause); // Prints a string describing the failed action. void print_NPE_failed_action(outputStream *os, int bci); @@ -311,7 +320,7 @@ static void print_local_var(outputStream *os, unsigned int bci, Method* method, if ((bci >= start) && (bci < end) && (elem->slot == slot)) { ConstantPool* cp = method->constants(); - char *var = cp->symbol_at(elem->name_cp_index)->as_C_string(); + char *var = cp->symbol_at(elem->name_cp_index)->as_C_string(); os->print("%s", var); return; @@ -342,6 +351,19 @@ static void print_local_var(outputStream *os, unsigned int bci, Method* method, } if (found && is_parameter) { + // check MethodParameters for a name, if it carries a name + int actual_param_index = param_index - 1; // 0 based + if (method->has_method_parameters() && actual_param_index < method->method_parameters_length()) { + MethodParametersElement elem = method->method_parameters_start()[actual_param_index]; + if (elem.name_cp_index != 0) { + ConstantPool* cp = method->constants(); + char *var = cp->symbol_at(elem.name_cp_index)->as_C_string(); + os->print("%s", var); + return; + } + } + + // TODO we should use arg%d forms, 0-based, like core reflection os->print("", param_index); } else { // This is the best we can do. @@ -1092,9 +1114,7 @@ int ExceptionMessageBuilder::do_instruction(int bci) { return len; } -#define INVALID_BYTECODE_ENCOUNTERED -1 -#define NPE_EXPLICIT_CONSTRUCTED -2 -int ExceptionMessageBuilder::get_NPE_null_slot(int bci) { +ExceptionMessageBuilder::BacktrackNullSlot ExceptionMessageBuilder::get_NPE_null_slot(int bci) { // Get the bytecode. address code_base = _method->constMethod()->code_base(); Bytecodes::Code code = Bytecodes::java_code_at(_method, code_base + bci); @@ -1110,7 +1130,7 @@ int ExceptionMessageBuilder::get_NPE_null_slot(int bci) { case Bytecodes::_athrow: case Bytecodes::_monitorenter: case Bytecodes::_monitorexit: - return 0; + return static_cast(0); case Bytecodes::_iaload: case Bytecodes::_faload: case Bytecodes::_aaload: @@ -1119,17 +1139,17 @@ int ExceptionMessageBuilder::get_NPE_null_slot(int bci) { case Bytecodes::_saload: case Bytecodes::_laload: case Bytecodes::_daload: - return 1; + return static_cast(1); case Bytecodes::_iastore: case Bytecodes::_fastore: case Bytecodes::_aastore: case Bytecodes::_bastore: case Bytecodes::_castore: case Bytecodes::_sastore: - return 2; + return static_cast(2); case Bytecodes::_lastore: case Bytecodes::_dastore: - return 3; + return static_cast(3); case Bytecodes::_putfield: { int cp_index = Bytes::get_native_u2(code_base + pos); ConstantPool* cp = _method->constants(); @@ -1137,7 +1157,7 @@ int ExceptionMessageBuilder::get_NPE_null_slot(int bci) { int type_index = cp->signature_ref_index_at(name_and_type_index); Symbol* signature = cp->symbol_at(type_index); BasicType bt = Signature::basic_type(signature); - return type2size[bt]; + return static_cast(type2size[bt]); } case Bytecodes::_invokevirtual: case Bytecodes::_invokespecial: @@ -1155,9 +1175,9 @@ int ExceptionMessageBuilder::get_NPE_null_slot(int bci) { int type_index = cp->signature_ref_index_at(name_and_type_index); Symbol* signature = cp->symbol_at(type_index); // The 'this' parameter was null. Return the slot of it. - return ArgumentSizeComputer(signature).size(); + return static_cast(ArgumentSizeComputer(signature).size()); } else { - return NPE_EXPLICIT_CONSTRUCTED; + return BacktrackNullSlot::NPE_EXPLICIT_CONSTRUCTED; } } @@ -1165,11 +1185,11 @@ int ExceptionMessageBuilder::get_NPE_null_slot(int bci) { break; } - return INVALID_BYTECODE_ENCOUNTERED; + return BacktrackNullSlot::INVALID_BYTECODE_ENCOUNTERED; } -bool ExceptionMessageBuilder::print_NPE_cause(outputStream* os, int bci, int slot) { - if (print_NPE_cause0(os, bci, slot, _max_cause_detail, false, " because \"")) { +bool ExceptionMessageBuilder::print_NPE_cause(outputStream* os, int bci, int slot, bool because_clause) { + if (print_NPE_cause0(os, bci, slot, _max_cause_detail, false, because_clause)) { os->print("\" is null"); return true; } @@ -1182,8 +1202,8 @@ bool ExceptionMessageBuilder::print_NPE_cause(outputStream* os, int bci, int slo // at bytecode 'bci'. Compute a message for that bytecode. If // necessary (array, field), recur further. // At most do max_detail recursions. -// Prefix is used to print a proper beginning of the whole -// sentence. +// because_clause is used to print a "because" prefix when this is +// not inner_expr () // inner_expr is used to omit some text, like 'static' in // inner expressions like array subscripts. // @@ -1191,7 +1211,7 @@ bool ExceptionMessageBuilder::print_NPE_cause(outputStream* os, int bci, int slo // bool ExceptionMessageBuilder::print_NPE_cause0(outputStream* os, int bci, int slot, int max_detail, - bool inner_expr, const char *prefix) { + bool inner_expr, bool because_clause) { assert(bci >= 0, "BCI too low"); assert(bci < get_size(), "BCI too large"); @@ -1227,12 +1247,16 @@ bool ExceptionMessageBuilder::print_NPE_cause0(outputStream* os, int bci, int sl } if (max_detail == _max_cause_detail && - prefix != nullptr && + !inner_expr && code != Bytecodes::_invokevirtual && code != Bytecodes::_invokespecial && code != Bytecodes::_invokestatic && code != Bytecodes::_invokeinterface) { - os->print("%s", prefix); + if (because_clause) { + os->print(" because \""); + } else { + os->print("\""); + } } switch (code) { @@ -1347,7 +1371,12 @@ bool ExceptionMessageBuilder::print_NPE_cause0(outputStream* os, int bci, int sl case Bytecodes::_invokeinterface: { int cp_index = Bytes::get_native_u2(code_base + pos); if (max_detail == _max_cause_detail && !inner_expr) { - os->print(" because the return value of \""); + if (because_clause) { + os->print(" because t"); + } else { + os->print("T"); + } + os->print("he return value of \""); } print_method_name(os, _method, cp_index, code); return true; @@ -1440,7 +1469,7 @@ void ExceptionMessageBuilder::print_NPE_failed_action(outputStream *os, int bci) } // Main API -bool BytecodeUtils::get_NPE_message_at(outputStream* ss, Method* method, int bci) { +bool BytecodeUtils::get_NPE_message_at(outputStream* ss, Method* method, int bci, NullSlot slot) { NoSafepointVerifier _nsv; // Cannot use this object over a safepoint. @@ -1454,15 +1483,22 @@ bool BytecodeUtils::get_NPE_message_at(outputStream* ss, Method* method, int bci ResourceMark rm; ExceptionMessageBuilder emb(method, bci); + // Is an explicit slot given? + if (slot != NullSlot::SEARCH) { + // Search from the given slot in bci in Method. + // Omit the failed action. + return emb.print_NPE_cause(ss, bci, static_cast(slot), false); + } + // The slot of the operand stack that contains the null reference. // Also checks for NPE explicitly constructed and returns NPE_EXPLICIT_CONSTRUCTED. - int slot = emb.get_NPE_null_slot(bci); + ExceptionMessageBuilder::BacktrackNullSlot backtrackSlot = emb.get_NPE_null_slot(bci); // Build the message. - if (slot == NPE_EXPLICIT_CONSTRUCTED) { + if (backtrackSlot == ExceptionMessageBuilder::BacktrackNullSlot::NPE_EXPLICIT_CONSTRUCTED) { // We don't want to print a message. return false; - } else if (slot == INVALID_BYTECODE_ENCOUNTERED) { + } else if (backtrackSlot == ExceptionMessageBuilder::BacktrackNullSlot::INVALID_BYTECODE_ENCOUNTERED) { // We encountered a bytecode that does not dereference a reference. DEBUG_ONLY(ss->print("There cannot be a NullPointerException at bci %d of method %s", bci, method->external_name())); @@ -1472,7 +1508,7 @@ bool BytecodeUtils::get_NPE_message_at(outputStream* ss, Method* method, int bci // performed because of the null reference. emb.print_NPE_failed_action(ss, bci); // Print a description of what is null. - if (!emb.print_NPE_cause(ss, bci, slot)) { + if (!emb.print_NPE_cause(ss, bci, static_cast(backtrackSlot), true)) { // Nothing was printed. End the sentence without the 'because' // subordinate sentence. } diff --git a/src/hotspot/share/interpreter/bytecodeUtils.hpp b/src/hotspot/share/interpreter/bytecodeUtils.hpp index ff23b9c000f40..318d749331811 100644 --- a/src/hotspot/share/interpreter/bytecodeUtils.hpp +++ b/src/hotspot/share/interpreter/bytecodeUtils.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019, 2022 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -34,8 +34,18 @@ class outputStream; class BytecodeUtils : public AllStatic { public: + // The slot where the null value comes from. + enum class NullSlot : int { + // Unknown; perform a search at the given bci to check instead. + SEARCH = -1, + // A given slot value to search from. + EXPLICIT + }; // NPE extended message. Return true if string is printed. - static bool get_NPE_message_at(outputStream* ss, Method* method, int bci); + // Slot can be nonnegative to indicate an explicit search for the source of null. + // If slot is SEARCH (default), also search for the action that caused the NPE before + // deriving the actual slot and source of null by code parsing. + static bool get_NPE_message_at(outputStream* ss, Method* method, int bci, NullSlot slot = NullSlot::SEARCH); }; #endif // SHARE_INTERPRETER_BYTECODEUTILS_HPP diff --git a/src/hotspot/share/prims/jvm.cpp b/src/hotspot/share/prims/jvm.cpp index 005776b00c296..cdb18765ec395 100644 --- a/src/hotspot/share/prims/jvm.cpp +++ b/src/hotspot/share/prims/jvm.cpp @@ -503,14 +503,19 @@ JVM_END // java.lang.NullPointerException /////////////////////////////////////////// -JVM_ENTRY(jstring, JVM_GetExtendedNPEMessage(JNIEnv *env, jthrowable throwable)) +JVM_ENTRY(jstring, JVM_GetExtendedNPEMessage(JNIEnv *env, jthrowable throwable, jint stack_offset, jint search_slot)) if (!ShowCodeDetailsInExceptionMessages) return nullptr; oop exc = JNIHandles::resolve_non_null(throwable); + // If we are performing an explicit search instructed by alternative internal NPE constructor + BytecodeUtils::NullSlot slot = search_slot >= 0 ? static_cast(search_slot) + : BytecodeUtils::NullSlot::SEARCH; Method* method; int bci; - if (!java_lang_Throwable::get_top_method_and_bci(exc, &method, &bci)) { + int depth = slot != BytecodeUtils::NullSlot::SEARCH ? stack_offset : 0; // 1-based depth + // The explicit search alternative internal NPE constructor is called from a @Hidden method, allow them + if (!java_lang_Throwable::get_method_and_bci(exc, &method, &bci, depth + 1, slot != BytecodeUtils::NullSlot::SEARCH)) { return nullptr; } if (method->is_native()) { @@ -518,7 +523,7 @@ JVM_ENTRY(jstring, JVM_GetExtendedNPEMessage(JNIEnv *env, jthrowable throwable)) } stringStream ss; - bool ok = BytecodeUtils::get_NPE_message_at(&ss, method, bci); + bool ok = BytecodeUtils::get_NPE_message_at(&ss, method, bci, slot); if (ok) { oop result = java_lang_String::create_oop_from_str(ss.base(), CHECK_NULL); return (jstring) JNIHandles::make_local(THREAD, result); diff --git a/src/java.base/share/classes/java/lang/NullPointerException.java b/src/java.base/share/classes/java/lang/NullPointerException.java index 7258b711b62d9..ecefb47efe21f 100644 --- a/src/java.base/share/classes/java/lang/NullPointerException.java +++ b/src/java.base/share/classes/java/lang/NullPointerException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1994, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1994, 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 @@ -57,6 +57,7 @@ public class NullPointerException extends RuntimeException { */ public NullPointerException() { super(); + extendedMessageState |= CONSTRUCTOR_FINISHED; } /** @@ -67,11 +68,45 @@ public NullPointerException() { */ public NullPointerException(String s) { super(s); + extendedMessageState |= CONSTRUCTOR_FINISHED; } - // 0: no backtrace filled in, no message computed. - // 1: backtrace filled in, no message computed. - // 2: message computed + // Creates an NPE with a custom backtrace configuration. + // The exception has no message if detailed NPE is not enabled. + NullPointerException(int stackOffset, int searchSlot) { + extendedMessageState = setupCustomBackTrace(stackOffset, searchSlot); + this(); + } + + private static int setupCustomBackTrace(int stackOffset, int searchSlot) { + return CUSTOM_TRACE + | encode(stackOffset, STACK_OFFSET_SIZE, STACK_OFFSET_SHIFT) + | encode(searchSlot, SEARCH_SLOT_SIZE, SEARCH_SLOT_SHIFT); + } + + private static final int + CONSTRUCTOR_FINISHED = 0x1, + MESSAGE_COMPUTED = 0x2, + CUSTOM_TRACE = 0x4; + private static final int + STACK_OFFSET_SHIFT = 4, + STACK_OFFSET_SIZE = 4, + SEARCH_SLOT_SHIFT = 8, + SEARCH_SLOT_SIZE = 4; + + private static int encode(int data, int size, int shift) { + int max = (1 << size) - 1; + if ((data & ~max) != 0) + throw new InternalError(); // bad arguments from trusted callers + return ((data & max) << shift); + } + + private static int decode(int encoded, int size, int shift) { + int max = (1 << size) - 1; + return (encoded >> shift) & max; + } + + // Access these fields only while holding this object's monitor lock. private transient int extendedMessageState; private transient String extendedMessage; @@ -81,12 +116,7 @@ public NullPointerException(String s) { public synchronized Throwable fillInStackTrace() { // If the stack trace is changed the extended NPE algorithm // will compute a wrong message. So compute it beforehand. - if (extendedMessageState == 0) { - extendedMessageState = 1; - } else if (extendedMessageState == 1) { - extendedMessage = getExtendedNPEMessage(); - extendedMessageState = 2; - } + ensureMessageComputed(); return super.fillInStackTrace(); } @@ -110,22 +140,34 @@ public String getMessage() { String message = super.getMessage(); if (message == null) { synchronized(this) { - if (extendedMessageState == 1) { - // Only the original stack trace was filled in. Message will - // compute correctly. - extendedMessage = getExtendedNPEMessage(); - extendedMessageState = 2; - } + ensureMessageComputed(); return extendedMessage; } } return message; } - /** - * Get an extended exception message. This returns a string describing - * the location and cause of the exception. It returns null for - * exceptions where this is not applicable. - */ - private native String getExtendedNPEMessage(); + // Must be called only while holding this object's monitor lock. + private void ensureMessageComputed() { + if ((extendedMessageState & (MESSAGE_COMPUTED | CONSTRUCTOR_FINISHED)) == CONSTRUCTOR_FINISHED) { + int stackOffset = decode(extendedMessageState, STACK_OFFSET_SIZE, STACK_OFFSET_SHIFT); + int searchSlot = (extendedMessageState & CUSTOM_TRACE) != 0 + ? decode(extendedMessageState, SEARCH_SLOT_SIZE, SEARCH_SLOT_SHIFT) + : -1; + extendedMessage = getExtendedNPEMessage(stackOffset, searchSlot); + extendedMessageState |= MESSAGE_COMPUTED; + } + } + + // Must be called only while holding this object's monitor lock. + // Gets an extended exception message. There are two modes: + // 1. `searchSlot >= 0`, follow the explicit stack offset and search slot + // configurations to trace how a particular argument, which turns out to + // be `null`, was evaluated. + // 2. `searchSlot == -1`, stack offset is 0 (a call to the nullary constructor) + // and the search slot will be derived by bytecode tracing. The message + // will also include the action that caused the NPE along with the source + // of the `null`. + // If the backtracking cannot find a verifiable result, this method returns `null`. + private native String getExtendedNPEMessage(int stackOffset, int searchSlot); } diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index 1d62d69889603..2bd13bc4cfbde 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -81,6 +81,7 @@ import jdk.internal.vm.ContinuationScope; import jdk.internal.vm.StackableScope; import jdk.internal.vm.ThreadContainer; +import jdk.internal.vm.annotation.Hidden; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; import sun.reflect.annotation.AnnotationType; @@ -2330,6 +2331,11 @@ public void copyToSegmentRaw(String string, MemorySegment segment, long offset) public boolean bytesCompatible(String string, Charset charset) { return string.bytesCompatible(charset); } + + @Hidden + public NullPointerException extendedNullPointerException(int stackOffset, int searchSlot) { + return new NullPointerException(stackOffset, searchSlot); + } }); } } diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index efa36b5b2d8fc..f6941e00d76ce 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -629,4 +629,10 @@ StackWalker newStackWalkerInstance(Set options, * Are the string bytes compatible with the given charset? */ boolean bytesCompatible(String string, Charset charset); + + /// Creates an extended NPE for general null-checking APIs. + /// The implementation is @Hidden to hide this JLA frame from the trace. + /// Stack offset is the number of non-hidden frames to skip, pointing to the null-checking API. + /// Search slot is the slot where the null-checked value is passed in. + NullPointerException extendedNullPointerException(int stackOffset, int searchSlot); } diff --git a/src/java.base/share/native/libjava/NullPointerException.c b/src/java.base/share/native/libjava/NullPointerException.c index de0d4798c63b7..fe5c1b8bec325 100644 --- a/src/java.base/share/native/libjava/NullPointerException.c +++ b/src/java.base/share/native/libjava/NullPointerException.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -30,7 +30,7 @@ #include "java_lang_NullPointerException.h" JNIEXPORT jstring JNICALL -Java_java_lang_NullPointerException_getExtendedNPEMessage(JNIEnv *env, jobject throwable) +Java_java_lang_NullPointerException_getExtendedNPEMessage(JNIEnv *env, jobject throwable, jint stackOffset, jint searchSlot) { - return JVM_GetExtendedNPEMessage(env, throwable); + return JVM_GetExtendedNPEMessage(env, throwable, stackOffset, searchSlot); } diff --git a/test/hotspot/jtreg/runtime/exceptionMsgs/NullPointerException/MethodParametersTest.java b/test/hotspot/jtreg/runtime/exceptionMsgs/NullPointerException/MethodParametersTest.java new file mode 100644 index 0000000000000..38c2e44fcacd2 --- /dev/null +++ b/test/hotspot/jtreg/runtime/exceptionMsgs/NullPointerException/MethodParametersTest.java @@ -0,0 +1,88 @@ +/* + * 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 + * @summary Test the MethodParameters-based NPE messages. + * @bug 8233268 + * @library /test/lib + * @modules java.base/jdk.internal.access + * @clean MethodParametersTest InnerClass + * @compile -parameters -g:none MethodParametersTest.java + * @run junit/othervm -XX:+ShowCodeDetailsInExceptionMessages MethodParametersTest + */ + +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; + +import jdk.internal.access.JavaLangAccess; +import jdk.internal.access.SharedSecrets; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class MethodParametersTest { + private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess(); + + // An arbitrary null-checking API + static void nullCheck(Object arg) { + if (arg == null) { + throw JLA.extendedNullPointerException(1, 0); + } + } + + class InnerClass {} + + @Disabled("Requires javac's API support") + @Test + void testOuterThis() { + var npe = assertThrows(NullPointerException.class, () -> { + try { + InnerClass.class.getDeclaredConstructor(MethodParametersTest.class).newInstance((Object) null); + } catch (InvocationTargetException ex) { + throw ex.getCause(); + } + }); + assertEquals("\"this$0\" is null", npe.getMessage()); + } + + // Random slot to param index mappings, both raw and null-check API NPEs + // 0, 1, 3, 5 + static void myMethod(String firstArg, long l1, double l2, int[] lastArg) { + nullCheck(firstArg); + System.out.println(lastArg.length); + Object a = l1 > 100 ? null : ""; // 6 + a.toString(); + } + + @Test + void testShuffles() { + var npe = assertThrows(NullPointerException.class, () -> myMethod(null, 2, 2, new int[0])); + assertEquals("\"firstArg\" is null", npe.getMessage()); + var msg = assertThrows(NullPointerException.class, () -> myMethod("", 2, 2, null)).getMessage(); + assertTrue(msg.endsWith("because \"lastArg\" is null"), msg); + msg = assertThrows(NullPointerException.class, () -> myMethod("", 2000, 2, new int[0])).getMessage(); + assertTrue(msg.endsWith("because \"\" is null"), msg); // No debug info + } +} \ No newline at end of file diff --git a/test/hotspot/jtreg/runtime/exceptionMsgs/NullPointerException/NullCheckAPITest.java b/test/hotspot/jtreg/runtime/exceptionMsgs/NullPointerException/NullCheckAPITest.java new file mode 100644 index 0000000000000..64d6c33858e0a --- /dev/null +++ b/test/hotspot/jtreg/runtime/exceptionMsgs/NullPointerException/NullCheckAPITest.java @@ -0,0 +1,137 @@ +/* + * 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 + * @summary Test the messages for arbitrary null-check APIs. + * @bug 8233268 + * @library /test/lib + * @modules java.base/jdk.internal.access + * @compile -g NullCheckAPITest.java + * @run junit/othervm -DnullCheckAPI.nestedThrow=true -XX:+ShowCodeDetailsInExceptionMessages NullCheckAPITest + * @run junit/othervm -DnullCheckAPI.nestedThrow=false -XX:+ShowCodeDetailsInExceptionMessages NullCheckAPITest + */ + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; + +import jdk.internal.access.JavaLangAccess; +import jdk.internal.access.SharedSecrets; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class NullCheckAPITest { + + private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess(); + private static final boolean NESTED_THROW = Boolean.getBoolean("nullCheckAPI.nestedThrow"); + + // An arbitrary null-checking API + static void nullCheck(Object arg) { + if (arg == null) { + if (NESTED_THROW) { + // 2 offset: nullCheck, throwNpe; + throwNpe(); + } else { + // 1 offset: nullCheck + throw JLA.extendedNullPointerException(1, 0); + } + } + } + + static void throwNpe() { + throw JLA.extendedNullPointerException(2, 0); + } + + /// A simple NPE message for an expression + static String simpleMessage(String cause) { + return "\"" + cause + "\" is null"; + } + + /// An NPE message for an invocation result + static String invocationMessage(String cause) { + return "The return value of " + simpleMessage(cause); + } + + static void checkSimpleMessage(Executable action, String cause) { + var msg = assertThrows(NullPointerException.class, action).getMessage(); + assertEquals(simpleMessage(cause), msg); + } + + static void checkInvocationMessage(Executable action, String cause) { + var msg = assertThrows(NullPointerException.class, action).getMessage(); + assertEquals(invocationMessage(cause), msg); + } + + class Dummy { Object field; } + + @Test + void test() { + checkSimpleMessage(() -> generateVariableNpe(null), "myA"); + checkSimpleMessage(() -> generateVariableNpe(new Dummy()), "myA.field"); + + checkInvocationMessage(() -> nullCheck(int.class.getSuperclass()), "java.lang.Class.getSuperclass()"); + } + + static class One extends Dummy { + One(NullCheckAPITest rnnt) { + rnnt.super(); + } + } + + @Test + @Disabled("Requires javac's API support") + void testRequireNonNull() { + checkSimpleMessage(() -> { + NullCheckAPITest t = null; + t.new Dummy(); + }, "t"); + checkSimpleMessage(() -> new One(null), "rnnt"); + + var npe = assertThrows(NullPointerException.class, () -> { + try { + Dummy.class.getDeclaredConstructor(NullCheckAPITest.class).newInstance((Object) null); + } catch (InvocationTargetException ex) { + throw ex.getCause(); + } + }); + assertEquals("\"this$0\" is null", npe.getMessage()); + + checkInvocationMessage(() -> { + switch (int.class.getGenericSuperclass()) { + case ParameterizedType pt -> {} + case Class cl -> {} + default -> {} + } + }, "java.lang.Class.getGenericSuperclass()"); + } + + // A method that generate NPE from variables + static void generateVariableNpe(Dummy myA) { + nullCheck(myA); + nullCheck(myA.field); + } +} \ No newline at end of file