Friday, October 17, 2014

Loading DLL in a web-based applet

This is going to be lengthy and technical.

The problem
My project has a page that calls an applet that in turn loads a DLL to perform certain tasks. This works when the page is first loaded, but dies when the page refreshes itself. Log traces indicate that the applet dies when calling the System.loadLibrary with the DLL after the first time. Suspicions rose at the inner workings of either the applet jar in browser cache, garbage collection, or DLL being loaded. My debug research went both ways.

Caching applet.jar in browser
The search started with me locating the various mechanisms used to flag to the browser that the applet is not to be cached. I found out about cache_option and cache_archive pair. I revisited the legacy_lifecycle switch. I'd even attempted to switch over to the "recommended" method of using deployJava.js in place of my <applet> tag. Fortunately and unfortunately, I found out that the jsession variable was already appending to the applet.jar?jsession= due to the framework in place, so that was a dead-end. It still didn't work.

Garbage Collection
Attempts were made to explicitly call System.gc() in the applet stop() and destroy() methods. The class for loading the DLL was set to null prior. Doesn't work.

System.loadLibrary calling of the DLL
Turning my attention to the DLL loading mechanism, I ventured to unload the DLL. The standard pattern was to make use of a static call to System.loadLibrary in a Java class. What could possibly have gone wrong? I found out that people suggested to unload the DLL because they have a need for it. Following their words, I crafted my custom ClassLoader.

My project was a web application. The applet is stored within a jar, hosted on the web server. The custom ClassLoader has to be able to access the http:// URI scheme, read the jar file and dump out the class file from within. I improvised by combining the solutions from here, here and here.

MyCustomClassLoader:
public class MyCustomClassLoader extends ClassLoader {
    private String MyCustomAppletUrl;
    /**
     * The HashMap where the classes will be cached
     */
    private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();

    public MyCustomClassLoader(String url) {
        this.MyCustomAppletUrl = url;
    }

    @Override
    public String toString() {
        return MyCustomClassLoader.class.getName();
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        if (classes.containsKey(name)) {
            return classes.get(name);
        }

        byte[] classData;

        try {
            classData = loadClassData(name);
        } catch (IOException | URISyntaxException e) {
            throw new ClassNotFoundException("Class [" + name + "] could not be found at " + new File(name).getAbsolutePath(), e);
        }

        Class<?> c = defineClass(name, classData, 0, classData.length);
        resolveClass(c);
        classes.put(name, c);

        return c;
    }
   
    private byte[] loadClassData(String name) throws IOException, URISyntaxException {
        URL url = new URL(MyCustomAppletUrl);
        BufferedInputStream in = new BufferedInputStream(url.openStream());
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int i;

        while ((i = in.read()) != -1) {
            out.write(i);
        }

        in.close();
        byte[] jarBytes = out.toByteArray();
        out.close();
       
        JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes));
        JarEntry entry;
        InputStream inStream = null;
        while ((entry = jis.getNextJarEntry()) != null) {
            if (entry.getName().equals(name.replace(".", "/") + ".class")) {
                inStream = jis;
                break;
            }
        }
       
        if(inStream != null) {
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            int nextValue = inStream.read(); 
            while (-1 != nextValue) { 
                byteStream.write(nextValue); 
                nextValue = inStream.read(); 
            }
           
            byte[] classData = byteStream.toByteArray();
            out.close();
            return classData;
        }
        return null;
    }
}


MyCustomLibraryLoader:
public class MyCustomLibraryLoader {
    private Class<?> MyCustomClass;
    private Object MyCustomClassObj;
   
    public MyCustomLibraryLoader(String url) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        MyCustomClassLoader cl = new MyCustomClassLoader(url);
        MyCustomClass = cl.findClass("com.nec.asia.MyCustom.client.MyCustom_Client");
        MyCustomClassObj = MyCustomClass.newInstance();
    }
   
    public String MyCustomClientAction(String inputXML) {
        System.out.println("MyCustomClientAction:");
        Method m;
        try {
            m = MyCustomClass.getMethod("MyCustom_Client_Action", String.class);
            return (String) m.invoke(MyCustomClassObj, inputXML);
        } catch (Exception e) {
            e.printStackTrace();
        }
       
        return null;
    }
   
    public String MyCustomVerifyView(String inputXML) {
        System.out.println("MyCustomVerifyView:");
        Method m;
        try {
            m = MyCustomClass.getMethod("MyCustom_VerifyView", String.class);
            return (String) m.invoke(MyCustomClassObj, inputXML);
        } catch (Exception e) {
            e.printStackTrace();
        }
       
        return null;
    }
}

And guess what? It didn't work. I meant that I managed to use them to retrieve the remote jar, extract the class and load the DLL, but my original problem remained. Reviewing another product with a similar setup proved vital, and had me deleting the above files.

The solution
Yes I had it working in the end. The class file with the native method calls was placed in a separate jar all by its lonely little self. This second jar was then added to the archive param in the applet:
<param name="archive" value="mywebapp-applet.jar,lonely-dll-caller.jar" />

And the applet behaved like my drama was totally uncalled for. Other stuff I decided to keep for now:
  • the legacy_lifecycle parameter
  • use of <applet> instead of <object> or even use deployJava.js
  • don't forget to include 
    • <param name="type" value="application/x-java-applet;" />
    • <param name="scriptable" value="true" />
    • <param name="mayscript" value="true" />
That's all folks! The applet loads the DLL process properly now, even after subsequent reloading of the same page.

No comments:

Post a Comment