javareflectionjava.lang.class

In Java Failing to invoke method with parameters even though seems to match the getMethod() call


I'm trying to access this JNA class by reflection (because I require them on Windows version but not macOS version, and a change in MacoS notarization rules causes JNA package to fail notarization. So by using reflection I can keep a single code base, and ship the dna classes only on Windows version)

package com.sun.jna.platform.win32;

import com.sun.jna.WString;
import com.sun.jna.platform.FileUtils;
import java.io.File;
import java.io.IOException;

public class W32FileUtils extends FileUtils {
    public W32FileUtils() {
    }

    public boolean hasTrash() {
        return true;
    }

    public void moveToTrash(File[] files) throws IOException {
        Shell32 shell = Shell32.INSTANCE;
        ShellAPI.SHFILEOPSTRUCT fileop = new ShellAPI.SHFILEOPSTRUCT();
        fileop.wFunc = 3;
        String[] paths = new String[files.length];

        int ret;
        for(ret = 0; ret < paths.length; ++ret) {
            paths[ret] = files[ret].getAbsolutePath();
        }

        fileop.pFrom = new WString(fileop.encodePaths(paths));
        fileop.fFlags = 1620;
        ret = shell.SHFileOperation(fileop);
        if (ret != 0) {
            throw new IOException("Move to trash failed: " + fileop.pFrom + ": " + Kernel32Util.formatMessageFromLastErrorCode(ret));
        } else if (fileop.fAnyOperationsAborted) {
            throw new IOException("Move to trash aborted");
        }
    }
}

This is my code

package com.jthink.songkong.analyse.duplicates;

import com.jthink.songkong.text.InfoMessage;
import com.jthink.songkong.ui.MainWindow;

import java.io.File;
import java.lang.reflect.Method;
import java.util.logging.Level;

    public class WindowsDeleteTrash
    {
        public boolean deleteTrash( File file)
        {
            Boolean isHasTrash=false;
            boolean result = false;
            Object instance = null;
            Class<?> windowsFileUtilsClass = null;
            try
            {
                windowsFileUtilsClass = Class.forName("com.sun.jna.platform.win32.W32FileUtils");
                Method instanceClassMethod = windowsFileUtilsClass.getMethod("getInstance");
                instance = instanceClassMethod.invoke(null);
                Method instanceHasTrashMethod = windowsFileUtilsClass.getMethod("hasTrash");
                isHasTrash = (Boolean) instanceHasTrashMethod.invoke(instance, null);
            }
            catch(Exception ex)
            {
                MainWindow.logger.log(Level.SEVERE, ex.getMessage(), ex);
                return file.delete();
            }
    
            if (isHasTrash)
            {
                try
                {
                    File[] files = new File[1];
                    files[0] = file;
    
                    Method      instanceMoveMethod  = windowsFileUtilsClass.getMethod( "moveToTrash", java.io.File[].class);
                    instanceMoveMethod.invoke(instance, files);
                    result = true;
                }
                //SONGKONG-1559
                catch (java.lang.NoClassDefFoundError ncde)
                {
                    MainWindow.logger.log(Level.SEVERE, InfoMessage.MSG_UNABLE_TO_MOVE_TO_TRASH.getMsg() + ncde.getMessage(), ncde);
                    result = file.delete();
                }
                //SONGKONG-1090
                catch(Exception ex)
                {
                    MainWindow.logger.log(Level.SEVERE, ex.getMessage(), ex);
                    result = file.delete();
                }
            }
            else
            {
                result = file.delete();
            }
            return result;
        }
    }

I can get instance and call to hasTrash() method ok, but when invoking the moveTrash() method it fails with argument type mismatch. I cannot understand why because it gets the method okay using parameter File[], and I pass it an array of File, so what am I doing wrong?

This is stack trace:

java.lang.IllegalArgumentException: argument type mismatch
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:107)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at com.jthink.songkong.analyse.duplicates.WindowsDeleteTrash.deleteTrash(WindowsDeleteTrash.java:44)

Should be noted that the instancemethod is declared to return FileUtils – and Windows32FileUtils subclasses this – but I don't see if this is relevant since the call to hasTrash() works fine.


Solution

  • The fix

    // replace:
    // instanceMoveMethod.invoke(instance, files);
    // with:
    instanceMoveMethod.invoke(instance, new Object[] {files});
    

    The explanation

    The weirdness that is happening here is that the actual signature of invoke is:

    invoke(Object receiver, Object[] args)
    // (where 'args' has the varargs flag)
    

    and you are calling it with .invoke(instance, arrayOfFiles) which is a dilemma to the compiler: That you intend to invoke that method with arrayOfFiles as args, which means invoke will unpack your file array,find 1 file object, and pass that as the first argument, which is a failure because the first arg needs to be an instance of File[] and not File, or, should the compiler var-args and wrap your arrayOfFiles object into an 1-size object array?

    To be clear, varargs is compiler sugar: javac knows what ... means and will assume when you call e.g. System.out.printf("Hi %s", name) you meant to write:

    Object[] $args = new Object[1];
    $args[0] = name;
    System.out.printf("Hi %s", $args);
    

    because the JVM has no idea what varargs means. You can check that by using javap - you can see how javac actually makes the code to create an array, fill it, and pass it - anytime you use the varargs system.

    Javac actually picks the first door: It thinks you meant your files array to itself be the list of arguments, so it dutifully unpacks it and passes that one file object to the method which it can't as a file array is needed, so, IllegalArgumentException: arg type mismatch is what you get.

    But File[] isn't Object[]

    Weeeellllll, tricky. Technically of course it isn't; as generics does correctly, higher order type aspects of a type should be invariant and therefore a File[] is neither a super nor a subtype of Object[].

    But java doesn't work that way, arrays are sort of covariant even though that's "wrong" as per typing rules. Here, this compiles, runs, and produces the same error you have:

    import java.io.File;
    import java.lang.reflect.*;
    
    public class Test {
      public static void example(Object[] args) {
        System.out.println("In example");
      }
    
      public static void main(String[] args) throws Exception {
        File[] test = new File[] {new File("whatever") };
        Method m = Test.class.getMethod("example", Object[].class);
        m.invoke(null, test);
      }
    }
    

    produces:

    Exception in thread "main" java.lang.IllegalArgumentException: argument type mismatch
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:107)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at Test.main(Test.java:12)