Hi Goetz,
please find the attached ASM-based patch. It is just a PoC, as such it
does not provide as fine-grained messages as the one discussed in the
RFE/JEP, but can be enhanced to cover custom debugging attribute, I believe.
When running this:
Object o = null;
o.toString();
you get:
Exception in thread "main" java.lang.NullPointerException: attempt to
dereference 'null' when calling method 'toString'
at org.oracle.npe.NPEHandler.main(NPEHandler.java:103)
While when running this:
Foo foo = null;
int y = foo.x;
You get this:
Exception in thread "main" java.lang.NullPointerException: attempt to
dereference 'null' when accessing field 'x'
at org.oracle.npe.NPEHandler.main(NPEHandler.java:105)
One problem I had is that ASM provides no way to get the instruction
given a program counter - which means we have to scan all the bytecodes
and update the sizes as we go along, and, ASM unfortunately doesn’t
expose opcode sizes either. A more robust solution would be to have a
big switch which returned the opcode size of any given opcode. Also,
accessing to StackWalker API on exception creation might not be
desirable in terms of performances, so this might be one of these area
where some VM help could be beneficial. Another problem is that we
cannot distinguish between user-generated exceptions (e.g. `throw new
NullPointerException`) and genuine NPE issued by the VM.
But I guess the upshot is that it works to leave all the gory detail of
bytecode grovelling to a bytecode API - if the logic is applied lazily,
then the impact on performances should be minimal, and the solution more
maintainable longer term.
Cheers
Maurizio
On 15/03/2019 07:59, Lindenmaier, Goetz wrote:
Yes, it would be nice if you shared that.
diff -r 03461dde9ace src/java.base/share/classes/java/lang/NullPointerException.java
--- a/src/java.base/share/classes/java/lang/NullPointerException.java Wed Mar 06 22:09:34 2019 +0100
+++ b/src/java.base/share/classes/java/lang/NullPointerException.java Fri Mar 08 01:51:22 2019 +0000
@@ -25,6 +25,24 @@
package java.lang;
+import jdk.internal.org.objectweb.asm.ClassReader;
+import jdk.internal.org.objectweb.asm.MethodVisitor;
+import jdk.internal.org.objectweb.asm.Opcodes;
+import jdk.internal.org.objectweb.asm.commons.CodeSizeEvaluator;
+import jdk.internal.org.objectweb.asm.tree.AbstractInsnNode;
+import jdk.internal.org.objectweb.asm.tree.ClassNode;
+import jdk.internal.org.objectweb.asm.tree.MethodNode;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.invoke.MethodType;
+import java.net.URL;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
+import java.util.Iterator;
+import java.util.stream.Stream;
+
/**
* Thrown when an application attempts to use {@code null} in a
* case where an object is required. These include:
@@ -53,11 +71,17 @@
class NullPointerException extends RuntimeException {
private static final long serialVersionUID = 5162710183389028792L;
+ transient Class<?> clazz;
+ transient String methodName;
+ transient MethodType methodType;
+ transient int pc;
+
/**
* Constructs a {@code NullPointerException} with no detail message.
*/
public NullPointerException() {
super();
+ init();
}
/**
@@ -68,5 +92,85 @@
*/
public NullPointerException(String s) {
super(s);
+ init();
+ }
+
+ void init() {
+ StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
+ walker.walk(s -> {
+ StackWalker.StackFrame frame = s.filter(fr -> fr.getDeclaringClass() != NullPointerException.class)
+ .findFirst().get();
+ clazz = frame.getDeclaringClass();
+ pc = frame.getByteCodeIndex();
+ methodName = frame.getMethodName();
+ methodType = frame.getMethodType();
+ return null;
+ });
+ }
+
+ @Override
+ public String getMessage() {
+ ProtectionDomain domain = clazz.getProtectionDomain();
+ CodeSource codeSource = domain.getCodeSource();
+ if (domain.getCodeSource() == null) {
+ return super.getMessage();
+ }
+ URL location = codeSource.getLocation();
+ if (location == null) {
+ return super.getMessage();
+ }
+ try {
+ File f = new File(location.getFile(),
+ clazz.getCanonicalName().replaceAll("\\.", "/") + ".class");
+ ClassReader cr = new ClassReader(new FileInputStream(f));
+ ClassNode classNode = new ClassNode();
+ cr.accept(classNode, 0);
+ MethodNode meth = classNode.methods.stream()
+ .filter(mn -> mn.name.equals(methodName) &&
+ mn.desc.equals(methodType.descriptorString()))
+ .findFirst().get();
+ InsnVisitor insnVisitor = new InsnVisitor(pc);
+ Stream.of(meth.instructions.toArray())
+ .forEach(insn -> insn.accept(insnVisitor));
+ return insnVisitor.detailedMessage == null ?
+ super.getMessage() : insnVisitor.detailedMessage;
+ } catch (IOException ex) {
+ return super.getMessage();
+ }
+ }
+
+ static class InsnVisitor extends CodeSizeEvaluator {
+
+ int pc;
+ String detailedMessage = null;
+
+ InsnVisitor(int pc) {
+ super(null);
+ this.pc = pc;
+ }
+
+ @Override
+ public void visitVarInsn(int opcode, int var) {
+ if (getMaxSize() == pc) {
+ detailedMessage = "attempt to dereference 'null' when accessing local variable";
+ }
+ super.visitVarInsn(opcode, var);
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
+ if (getMaxSize() == pc) {
+ detailedMessage = "attempt to dereference 'null' when accessing field '" + name + "'";
+ }
+ super.visitFieldInsn(opcode, owner, name, descriptor);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
+ if (getMaxSize() == pc) {
+ detailedMessage = "attempt to dereference 'null' when calling method '" + name + "'";
+ }
+ super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+ }
}
}