Bug 125250 - [doc] JNI Cannot Find Java Routines with RCP
Summary: [doc] JNI Cannot Find Java Routines with RCP
Status: RESOLVED FIXED
Alias: None
Product: Platform
Classification: Eclipse Project
Component: Releng (show other bugs)
Version: 3.2   Edit
Hardware: PC Windows All
: P3 normal (vote)
Target Milestone: 3.2 RC7   Edit
Assignee: Sonia Dimitrov CLA
QA Contact:
URL:
Whiteboard:
Keywords: Documentation
Depends on:
Blocks:
 
Reported: 2006-01-25 16:01 EST by Kenneth Evans, Jr. CLA
Modified: 2011-01-03 11:41 EST (History)
4 users (show)

See Also:


Attachments
readme patch (3.82 KB, patch)
2006-06-01 18:06 EDT, Thomas Watson CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Kenneth Evans, Jr. CLA 2006-01-25 16:01:30 EST
Hopefully this has been submitted in the right place and with the
right priority.  (It is a stopper for me.)

I have an RCP application that uses SWT and a JNI package of my own.
I also have a stand-alone SWT version that does essentially the same
thing.  In both of these, the JNI part can be replaced with a pure
Java implementation.  Apart from a switch that uses the full Java
version or not, they all use the same package.  The JNI implementation
is old code that has been working for some time, both in and out of
Eclipse.  All of these versions work except the RCP application using
JNI.  That is, it is not a bug in the package nor in making the RCP
product.

The RCP JNI version fails, eventually giving an exception, because the
C routine FindClass in the following C++ code returns null:

extern "C" {
  static int theMessageCallback(const char* pFormat, va_list args) {
    jobject callback= (jobject) epicsThreadPrivateGet(messageCallbackID);
    if (callback==NULL) return ECA_NORMAL;
    JNIEnv* env;

    _jvm->AttachCurrentThread((void**)&env, NULL);

    char buf[1024];
    sprintf(buf, pFormat, args);
    jstring msg= env->NewStringUTF(buf);

    jclass clazz = env->FindClass("gov/aps/jca/jni/JNIContextMessageCallback");

    jmethodID methodID = env->GetMethodID(clazz, "fire",
      "(Ljava/lang/String;)V");
    env->CallVoidMethod(callback, methodID,  msg);
    _jvm->DetachCurrentThread();
    return ECA_NORMAL;
  }
}

This is a C callback, and the idea is to call the Java callback with
the string from the arguments.  (The problem is that clazz is null.
env is not null.)  The callback gets called from code in another DLL
that is independent of the JNI wrappers.  That library knows nothing
about Java, and the callback prototype doesn't have an env as an
argument.  It calls the Java routine that will handle the callback in
Java, getting the env from the thread.  This is different from what is
more commonly done, which is to have a C implementation of a Java
class, that gets passed the env, which (indirectly) knows about the
class.

The reason it fails is that the C function, FindClass, which is purely
J2SDK, not Eclipse, uses the classpath to find the class.  With an RCP
application, Eclipse apparently uses <app-dir>/startup.jar as the
classpath.  This is determined by printing out java.class.path from
the running app.  startup.jar has very little in it and little, if
anything, that is specific to the particular RCP app.  Consequently
FindClass does not find the class in this case (because it is not in
the classpath).  The RCP app does find the other classes in whatever
manner Eclipse uses to locate jars in plug-ins, presumably using its
own classloaders.  It works OK until the above C callback gets called.

The reason it works standalone (not a plug-in in an RCP app) is that I
specify the classpath specifically to include that jar, and FindClass
uses that classpath and finds it.  (The reason it works with the pure
Java version is that FindClass is not used.)

I was unable to get the RCP app to use my classpath, for instance by
specifying it in vmargs or myapp.ini.  Eclipse apparently *overwrites*
the classpath with the above.  I would consider this also to be a bug.
It should append or use what is specified on the command line (which
could include startup.jar).

I can make it work by manually putting the package in startup.jar.
This allows FindClass to locate it in the classpath (while Eclipse
apparently finds it in the plugin directory, since the jar files are
really in another plug-in).  This is not a good solution,
(if for no other reason than that the class is in two places, now) but
it demonstrates the problem, which seems to be that FindClass does not
work in the Eclipse environment when called from C or C++ code that is
not directly part of the native implementation of a Java method and
that the reason is because the J2SDK FindClass cannot find the class
as Eclipse can.

This is a stopper for me.  It is a reasonable and straightforward
thing to do.  I think it is an oversight in the Eclipse
implementation.

I cannot change the implementation, as it is not my code.  In
addition, it is a correct JNI implementation.

I apologize for this being long.  The problem is somewhat deep and
hard to easily understand.  I would be happy to elucidate, if
necessary.
Comment 1 Jeff McAffer CLA 2006-01-26 15:39:16 EST
Your analysis appears complete.  Thanks!

Yes, the classpath used/expected by Eclipse is just startup.jar.  You can put more stuff there but it will never be picked up by anything in Eclipse itself.  This is desired behaviour and is true of several (if not all) OSGi implementations.  OSGi bundles are exposed only to the types they import/require.  Adding things to the classpath does not export/provide them to OSGi so they will never be found.

Having said that, it would seem to be overkill for us to be overwriting anything you did happen to put on the classpath when running Eclipes.  For example, did you try eclipse.exe -classpath startup.jar;foo.jar and have it not work?  I'm pretty sure that you can do this sort of thing (ie., I believe i have done it in the past).  If you can't please open a bug report with details.

If it is a problem, try running Eclipse directly using java.exe rather than eclipse.exe.  There is no real magic there.   Just something like 
   java -cp startup.jar;foo.jar org.eclipse.core.launcher.Main -application/product <id>

You can likely get what you want if you add to the bootclasspath either on the command line or in eclipse.ini.  Note that this will force all bundles to have direct access to the classes you put there regardless any import/export statements in the manifests.  That is, the duplicate copy in your plugin will be ignored in favour of this since the boot loader (not the app loader) is alwasy consulted first.

Finally, you can eliminate some duplication by putting the foo.jar on the appclasspath and use either osgi.parentClassloader=app or the app buddy loading policy (see the 3.1 help) in your bundles that use the classes on the app classpath.  

Note that most of these approaches are somewhat hard to manage since they involve globally providing a set of classes and messing with global configuration information. 

As for FindClass, not sure what so say there.  The function should really take a classloader or object to start from rather than just assuming the that it should use the app classloader.  There are lots of other systems with which this approahc will not work (JBoss, context classloaders, ...)
Comment 2 Kenneth Evans, Jr. CLA 2006-01-26 17:28:38 EST
Thanks for your reply.

1. It is my experience that the app.exe (eclipse.exe) overwrites
anything you specify on the classpath with <app-dir>/startup.jar.  I
had previously tried setting -classpath in -vmargs both through the
app.ini and via entering it as an argument to app.exe.  I verified
that javaw is started as a separate process with -classpath as I
specify.  After that, if I print out java.class.path in the program or
display it in a dialog, it is always <app-dir>/startup.jar.  I just
tried using -classpath instead of -vmargs -classpath, as you
suggested, and it still prints <app-dir>/startup.jar.  (I was not
aware that -classpath was an eclipse.exe option.)  That it behaves
this way should be easy for you to confirm.

2. I don't want to do that anyway.  I want the jars for the JNI
package to be in a plug-in (or eventually plug-ins).  Trying to
overwrite the classpath is a kludge, and, as you said, hard to manage.
I only tried it to verify my diagnosis of the problem was accurate.

3. The problem is really that FindClass does not work under Eclipse
(under these conditions) and that FindClass is an essential part of
JNI.  I cannot rewrite FindClass.  It is part of Java.  I cannot even
rewrite the package as I do not own it.  Moreover, it is a working
Java package outside of Eclipse, and what is done is standard
practice.  This package is part of control system software used to
control accelerators and other large physics experiments around the
world, and I work in that community.  In addition to control systems,
I would like to use Eclipse to manage X-ray scattering data analysis.
Most of the analysis codes (literally hundreds) are written by
scientists, often in FORTRAN or C, seldom in Java.  Thus, I need to be
able to access native code, whether that is elegant or not.  If
FindClass and JNI does not work, I am pretty much out of business in
using Eclipse, which would be unfortunate.

I am not sufficiently familiar with the internal workings of Eclipse
(or perhaps even Java) to see a way to get around this problem.  I
think there is incentive for the appropriate developers to look at it.
It is a generic problem with Eclipse and JNI.
Comment 3 Kenneth Evans, Jr. CLA 2006-01-26 17:30:18 EST
Thanks for your reply.

1. It is my experience that the app.exe (eclipse.exe) overwrites
anything you specify on the classpath with <app-dir>/startup.jar.  I
had previously tried setting -classpath in -vmargs both through the
app.ini and via entering it as an argument to app.exe.  I verified
that javaw is started as a separate process with -classpath as I
specify.  After that, if I print out java.class.path in the program or
display it in a dialog, it is always <app-dir>/startup.jar.  I just
tried using -classpath instead of -vmargs -classpath, as you
suggested, and it still prints <app-dir>/startup.jar.  (I was not
aware that -classpath was an eclipse.exe option.)  That it behaves
this way should be easy for you to confirm.

2. I don't want to do that anyway.  I want the jars for the JNI
package to be in a plug-in (or eventually plug-ins).  Trying to
overwrite the classpath is a kludge, and, as you said, hard to manage.
I only tried it to verify my diagnosis of the problem was accurate.

3. The problem is really that FindClass does not work under Eclipse
(under these conditions) and that FindClass is an essential part of
JNI.  I cannot rewrite FindClass.  It is part of Java.  I cannot even
rewrite the package as I do not own it.  Moreover, it is a working
Java package outside of Eclipse, and what is done is standard
practice.  This package is part of control system software used to
control accelerators and other large physics experiments around the
world, and I work in that community.  In addition to control systems,
I would like to use Eclipse to manage X-ray scattering data analysis.
Most of the analysis codes (literally hundreds) are written by
scientists, often in FORTRAN or C, seldom in Java.  Thus, I need to be
able to access native code, whether that is elegant or not.  If
FindClass and JNI does not work, I am pretty much out of business in
using Eclipse, which would be unfortunate.

I am not sufficiently familiar with the internal workings of Eclipse
(or perhaps even Java) to see a way to get around this problem.  I
think there is incentive for the appropriate developers to look at it.
It is a generic problem with Eclipse and JNI.
Comment 4 Jeff McAffer CLA 2006-01-26 21:38:34 EST
I understand the problem and appreciate your situation but fundamentally there are many cases for which the FindClass approach is broken.  At a very basic level Eclipse the classpaths managed by Eclipse form dynamic acyclic digraph, not a fixed linear progression as expected by the app class loader.  We can, through some of the mechanisms mentioned allow plugins to see things on the app classpath but what you want is for someone looking at the app classpath from C to see classes in plugins.

I hope I am missing something here but don't think I am.
Comment 5 Kenneth Evans, Jr. CLA 2006-01-27 13:15:20 EST
The big picture is (if what you say is correct and cannot be fixed)
that JNI is not usable with Eclipse, or more accurately, that it is
only usable in a limited fashion.

>> what you want is for someone looking at the app classpath from C to
>> see classes in plugins

Yes.  A significant part of JNI is that you can call Java from C.  If
my package is implemented in a plug-in, then I should be able to call
back into the methods in its classes.
Comment 6 Kenneth Evans, Jr. CLA 2006-01-29 14:13:24 EST
>>As for FindClass, not sure what so say there.  The function should
>>really take a classloader or object to start from rather than just
>>assuming the that it should use the app classloader.

What I have found is the following:
  
(http://java.sun.com/j2se/1.4.2/docs/guide/jni/jni-12.html#FindClass)
FindClass locates the class loader associated with the current native
method. If the native code belongs to a system class, no class loader
will be involved. Otherwise, the proper class loader will be invoked
to load and link the named class.  (We are not a system class, so the
latter should happen.)

On the other hand, it appears that the JNI defaults to
JNI_VERSION_1_1, which doesn't know about class loaders and uses the
classpath.  This may be the problem, and I will check and get back.
If so, it is my bug.
Comment 7 Jeff McAffer CLA 2006-01-29 14:33:56 EST
Great!  Please to let us know.  This could go in a FAQ somewhere.
Comment 8 Kenneth Evans, Jr. CLA 2006-02-01 17:15:37 EST
I have checked, and the version is not the problem.  There is a
GetVersion method (C code) and it returns JNI_VERSION_1_4.  Thus, it
is well beyond JNI_VERSION_1_1 AND should invoke "the proper class
loader" as in the reference I quoted (and others).  The only thing *I*
can change is to add an OnLoad method that returns the required
version.  However, this is only used to allow access to new functions
at the end of the JNI function tables, and I do not use any of the new
functions, just basic ones like FindClass().  I have tried all of the
three possibilities (JNI_VERSION_1_n, with n = 1, 2, and 4) anyway.  I
am using j2sdk1.4.2_06.  The jdk1.5.0_05 jni.h has the same options.
I have also tried the jdk1.5.0_05 runtime.  The same thing happens:
It cannot find the class using FindClass() from the callback.

I have also debugged on it.  If I do:

this.getClass().getClassLoader().loadClass("MyClass");

then it uses org.eclipse.core.runtime.adaptor.EclipseClassLoader.

However, it appears to be in java.net.URLClassLoader, when it cannot
find the class from the JNI FindClass.  Thus, Java's idea of "the
proper class loader" is different from ours.

It thus looks like JNI cannot be made to work with jar files in a
plug-in under Eclipse, at least a long as Eclipse overwrites the
java.class.path with one that does not contain the requested class.
The JNI implementation relies on the class being in the
java.class.path.

This is getting deeper into the internal workings of Eclipse and Java
than my expertise warrants.  I think it is an important shortcoming of
Eclipse, and should be fixed, or perhaps there is a work around.  To
fix it or work around it probably takes more knowledge and access than
I have.  That is why I have submitted it as a bug.

I will be happy to elaborate if this is not clear.
Comment 9 Simon Kaegi CLA 2006-02-02 00:23:41 EST
Hi Kenneth,
I've been following this bug and might be able to help. I've been using JNI and FindClass callbacks in Eclipse without the problems you're having.

I want to clarify something about your circumstances. Is the callback happening in the second DLL (e.g. a DLL other than the one with the JNI wrappers loaded via System.loadLibrary(...))? If so I suspect that's the problem; only the first DLL will be associated with the plugin's classloader. This isn't really just an Eclipse thing, it's a Java/JNI thing.

The other DLL will normally be loaded by the OS process (outside of the java world) and won't have an associated classloader to provide context. This is why it ends up using the App Classloader.

One dirty trick that you might try is using System.loadLibrary(xxx) to load that 2nd DLL. In this case the process shouldn't have to load the DLL again and "maybe" the DLL will still do its class lookup with the plugin's classloader.

Beyond this I think Jeff's suggestions in comment #1 should work and are probably the best way to go.
Comment 10 Kenneth Evans, Jr. CLA 2006-02-02 10:39:47 EST
Simon,

Thanks for your reply and suggestions.  Yes, you are correct that the
callback that uses FindClass is coming from a 2nd DLL loaded by the
JNI DLL.  Moreover, it is in its own C thread, so it's not clear what
AttachCurrentThread is doing (except that it is not what we want).
The JNI documentation does not go into much depth, and I haven't been
able to find if the source code is available.  In addition, I am not
very experienced with JNI, though I probably will be if I continue on
my current path: I need to incorporate FORTRAN code into RCP apps,
which should be even more fun.

Your suggestion may work.  The C callback in question will also take a
void * clientData when created and will return that to the callback.
I am considering passing the env from the routine that creates it and
using that env->FindClass.  If that doesn't work, there should be
something else I can pass that gets us back to the EclipseClassLoader.

The solutions in Comment #1 are less attractive.  I would, of course,
like to just use the Product Export Wizard and other tools provided by
Eclipse without extra manual steps.  (KISS)

It is becoming clear that it is more of a problem in using Eclipse
than a bug.  I do, however, think Eclipse should not overwrite a
classpath specified after vmargs.  I believe in enabling the user.
If he shoots himself in the foot, that is his problem.  The good ones
don't.

In my defense, it is tested code that works well outside Eclipse.
Comment 11 Jeff McAffer CLA 2006-02-03 22:25:54 EST
We need a bug report for the overwriting of the classpath when using eclipse.exe.  Pleaes add the details of your usecase (e.g., examples)
Comment 12 Kenneth Evans, Jr. CLA 2006-02-05 12:39:16 EST
To demonstrate the overwriting of the classpath:

1. Make a Hello World RCP application via the New Project wizard.

2. Add the following lines to Application.java in the run method:

      // Get Properties
      String info = "";
      info += "\nProperty Information\n";
      String [] name = {
        "java.library.path",
        "java.class.path",
      };
      for(int i=0; i < name.length; i++) {
        String value = System.getProperty(name[i]);
        String line = name[i] + ": " + value + "\n";
        info += line;
      }
      System.out.print(info);

3. Make a Run configuration and put the following in the VM arguments:

-cp junk1 -Djava.library.path=junk2 -Djava.class.path=junk3

I get:

Property Information
java.library.path: junk2
java.class.path: c:\eclipse\startup.jar

It ignores the classpath. You can also try to specify the classpath
in your own way.  The classpath is ignored in any way I have tried.

If this is not sufficient, I can send the project, but it doesn't
seem necessary.  The above is the only change.  The problem is easy
to demonstrate.
Comment 13 Jeff McAffer CLA 2006-02-05 15:56:06 EST
I opened bug 126499 to deal with the launcher overwrite problem.
Comment 14 Kenneth Evans, Jr. CLA 2006-02-10 13:57:24 EST
I believe I have worked around the problem.  The workaround seems to
be to cache the method ID and to use the cached value instead of
calling FindClass in the callback.  (This is also more efficient.)

In my case the callback comes from a native thread that is different
from, and otherwise independent of, the thread in which the JNI was
loaded.  I do not know if this is a necessary condition or if
FindClass, called in a function that needs AttachCurrentThread, always
uses the classpath, as it does in my case.  The JNI documentation
(http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/functions.html) is
ambiguous:

It says: This function loads a locally-defined class. It searches the
directories and zip files specified by the CLASSPATH environment
variable for the class with the specified name.

It also says: The Java security model has been extended to allow
non-system classes to load and call native methods.  In the Java 2
Platform, FindClass locates the class loader associated with the
current native method. If the native code belongs to a system class,
no class loader will be involved. Otherwise, the proper class loader
will be invoked to load and link the named class.

and: When FindClass is called through the Invocation Interface, there
is no current native method or its associated class loader.  In that
case, the result of ClassLoader.getBaseClassLoader is used.  This is
the class loader the virtual machine creates for applications, and is
able to locate classes listed in the java.class.path property.

My experience is that if the desired function is in the classpath,
there is no problem.  However, under Eclipse, it is not in the
classpath, it does not use the Eclipse class loader, and it does not
find the class.

I presume the JNI folks would say it is using the Invocation
Interface in this case.

It is probably not feasible to change the Eclipse behavior; however,
it is a problem.  It was mentioned that a FAQ might be appropriate.

This would be my crack at one:

JNI implementations that work outside of Eclipse, may not work under
Eclipse.

In particular, there is a problem using Eclipse with a JNI
implementation that uses FindClass in a function where the JNIEnv
pointer is not available, such as in a C callback.  The reason is that
FindClass, in this case, seems to use the classpath to find the class.
If the desired function is in the classpath, as it would typically be
in a stand-alone application, there is no problem.  However, under
Eclipse, it is not in the classpath, because Eclipse, by itself or as
an RCP application, overwrites the classpath with startup.jar, which
only includes generic launcher classes.  Eclipse then uses its own
class loader to find classes.

The Eclipse class loader does appear to be used by FindClass in JNI
functions which are passed the JNIEnv pointer, but not when you have
to use AttachCurrentThread to get the JNIEnv pointer.

Example:

static JavaVM* jvm;  // Global variable

void myCallback(void) {
    JNIEnv* env;
    jvm->AttachCurrentThread((void**)&env, NULL);
  // Fails if "some/class" is not in the classpath:
    jclass cls = env->FindClass("some/class");
    jmethodID methodID = env->GetMethodID(cls, "methodName",
      "(Ljava/lang/String;)V or whatever signature");
    env->CallVoidMethod(callback, methodID, ...);
    jvm->DetachCurrentThread();
  }
}

A solution is to cache the method ID, for example:

static jmethodID mid;  // Global variable

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
...
  // Store the JavaVM pointer
    jvm = vm;

  // Find the class and store the method ID
  // Will use the class loader that loaded the JNI library
    jclass cls = env->FindClass(className"some/class");
    if(!cls) goto ERR;

    mid = env->GetMethodID(cls, "methodName",
      "(Ljava/lang/String;)V or whatever signature");
    if(!mid) goto ERR;
...
}

void myCallback(void) {
    JNIEnv* env;
    jvm->AttachCurrentThread((void**)&env, NULL);
    env->CallVoidMethod(callback, mid, ...);
     // Handle error ...
    jvm->DetachCurrentThread();
  }
}
Comment 15 Jeff McAffer CLA 2006-02-11 21:01:49 EST
Thanks for all the time everyone has spent investigating this.  Thanks also for the FAQ doc.  I'm downgrading this bug and marking as a doc bug so that we are sure to include the text somewhere.  Perhaps we can put it on the wiki?  I'm not sure where it would go in the formal doc.
Comment 16 Alex Blewitt CLA 2006-02-18 21:25:48 EST
As noted on bug 126499, the fact that the classpath is 'overwritten' is not a bug, and that's not what's happening. Eclipse runs two VMs; one is the one you launch from the command line (eclipse.exe), and the other one is the one that startup.jar launches. What you're finding is that VM1's classpath, set on the command line, doesn't match VM2's classpath (the one that your RCP app and any other code is written in). So specifying the classpath on the runtime argument doesn't have any effect.

That's why when Eclipse goes down, you get a nice dialog box giving you information. That box is from VM1, and that's the one whose classpath you have set. It's also why the Update Manager can restart Eclipse after an update; it returns with a specific error code that signals to VM1 that it should be relaunched afterwards.

Frankly, the fact that there are two VMs in operation at this time may result in unexpected/unexplained behaviour. If there were a global variable that was being expected to be set by the JVM when it initialised, then what would be the effect if a second VM was started in the same process? Would it get overwritten? My guess is probably not (though I couldn't be sure). It wouldn't surprise me if this wasn't a bug at all, and in fact you're connecting to VM1 and VM1's thread (that is blocked on the synchronous call to fire up the Eclipse engine) and not VM2 and VM2's thread which has the classloader set.

Try running that code and accessing 'org.eclipse.core.launcher.Main' as a class. You won't find that in the Eclipse runtime (I think; but best check by firing up a quick RCP app to ensure), but you will find it in the bootstrap VM. That will easily tell you whether you're looking at the right VM or not, and with it, the answer to all of your questions.

In summary; cache the VM that you expect to see, and not one that may be provided, so that you know which one you're looking at.

Regarding other comments (E.g. eclipse -classpath foo.jar;startup.jar) -- that is *only* going to change the classpath of VM1, not VM2. You might get better mileage if you put the -classpath in the 'eclipse.ini' (or app.ini) file, or after the -vmargs argument, since that controls the options from VM2.

Alex.
Comment 17 Jeff McAffer CLA 2006-03-01 22:42:12 EST
just to clarify, eclipse.exe starts only one vm.  in fact, it is the other way around.  several eclipse.exe's are run.  One to launch the vm then the startup code of Eclipes (in java) launches another to run the splash screen.
Comment 18 Pascal Rapicault CLA 2006-04-06 16:43:26 EDT
Need to doc the behavior described by Simon in comment #9.
Comment 19 Thomas Watson CLA 2006-06-01 18:06:57 EDT
Created attachment 43315 [details]
readme patch

This patch adds a section to the readme for this issue.
Comment 20 Thomas Watson CLA 2006-06-01 18:08:42 EDT
Releng team, please release the readme to document this limitation.  Thanks.
Comment 21 Sonia Dimitrov CLA 2006-06-01 22:42:48 EDT
Patch released.
Comment 22 Oliver Luecking CLA 2011-01-03 11:41:19 EST
Hello,
is there any news regarding this problem?

I have third party C-dll's (hardware drivers) which use extensivly callback methods.

The vendors will never change their calling of callback methods to work with Eclipse RCP.

So there will be a limitation using RCP.

The solution "adding a Readme patch" doensn't really solve the problem.

Probably I miss the point or perhaps you have additional arguments, I don't know?