3

tl;dr: C++ plugin needs to call Java .jar library. How to deploy this to users without too much headache?

I'm writing a Qt plugin for a Qt application. The plugin needs to make calls to an existing Java library. This needs to work cross-platform (Win, Mac, Linux) and architecture (32-bit and 64-bit Intel, no PPC).

I got a simple "hello world" JNI example compiling and running. I updated the CMake script to "find_package(JNI REQUIRED)" etc. so it compiles against the jni.h header and dynamically links against the JVM library.

On Windows at least, CMake does a good job of finding the right JVM to use at compile time. It's finding the right JRE (jvm.dll, etc.) at runtime that concerns me, since I have less control over user's computers.

How will it work when I send the plugin to my users? They will need a JRE installed for the proper architecture. But that doesn't mean the JRE lib directory(ies) will be in their path. If they're not, the plugin just bails out and doesn't load.

It also seems troublesome that on Windows, the 64-bit JDK installed jvm.dll to:

C:\Program Files\Java\jre7\bin\server\jvm.dll

But the 32-bit JDK installed it to:

C:\Program Files (x86)\Java\jre7\bin\client\jvm.dll

I understand the PF vs. PFx86 difference, but the server/client thing I don't get. Are these actually different JREs?

Will it work if I have compiled/linked against one JRE version and the user has a different version?

I assume this will all be easier on Linux/Mac, but I haven't gotten that far yet.

Any help is appreciated. I'm not tied to using JNI, but can't afford a $2000 compiler to turn the Java into a native code library (not that I have the source anyhow), and I hear gcj may not be up to the task (and probably wouldn't help much on Windows).

Dan
  • 5,929
  • 6
  • 42
  • 52
  • You could deploy your own jre with your application, i guess that would be the simplest way to solve your problem. That way you could ensure that your native library works with the jre and you won't have to struggle with dll lookup problems. – Ortwin Angermeier Feb 02 '12 at 22:18
  • I assumed license issues would prevent me from deploying my own JRE. Any suggestions on how to do this? Also, in this case I have control over the application, but hypothetically, what if I didn't and was just writing a plugin? – Dan Feb 02 '12 at 22:22
  • I have never done that before, i was just thinking about Open Office. If i remember correctly they had an install option that included a jre. If you are just deploying a plugin, i guess you would have to write a install script that ensures java is installed with the right version and sets up your path variables. – Ortwin Angermeier Feb 02 '12 at 22:32
  • @Dan, how is the plugin installed or is there any configuration? – hmjd Feb 02 '12 at 22:42
  • @hmjd, On Windows it's just a single .dll that gets copied into an application-specific plugins directory. Basically the same on Mac/Linux except users compile the plugin from source and copy resulting .so or .dylib into place. – Dan Feb 02 '12 at 22:53
  • I had a similar issue to yours, on Windows only, and resolved it by using delayed DLL loading and informing my binary where its JRE was installed (avoiding depending on a system wide environment variable which would be problematic). If it is possible for you to modify your DLL to either read a registry value or configuration file to know which JRE to use I would have a possible solution for Windows only. – hmjd Feb 02 '12 at 22:57
  • @hmjd, I was considering that after looking at http://java.sun.com/docs/books/jni/html/invoke.html#24891 -- could work on linux/mac too with some #ifdef's for search path and dlopen/dlsym vs. LoadLibrary/GetProcAddress. Does that end up being a major PITA to have to look up a function pointer for every damn symbol? Any tips/examples much appreciated! – Dan Feb 02 '12 at 23:29
  • Hmm... wonder if Qt has an abstraction for dlopen type stuff... need to read up on QLibrary. – Dan Feb 02 '12 at 23:29

3 Answers3

2

Some more detailed notes to add to @hmjd's answer, a lot of these details tripped me up.

This is the command line I used to compile and link a test program:

cl
  -I"C:\Program Files (x86)\Java\jdk1.7.0_02\include"
  -I"C:\Program Files (x86)\Java\jdk1.7.0_02\include\win32"
  /EHsc
  -MD
  Test.cpp
  jvm.lib
  delayimp.lib
  /link
  /LIBPATH:"C:\Program Files (x86)\Java\jdk1.7.0_02\lib"
  /DELAYLOAD:jvm.dll

A few things to note:

  1. Include paths to the header files (jni.h, etc.)
  2. Add /EHsc to avoid this warning: "c:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\INCLUDE\xlocale(323) : warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc"
  3. Still need to include jvm.lib, even though it should be delay loaded.
  4. delayimp.lib has to be included.
  5. /LIBPATH needs to go after the /link option so the linker gets it. This is the path to the .lib file.
  6. /DELAYLOAD gets the .dll file, not the .lib file! If you accidentally give it the .lib file you don't get a useful error message, it just says "LINK : warning LNK4199: /DELAYLOAD:jvm.lib ignored; no imports found from jvm.lib".

In the .cpp file, #include "windows.h", figure out which directory has the jvm.dll, and make a call such as this:

std::string temp = "C:\\Program Files (x86)\\Java\\jdk1.7.0_02\\jre\\bin\\client";
SetDllDirectory(temp.c_str());

Do that before calling any function from the library, so when LoadLibrary() is called it knows where to find the DLL. An alternative is to modify the PATH variable with SetEnvironmentVariable() but SetDllDirectory() seems like a better choice.

Somewhere in your startup code, add code like this:

__try
{
  // call a function from the DLL to make sure it can be loaded
  ::JNI_CreateJavaVM(...);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
  // If not, fail
}

Best to put that in its own function somewhere, since the __try/__except Structured Exception Handling (SEH) stuff doesn't tolerate sharing a function with code that might doing object unwinding.

Without the SEH stuff, the program just crashes if the DLL can't be found.

Some links that were helpful:

Community
  • 1
  • 1
Dan
  • 5,929
  • 6
  • 42
  • 52
1

A possible solution for Windows only.

Build the QT plugin using the delayed DLL load feature

See DELAYLOAD on how to do this but it is just adding /DELAYLOAD:jvm.dll and Delayimp.lib to the linker command. It means that the jvm.dll will not be loaded when the QT plugin is loaded but at the point it is required (note it does not require using LoadLibrary() and GetProcAddress()). (I don't know if there is a similar feature on Linux or Mac)

Provide a mechanism to inform the QT plugin of what JRE to use

This could be either a registry value, a configuration file or an environment variable specifically for the plugin (definitely not environment variables JAVA_HOME or JRE_HOME that other applications may depend on). Example environment variables:

  • DANQT_32_JRE_HOME=C:\Program Files (x86)\Java\jre7 (for 32-bit JRE)
  • DANQT_64_JRE_HOME=C:\Program Files\Java\jre7 (for 64-bit JRE)

QT Plugin Modify its PATH

Before the QT plugin invokes any functions dependent on the JRE it modifies its PATH environment variable by inserting, for example, %DANQT_32_JRE_HOME%\bin\server;%DANQT_32_JRE_HOME%\bin\client; at the start of the value for PATH. This means when the QT plugin performs its first action that requires the JRE it will be loaded from the inserted directories. (Different environment variables for 64-bit). As for bin\server and bin\client my understanding is that these are essentially the same but the server performs more during initialisation for runtime performance reasons.

I am unsure of compatibility if the QT plugin was built against JRE 6 and JRE 7 was installed. If there are compatibility issues then make it a prerequisite installation requirement or, if permitted (I am unsure of legalities), ship the jvm.dll with the QT plugin.

hmjd
  • 120,187
  • 20
  • 207
  • 252
  • Thanks so much for your help! This looks like a great solution. Sounds like gcc does lazy loading by default, so I might not even have to do anything there (depends on whether I can tweak the appropriate PATH variable before the loading occurs). If this doesn't work, I can fall back on QLibrary and just write a bunch of wrapper functions. – Dan Feb 03 '12 at 17:53
0

Since I can't figure out an equivalent to /DELAYLOAD on Linux (asked here: How can I make lazy/delay loading work in Linux?), I'm stuck using dlopen(). It's not as bad as I thought it would be -- I can use the technique described here: Alternatives to dlsym() and dlopen() in C++ . In fact, it seems like the jni.h header was designed for this approach. Probably what I should have done in the first place, on Windows too.

Community
  • 1
  • 1
Dan
  • 5,929
  • 6
  • 42
  • 52