0

I am trying to delete a DLL which has been loaded into JNA and later disposed. I have tried all the solutions described in the answer to this question, but they are not working: How to dispose library loaded with JNA

Here is code I've tried without a time delay:

import java.io.File;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLibrary;

class Filter {

    private static ExtDLLTool DLLUtil;
    final private static String dllPath = "./ExternalDownloader_64.dll"; 

    static {
        DLLUtil = (ExtDLLTool) Native.loadLibrary(dllPath, ExtDLLTool.class);
    }

    public static void main(String[] args) {

        if (DLLUtil != null) {
            DLLUtil = null;
            NativeLibrary lib = NativeLibrary.getInstance(dllPath);
            lib.dispose();
        }

        File dllFile = new File(dllPath);
        if(dllFile.exists()){
            boolean isDeleted = dllFile.delete();
            if(!isDeleted){
                System.out.println("Unable to delete dll file, since it hold by jvm");
            }
        }

    }

    private interface ExtDLLTool extends Library {
        String validateNomination(String dloadProps);
    }
}

I added a time delay to give the native code time to release the handle:

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLibrary;

class Filter {

    private static ExtDLLTool DLLUtil;
    final private static String dllPath = "./ExternalDownloader_64.dll";

    static {
        DLLUtil = (ExtDLLTool) Native.loadLibrary(dllPath, ExtDLLTool.class);
    }

    public static void main(String[] args) throws Exception{

        if (DLLUtil != null) {
            DLLUtil = null;
            NativeLibrary lib = NativeLibrary.getInstance(dllPath);
            lib.dispose();
            Thread.sleep(3000);
        }

        File dllFile = new File(dllPath);
        if(dllFile.exists()){
            Files.delete(Paths.get(dllPath));
            // boolean isDeleted = dllFile.delete();
            if(dllFile.exists()){
                System.out.println("Unable to delete dll file, since it hold by jvm");
            }
        }
    }

    private interface ExtDLLTool extends Library {
        String validateNomination(String dloadProps);
    }
}

This code results in an exception implying the JVM has not released the file.

Exception in thread "main" java.nio.file.AccessDeniedException: .\ExternalDownloader_64.dll at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:83) at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97) at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102) at sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:269)

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
  • Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. – Community Dec 12 '22 at 14:33
  • @DanielWiddis Posted the code – Naveen Rathnam Dec 15 '22 at 02:51
  • @NaveenRathnam Thanks for the code. I don't see any time delay between your dispose and your checking whether it was disposed. The linked answer that I wrote says, "Note that in all these options you may want a small time delay of a few milliseconds after disposing" – Daniel Widdis Dec 15 '22 at 06:10
  • @DanielWiddis tried with time delay of 3000 milliseconds and Files.delete(). still not working. getting below exception : Exception in thread "main" java.nio.file.AccessDeniedException: .\ExternalDownloader_64.dll at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:83) at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97) at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102) at sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:269) – Naveen Rathnam Dec 15 '22 at 18:04
  • @NaveenRathnam Glad you got it solved. If this or any answer has solved your question please consider [accepting it](https://meta.stackexchange.com/q/5234/179419) by clicking the check-mark. This indicates to the wider community that you've found a solution and gives some reputation to both the answerer and yourself. There is no obligation to do this. – Daniel Widdis Dec 19 '22 at 01:30

2 Answers2

1

In the end the problem is, that Native#open is called twice and Native#close only once. The assumption behind the presented code is, that:

    NativeLibrary lib = NativeLibrary.getInstance(dllPath);

yields the same NativeLibrary instance, that is used by:

    DLLUtil = (ExtDLLTool) Native.loadLibrary(dllPath, ExtDLLTool.class);

This assumption does not hold. Indeed NativeLibrary#load does use caching and if invoked with the same parameters it will yield only a single instance.

The codepath behind Native.loadLibrary passes two options to Native#loadLibrary: calling-convention and classloader. The calling-convention is equal to the default calling convention, so can be ignored. It is/would be automatically added in NativeLibrary#getInstance. The classloader though is not set to a default value and there is the difference. The options are part of the caching key and thus a second instance of the NativeLibrary is created and not the first returned.

To make it work, the call to NativeLibrary#getInstance must pass the correct classloader. If you modify the sample like this:

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLibrary;

class Filter {

    private static ExtDLLTool DLLUtil;
    final private static String dllPath = "./ExternalDownloader_64.dll";

    static {
        DLLUtil = (ExtDLLTool) Native.loadLibrary(dllPath, ExtDLLTool.class);
    }

    public static void main(String[] args) throws Exception{

        if (DLLUtil != null) {
            DLLUtil = null;
            NativeLibrary lib = NativeLibrary.getInstance(dllPath, ExtDLLTool.class.getClassLoader());
            lib.dispose();
            Thread.sleep(3000);
        }

        File dllFile = new File(dllPath);
        if(dllFile.exists()){
            Files.delete(Paths.get(dllPath));
            // boolean isDeleted = dllFile.delete();
            if(dllFile.exists()){
                System.out.println("Unable to delete dll file, since it hold by jvm");
            }
        }
    }

    private interface ExtDLLTool extends Library {
        String validateNomination(String dloadProps);
    }
}

it works as expected.

After discussion there is another requirement: The cache path is only hit in a limited number of cases:

  • the library name is the filename of the library (without a prefix)
  • the library name is the absolute path to the library
  • the library name is the "base" name without any prefixes or suffixes the default library search mechanism adds (on windows ".dll" should be stripped, on linux "lib" prefix and ".so" suffix should be stripped) (UNTESTED!)

The TL;DR version: find the absolute path name and use that for interface loading and NativeLibrary loading.

  • My test case still fails to delete the file even with the `getClassLoader()` call. – Daniel Widdis Dec 18 '22 at 17:10
  • Check with a debugger whether you get the same instance of NativeLibrary, that back(ed) the ExtDLLTool (I used the testlibrary from JNA). You can also place breakpoints on the locations where Native#open is invoked, you should only see a single call. – Matthias Bläsing Dec 18 '22 at 17:15
  • Quick test using the toStrings(): `NativeLibrary.getInstance(dllPath)` and `NativeLibrary.getInstance(dllPath, ExtDLLTool.class.getClassLoader())` both return the same Java object `Native Library <..\..\..\temp\test.dll@1667235840>`. And the `ExtDLLTool` string is `Proxy interface to Native Library <..\..\..\temp\test.dll@1667235840>` – Daniel Widdis Dec 18 '22 at 17:20
  • The does not help as the same handle is returned. You must use a debugger or `System#identityHashCode`. I can reproduce the problem with this code: `NativeLibrary lib = NativeLibrary.getInstance(dllPath); lib.close(); Files.delete(Paths.get(dllPath)); ` And can fix it by moving to: `NativeLibrary lib = NativeLibrary.getInstance(dllPath, ExtDLLTool.class.getClassLoader()); lib.close(); Files.delete(Paths.get(dllPath));` – Matthias Bläsing Dec 18 '22 at 17:25
  • That code is [precisely what I'm using](https://i.stack.imgur.com/ZkYgK.png) that does not change the failure to delete. Working on setting up debug. – Daniel Widdis Dec 18 '22 at 17:31
  • Debugging indicates even adding the class loader argument as suggested gives me a different internal NativeLibrary object than the one inside `ExtDLLTool`. – Daniel Widdis Dec 18 '22 at 17:45
  • Even repeatedly calling `NativeLibrary.getInstance(dllPath, ExtDLLTool.class.getClassLoader());` returns a different object each time (verified with both debugger and `identityHashCode`). The Keys and Values in the options hash map are identical but the HashMaps themselves are different objects. – Daniel Widdis Dec 18 '22 at 17:57
  • I think I see the difference. You are using a relative path. Indeed, then you'll never hit the "cache" path. Please try to provide an absolute path as library name. – Matthias Bläsing Dec 18 '22 at 20:32
0

I was able to reproduce the problem with your code, but only on Windows. When reproducible, I was able to successfully delete the file by adding a garbage collection suggestion before the time delay:

if (DLLUtil != null) {
    DLLUtil = null;
    NativeLibrary lib = NativeLibrary.getInstance(dllPath);
    lib.close();
    System.gc();
    System.gc();
    Thread.sleep(3000);
}

When JNA loads a Windows DLL via Native.loadLibrary(), it internally executes the WinAPI LoadLibraryExW function.

Internally the Java instance is stored in a map to be re-used when possible -- however for this to happen, it requires two things to look up the same Java object:

  • the DLL Path must be an absolute path
  • the options must match. In this case, you would need to pass the classloader as an argument as Matthias Bläsing indicated in his answer:
// if loaded like this:
DLLUtil = (ExtDLLTool) Native.loadLibrary(dllPath, ExtDLLTool.class);
// fetch from cache like this:
NativeLibrary lib = NativeLibrary.getInstance(dllPath, ExtDLLTool.class.getClassLoader());
lib.dispose();

This should allow you to delete the file.

However, in your case, with the relative path, the library is getting unloaded but the old java object isn't getting closed until GC occurs.

The dispose() (or close() as of 5.12) call in JNA eventually calls the Native.close() method which uses the Windows API FreeLibrary function. This unloads the DLL from the Process memory, so the advice on the linked question on how to dispose is still accurate in the case that you want to re-load the library. If you're not reloading the library, using dispose() (5.11-) or close() (5.12+) is optional.

If you must use a relative path, consider this approach using a PhantomReference inspired by this answer to track the deletion:

if (DLLUtil != null) {
    // Unload the DLL from process memory
    // Optional here, as it will be called by a cleaner on GC below
    NativeLibrary lib = NativeLibrary.getInstance(dllPath);
    lib.close();
    System.out.println("Closed.");

    // Remove any internal JVM references to the file
    final ReferenceQueue rq = new ReferenceQueue();
    final PhantomReference phantom = new PhantomReference(DLLUtil, rq);
    DLLUtil = null;

    // Poll until GC removes the reference
    int count = 0;
    while (rq.poll() == null) {
        System.out.println("Waiting...");
        Thread.sleep(1000);
        if (++count > 4) {
            // After 5 seconds prompt for GC!
            System.out.println("Suggesting GC...");
            System.gc();
        }
    }
    System.out.println("Collected.");
}

The DLL was successfully deleted following this sequence. It did take a second GC call to take effect:

Closed.
Waiting...
Waiting...
Waiting...
Waiting...
Waiting...
Suggesting GC...
Waiting...
Suggesting GC...
Collected.
Deleted!
Community
  • 1
  • 1
Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
  • Tried your suggestion. still unable to unload / delete the dll. I am using latest jna version 5.12.1. DllUtil.dispose() - not possible. if possible try executing the code. Thanks – Naveen Rathnam Dec 17 '22 at 10:43
  • @NaveenRathnam Reproduced the error with your code; got it to delete by inserting `System.gc()` call before the time delay. Looking in more detail into why that is the case and will update my answer (and the linked one as well) later. – Daniel Widdis Dec 17 '22 at 19:05