views:

61

answers:

2

Hi,

Here's the background of the underlying problem, I am collaborating with a group on a project which uses Swt libraries and I am trying to package the software for deployment. As it turns out SWT is very platform/architecture dependent. I would like to be able to package all six jars (linux, mac, win and 32/64-bit) into the same package and use the appropriate library depending on the system. I realize that it is a tough challenge however, switching to Swing (or anything else) isn't really an option right now.

I have found a number of relevant threads (@Aaron Digulla's thread and @mchr's thread) which provided me valuable insights regarding the problem at hand. I have tried to implement the solution proposed by @Alexey Romanov here. With one difference, as the loadSwtJar() method he proposes is not static, I instantiate the object, and immediately following that, run the method before anything else is done to the object.

It appears as the loading procedure doesn't work properly. My reasoning for this statement is as follows:

  • If all Swt jars are removed from the classpath of the executable jar file, then Exception in thread "main" java.lang.NoClassDefFoundError: org/eclipse/swt/events/MouseListener is thrown which is caused by: java.lang.ClassNotFoundException: org.eclipse.swt.events.MouseListener

to me this means that the libraries are not found on classpath, am I mistaken?

  • If swt jars are left on the classpath then the first jar file is used by the system during execution. Meaning if gtk-linux-x86_64 happens to be the first swt jar on the list of jars then the system tries to use that, regardless if the system is win32 or Mac OSX.

I have tried to add some output to see if the loadSwtJar() method is choosing the right jar, and the output seems right on all platforms I have tried, as in the right package is selected (and the files do exist in the runnable jar). But nevertheless the right library is not loaded hence execution errors occur: Exception in thread "main" java.lang.reflect.InvocationTargetException caused by for ex: Caused by: java.lang.UnsatisfiedLinkError: Cannot load 32-bit SWT libraries on 64-bit JVM (Note that this is the error I get on my Linux machine if I change the order of appearance of 64-bit and 32 bit swt libraries on the build.xml file)

So, what seems to be the problem here? Am I missing out on some detail, or is it simply not possible to check system properties and load an appropriate library accordingly?

Finally below is an excerpt of my build file, figured it might help finding the source of the problem.

Thanks in advance,


EDIT: After a long debug session with a colleague, the problem is resolved (except an annoying bug regarding Thread management on MacOS as I mentioned here). It involved tweaking with the ANT build as well as the way the main class was written. (The main class, as it turns out, was extending & implementing references from the SWT library which meant that the code wouldn't compile at all, wrapped the main class with another class and loaded the SWT jars from there which seemed to be enough to tackle the problem)

Thanks and regards to everyone who contributed, especially @Aaron. Really appreciated!

A: 

You could use Java Web Start as a bootstrap mechanism for your multi platform SWT application. See a corresponding entry in SWT FAQ.

Alternatively, you could put SWT native libraries for each platform into a separate folders and specify them -Djava.library.path in your platform-specific startup script.

Eugene Kuleshov
I have seen the SWT FAQ entry, and was hoping to avoid that, in other words having 6 different files to manage. But I guess it'll have to be the B-plan if this doesnt work out in the end. By the way; just to clarify, the FAQ suggests separating SWT-libraries from the rest of the dependencies and including them at the time of execution by the JNLP file, is that right?
posdef
Correct, JNLP loader will take care of loading platform-specific libs and jars for you.
Eugene Kuleshov
I have been looking more into this example as a backup plan, and I have to say it's rather suspicious that they have not included in the example (even SunOS is included). Besides, is there a full list of possible return values for `<resources os=... arch=...>` calls?
posdef
nevermind my previous comment, I have found a great reference (http://mindprod.com/jgloss/properties.html#OSNAME ) on the matter
posdef
+2  A: 

Here is a copy of the latest version of my Main class. Let me know if that works for you. I tested it on Linux (32/64bit) and Windows (32bit).

package de.pdark.epen.editor;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

import org.apache.commons.lang.SystemUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
import de.pdark.epen.exceptions.WikiException;

public class Main
{
    public final static String VERSION = "V0.9 (13.05.2010)"; //$NON-NLS-1$
    private final static Logger log = LoggerFactory.getLogger (Main.class);

    private static final String ORG_ECLIPSE_SWT_WIDGETS_SHELL = "org.eclipse.swt.widgets.Shell"; //$NON-NLS-1$

    /**
    * @param args
    */
    @SuppressWarnings({"nls", "PMD.SystemPrintln"})
    public static void main (String[] args)
    {
        String msg = "Starting ePen "+VERSION;
        System.out.println (msg);
        log.info (msg);

        LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory ();
        StatusPrinter.print (lc);

        int rc = 1;
        try
        {
            Main main = new Main ();
            main.run (args);
            rc = 0;
        }
        catch (Throwable t) //NOPMD
        {
            ExceptionUtils.printRootCauseStackTrace (t);
        }
        finally
        {
            System.out.println ("Done.");
            log.info ("Exit {}", rc);
            System.exit (rc); //NOPMD
        }
    }

    @SuppressWarnings({"nls", "PMD.SystemPrintln", "PMD.SignatureDeclareThrowsException"})
    private void run (String[] args) throws Exception
    {
        if (!SystemUtils.isJavaVersionAtLeast (150))
        {
            System.out.println ("Version="+SystemUtils.JAVA_VERSION_INT);
            throw new WikiException ("Need at least Java 5 but this Java is only "+SystemUtils.JAVA_VERSION);
        }

        loadSwtJar ();

        URLClassLoader cl = (URLClassLoader) getClass().getClassLoader(); //NOPMD
        Class<?> c = cl.loadClass ("de.pdark.epen.editor.EPenEditor");
        Class<?> shellClass = cl.loadClass (ORG_ECLIPSE_SWT_WIDGETS_SHELL);

        Constructor<?> ctor = c.getConstructor (shellClass);
        Object obj = ctor.newInstance (new Object[] { null });
        Method run = c.getMethod ("run", args.getClass ()); //$NON-NLS-1$
        run.invoke (obj, new Object[] { args });
    }

    @SuppressWarnings({"nls", "PMD"})
    private void loadSwtJar ()
    {
        try {
            Class.forName (ORG_ECLIPSE_SWT_WIDGETS_SHELL);
            // Already on classpath
            return;
        } catch (ClassNotFoundException e) {
            // Add the JAR
        }

        String osName = SystemUtils.OS_NAME.toLowerCase ();
        String osArch = SystemUtils.OS_ARCH.toLowerCase ();

        String swtFileNameOsPart = 
            osName.contains("win") ? "win32" :
            osName.contains("mac") ? "macosx" :
            osName.contains("linux") || osName.contains("nix") ? "linux" :
            null;
        String swtFileNameUiPart = 
            osName.contains("win") ? "win32" :
            osName.contains("mac") ? "cocoa" :
            osName.contains("linux") || osName.contains("nix") ? "gtk" :
            null;

        if (null == swtFileNameOsPart)
        {
            throw new RuntimeException ("Can't determine name of SWT Jar from os.name=[" + osName + "] and os.arch=["
                    + osArch + "]");
        }

        String swtFileNameArchPart = osArch.contains ("64") ? ".x86_64" : ".x86";
        if(".x86".equals(swtFileNameArchPart) && "macosx".equals(swtFileNameOsPart)) {
            swtFileNameArchPart = "";
        }

        String swtFileName = "org.eclipse.swt." + swtFileNameUiPart + "." + swtFileNameOsPart + swtFileNameArchPart + "-3.6.0.jar";
        File file = new File ("swt", swtFileName);
        if (!file.exists ())
        {
            throw new RuntimeException ("Can't locate SWT Jar " + file.getAbsolutePath ());
        }
        try
        {
            URLClassLoader classLoader = (URLClassLoader) getClass ().getClassLoader ();
            Method addUrlMethod = URLClassLoader.class.getDeclaredMethod ("addURL", URL.class);
            addUrlMethod.setAccessible (true);

            URL swtFileUrl = file.toURI ().toURL ();
            log.info ("Adding {} to the classpath", swtFileUrl);
            addUrlMethod.invoke (classLoader, swtFileUrl);
        }
        catch (Exception e)
        {
            throw new RuntimeException ("Unable to add the swt jar to the class path: " + file.getAbsoluteFile (), e);
        }
    }
}
Aaron Digulla
Thanks Aaron. Tho I have to say I am not quite sure how you dig to find the swtFiles, do you have your swt files inside one big jar with all the other dependencies or in some other way?
posdef
@Aaron: one further question, have you experienced any problems with SWT packages being unsigned? or is it just me...
posdef
@posdef: I put all normal JARs into a directory called `lib/` and the six SWT JARs into `swt/`. If you put them all into one directory, chances are that the classpath from the main JAR's manifest will try to load the wrong one.
Aaron Digulla
@posdef: I strongly suggest to use the original JARs unmodified. Otherwise the loading of the DLLs will fail. I couldn't figure out why. If I need changes in the SWT classes, I copy the source into a different package and modify it there. That usually leads to a lot of copying but that's how SWT works.
Aaron Digulla
We use a very similar approach: dynamically loading the SWT library. Each is in a separate directory, for instance, `libs/linux/x64/swt.jar`. My understanding is that obtaining the main class loader *and* setting `addURL()` to accessible are more or less hacks, and may possibly be fixed as security patches in the future. I hope they will not, and can confirm this method presently works on JRE 1.5 and up on all platforms, but feel that a warning may be in order.
Paul Lammertsma
@Paul: Agreed. The fix is simple: If it stops working, you can simply create a new `URLClassLoader`, use the current CL as parent and load the classes with the new CL.
Aaron Digulla
@Aaron: I gotta modify the names of the SWT jars and your code a bit to fit my system here, but shouldn't be too complicated. I'll get back on that try. As for the SWT jars being unsigned, here's the thing; I didn't even touch them, just renaming as they recommend on the FAQ [http://www.eclipse.org/swt/jws/]
posdef
@Aaron: I have just tried to run your code, with small modifications to fit the files I have here. Unfortunately, no joy on that shot... See my edit on the original post.
posdef
@posdef: You must compile against the version of SWT for the build platform. Later, when you run the result, you can use any SWT JAR: they contain the same API classes and can be exchanged. But of course for compiling, you need the right one.
Aaron Digulla
@Aaron: In other words, I compile with the appropriate jar for my platform (linux_64) on the classpath however I supply all six jars in the package? Is it important if the "jar package" I create has the initial jar on the classpath? (Sorry for the fuzzy question)
posdef
@posdef: Re compile: Yes. Re package: No! Only one single SWT JAR must be on the classpath. Note that you can *mention* it but Java must be unable to load the file until the code above is execute. So it's safe to have the compile JAR in the classpath-string saved in the executable JAR which you create as long as the JAR-file isn't there.
Aaron Digulla
@Aaron: ok, I just followed these instructions but still no joy.. `Caused by: java.lang.ClassNotFoundException: org.eclipse.swt.events.MouseListener at java.net.URLClassLoader$1.run(URLClassLoader.java:202) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:307) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:248) ... 12 more`
posdef
Glad that it worked out :-)
Aaron Digulla