Java Forum / First Aid / October 2007
Calling a C function from Java using the JNI
csharpdotcom - 19 Oct 2007 14:42 GMT Hi all, this is my first posting here, and would most appreciate some kind help.
I need to pass a string to the Linux shell from Java, and because of the restrictions of the "getRuntime()" method, I'm trying to call up a C program from Java to which is I passed a string. The C program then executes the "system()" function containing the string that I want to pass to the Linux shell. The idea eventually is to execute the code as a bean in Glassfish, but I'm having trouble testing it out from the CLI. There seems to be some problem with packages and classpaths. The code is in a file in a folder tree, the last two levels being "/ com/corejsf", and in the programs I have the declaration "package.com.corejsf;"
There are four listings as follows:
// Listing 1 package com.corejsf;
import java.io.*; import java.util.*;
class CallUserBean { public static void main(String[] args) { UserBean bean = new UserBean(); String s = new String(); s = "Passing this string to UserBean"; System.out.println(s); bean.setName(s); } }
Which calls the class in "UserBean.java"
// Listing 2 package com.corejsf;
import java.io.*; import java.util.*;
public class UserBean { private String name; private String password;
// PROPERTY: name public String getName() { return name; } public void setName(String newValue) { System.out.println("Now in UserBean.setName()"); System.out.println(newValue); String command = new String(); command = "ls -lt > jnioutput.txt"; System.out.println("Sending the string - " + command + " - to native code"); CallSystem.system(command);
name = newValue; }
// PROPERTY: password public String getPassword() { return password; } public void setPassword(String newValue) { password = newValue; } }
At the moment for the test only the methods "getName", "setPassword" and "getPassword" are not used. This in turns calls the class in "CallSystem.java":
Listing 3 package com.corejsf;
class CallSystem { public static native void system(String s); static { System.loadLibrary("CallSystem"); } }
Which calls the native C code in "CallSystem.c" ("UserBean.h" was created with the "javah" command as required):
Listing 4 #include "UserBean.h" #include <stdio.h> #include <stdlib.h> #include <string.h>
JNIEXPORT void JNICALL Java_CallSystem_system(JNIEnv* env, jclass cl, jstring jcommand) { const char *ccommand;
ccommand = (*env)->GetStringUTFChars(env, jcommand, NULL); system(ccommand);
(*env)->ReleaseStringUTFChars(env, jcommand, ccommand); }
which passes a string to the shell for execution. In this case a simple test "ls" is executed, as you can see in listing 1.
After I compile the C code and link in the shared library, "libCallSystem.so", then compile the Java files with the "package com.corejsf;" commented out in listings 1, 2 and 3, the code works with the command:
"java -Djava.library.path=. CallUserBean"
from inside /com/corejsf and generates the test file "jnioutput.txt" and write to it. This means that the native code is being called up correctly. However, if I uncomment the package statements and recompile the Java files using the statements "javac -classpath "../../" CallSystem.java", and likewise for the other two files then issue the command:
"java -Djava.library.path=. -classpath ../../ CallUserBean"
the code in Java is executed correctly and generates the correct output, but fails when it tries to call up the native code. I get the message:
Exception in thread "main" java.lang.UnsatisfiedLinkError: com.corejsf.CallSystem.system(Ljava/lang/String;)V at com.corejsf.CallSystem.system(Native Method) at com.corejsf.UserBean.setName(UserBean.java:19) at CallUserBean.main(CallUserBean.java:11)
At best similar messages are obtained when I try other combinations of "-Djava.library.path" and "-classpath", or the program doesn't even get that far. The fact that I can get it to work with the "package" statements commented out means that it must be something to do with the way the code is linked, and would be most grateful to have some advice on this.
If this can be sorted out, the idea is to remove the code from listing one, recompile listings 2 and 3, put them in a ".war" file, and put the shared library in the appropriate folder for Glasssfish. I have in fact done this, and get the same error as above.
Christopher Sharp
Gordon Beaton - 19 Oct 2007 15:15 GMT > I need to pass a string to the Linux shell from Java, and because of > the restrictions of the "getRuntime()" method, I'm trying to call up > a C program from Java to which is I passed a string. The C program > then executes the "system()" function containing the string that I > want to pass to the Linux shell. There is absolutely no need to use JNI for this. This should work:
String[] cmd = { "/bin/sh", "-c", "ls -lt > jnioutput.txt" }; Runtime.getRuntime().exec(cmd);
I'm guessing that you tried to use redirection or quoting or other shell features without specifying a shell explicitly. But since Runtime.exec() doesn't run your command in a shell unless you specify one, those things aren't available by default.
If that's "just an example" then post the real original string you're having trouble with, and the Runtime.exec() code you tried to use. I can't really tell since your setName() method looks a little nonsensical: I can't see how "ls -l" relates to the command in question.
If all you need to do is get a file list into "jnioutput.txt" then you really don't even need Runtime.exec, since you can get the same information with File.listFiles() (etc) and then write to the file with a PrintWriter.
Finally, your JNI exception is due to the fact that you failed to specify the fully qualified classname (i.e. *with* package name) when you ran javah to generate UserBean.h, and so your native code doesn't match what's expected by the JVM.
/gordon
--
csharpdotcom - 19 Oct 2007 15:46 GMT Hi Gordon,
Many thanks for your reply.
> There is absolutely no need to use JNI for this. This should work: > > String[] cmd = { "/bin/sh", "-c", "ls -lt > jnioutput.txt" }; > Runtime.getRuntime().exec(cmd); In fact that is exactly what I did in Glassfish, but it will only work with simple commands, and appears to fail when I include a "cd" in the string. I'm just using this particular string to test things out.
> I'm guessing that you tried to use redirection or quoting or other > shell features without specifying a shell explicitly. But since [quoted text clipped - 6 lines] > nonsensical: I can't see how "ls -l" relates to the command in > question. The original string gave the full path to a particular folder and included the "cd" command, which caused the server to fail.
> If all you need to do is get a file list into "jnioutput.txt" then you > really don't even need Runtime.exec, since you can get the same > information with File.listFiles() (etc) and then write to the file > with a PrintWriter. That is not all I want to do, I'm just trying a simple "Hello World" type example first to get the wringles out of the linking problems.
> Finally, your JNI exception is due to the fact that you failed to > specify the fully qualified classname (i.e. *with* package name) when > you ran javah to generate UserBean.h, and so your native code doesn't > match what's expected by the JVM. > > /gordon I've just tried "javah -classpath ../../ UserBean" , but it doesn't find the class. Obviously there is something simple that I'm missing.
Christopher Sharp
csharpdotcom - 19 Oct 2007 16:34 GMT OK, I've just got that part to work, many thanks, I typed:
javah -classpath ../../ -o UserBean.h com.corejsf.UserBean
The "-classpath ../../" part because I have to go up in the tree as I am in the folder with the code, and the output filename, otherwise I get "com_corejsf_UserBean.h". I then compiled the C code with:
gcc -D_REENTRANT -fPIC -I/<path-to-jdk>/include -I/<path-to-jdk>/ include/linux -c CallSystem.c
which generated the object file, then typed:
gcc -shared CallSystem.o -o libCallSystem.so
to create the shared library. However, on attempting to execute:
java -Djava.library.path=. -classpath "../.." CallUserBean
I get the same link errors as before, so something is still wrong.
Christopher
Gordon Beaton - 19 Oct 2007 16:40 GMT > I get the same link errors as before, so something is still wrong. After running javah with the correct classname, did you modify the C source so the native method matches the declaration in the new header file?
/gordon
--
csharpdotcom - 19 Oct 2007 16:48 GMT > After running javah with the correct classname, did you modify the C > source so the native method matches the declaration in the new header [quoted text clipped - 3 lines] > > -- In fact I sent a post where I had created the new header file with the same name as before, and the C code compiled and linked OK, and I created the shared library, but I'm still having trouble running the Java. For some reason the post hasn't appeared yet.
If I don't get it solved in the next few minutes, it will have to wait until tomorrow morning, as I'm going out for the evening.
Christopher
Gordon Beaton - 19 Oct 2007 17:06 GMT > In fact I sent a post where I had created the new header file with > the same name as before, and the C code compiled and linked OK, and > I created the shared library, but I'm still having trouble running > the Java. For some reason the post hasn't appeared yet. Whatever - I don't think you're reading what I've written.
It doesn't matter how many times you re-run javah and recompile your code. The important thing is that the name of the function itself in the C source is correct with respect to the fully qualified name of the class, i.e. with the package declaration.
The generated header file shows you what the correct name is, but only if you run javah correctly. Make sure you don't have old copies of the header file or the shared library that might disturb your observations.
For about the fifth time though, you can probably solve this with Runtime.exec().
/gordon
--
Gordon Beaton - 19 Oct 2007 16:38 GMT > In fact that is exactly what I did in Glassfish, but it will only > work with simple commands, and appears to fail when I include a "cd" > in the string. I'm just using this particular string to test things > out. Again, please post a real example of a real command string, so I don't need to make incorrect assumptions.
You can't use cd by itself (well you can, but its effect is lost when the process ends, and it certainly won't affect the Java application). The same is true of system().
You can do this though:
String[] cmd = { "/bin/sh", "-c", "cd /tmp; ls -lt > jnioutput.txt" }; Runtime.getRuntime().exec(cmd);
In this example, "ls -lt > jnioutput" will run in the /tmp directory. Note that semicolon is necessary to separate the different commands.
> I've just tried "javah -classpath ../../ UserBean" , but it doesn't > find the class. Obviously there is something simple that I'm > missing. The name of the class is something like com.foo.UserBean, where the first part comes from the package declaration in the java source, so your command should be: "javah com.foo.UserBean". If you need to specify a classpath, you can do that too, but "UserBean" simply isn't the full name of the class.
I'm still convinced that Runtume.exec() is sufficient, but you need to post a real example or I can't be more specific.
/gordon
--
Lothar Kimmeringer - 19 Oct 2007 17:29 GMT > String[] cmd = { "/bin/sh", "-c", "cd /tmp; ls -lt > jnioutput.txt" }; Better "ls /tmp && ls -lt > jnioutput.txt" to make sure that the second part is not executed if /tmp doesn't exist or is restricted in access.
To make sure that the process doesn't block STDERR must be read or a 2> /dev/null must be added to the ls.
If there are many commands to be executed I think a shell-script should be considered, that is executed by Java the same way as already shown. I really don't see the need for a JNI-Wrapper, either.
Regards, Lothar
 Signature Lothar Kimmeringer E-Mail: spamfang@kimmeringer.de PGP-encrypted mails preferred (Key-ID: 0x8BC3CD81)
Always remember: The answer is forty-two, there can only be wrong questions!
Roedy Green - 26 Oct 2007 22:29 GMT >In fact that is exactly what I did in Glassfish, but it will only work >with simple commands, and appears to fail when I include a "cd" in the >string. I'm just using this particular string to test things out. that's because CD is not an executable but a command to the command interpreter. See http://mindprod.com/jgloss/exec.html for how to spawn a command interpreter to handle these.
 Signature Roedy Green Canadian Mind Products The Java Glossary http://mindprod.com
csharpdotcom - 29 Oct 2007 22:13 GMT On 26 Oct, 22:29, Roedy Green <see_webs...@mindprod.com.invalid> wrote:
> >In fact that is exactly what I did in Glassfish, but it will only work > >with simple commands, and appears to fail when I include a "cd" in the [quoted text clipped - 6 lines] > Roedy Green Canadian Mind Products > The Java Glossaryhttp://mindprod.com Many thanks - in fact I had found a solution to this. I used the newer ProcessBuilder class, and the directory and start methods to invoke a bash script in a specified directory, then passed some arguments as a test. Once a shell script is invoked, it can do anything.
Obviously in a real world application precautions have to be taken to make sure that the system stays secure.
Christopher Sharp
Roedy Green - 29 Oct 2007 22:23 GMT > I used the >newer >ProcessBuilder class, and the directory and start methods to invoke a >bash script in a specified directory, then passed some arguments as a >test. Once a shell script is invoked, it can do anything. The problem is this becomes platform specific. The link I gave you also covers ProcessBuilder.
http://mindprod.com/jgloss/exec.html
 Signature Roedy Green Canadian Mind Products The Java Glossary http://mindprod.com
Roedy Green - 20 Oct 2007 06:29 GMT >getRuntime()" What you want to do sounds quite doable with an ordinary exec, which is much simpler than using JNI. See http://mindprod.com/jgloss/exec.html
It has many variations. Don't give up on it too quickly.
 Signature Roedy Green Canadian Mind Products The Java Glossary http://mindprod.com
csharpdotcom - 20 Oct 2007 16:56 GMT Hi Gordon and Roedy,
I've just logged on since yesterday and see some further very valuable comments - many thanks.
Well I just got the JNI to work at last! On my way back from party last night I had the idea of changing the name of the function in the C program, and only just had the chance to try it out, and it worked! What I did was first to create the ".h" file with the default name generated by javah, rather than giving it the name I want it to have using the command:
javah -classpath ../../ com.corejsf.UserBean
which creates the filename "com_corejsf_UserBean.h", then in "CallSystem.c" I put in that filename in the "include" statement, but then I changed the name of the function in the C program called from Java from "Java_CallSystem_system" to "Java_com_corejsf_CallSystem_system". That is the critical change which caused the program to work.
Nowhere had I seen any documentation on this detail, either in one of my books or online. I'm now going to try this out with Glassfish, as well as looking again at the "getRuntime()" method, links to which you kindly sent me.
Christopher
Gordon Beaton - 20 Oct 2007 17:54 GMT > Nowhere had I seen any documentation on this detail, either in one of > my books or online. The javah documentation is pretty clear on this point:
SYNOPSIS javah [options] fully-qualified-classname
DESCRIPTION [...] If the class passed to javah is inside a package, the package name is prepended to both the header file name and the structure name.
It's also in my *first* response to you in this thread.
/gordon
--
Lew - 20 Oct 2007 17:58 GMT >> Nowhere had I seen any documentation on this detail, either in one of >> my books or online. [quoted text clipped - 10 lines] > > It's also in my *first* response to you in this thread. Yeah, when I read the OP's
> On my way back from party > last night I had the idea of changing the name of the function in the > C program, I thought to myself, "Self, surely we remember that advice from Gordon's *first* response in this thread?"
to the OP: it seems a bit strange to claim credit for having just, on 20 October, getting the idea that was given to you on 19 October.
 Signature Lew
csharpdotcom - 20 Oct 2007 19:13 GMT Yes, OK, many thanks, somehow I missed the earlier comments - sorry about that, but I was getting tired.
OK, I got the JNI to work with Glassfish, which is great. Given http://mindprod.com/jgloss/exec.htm, in particular the "gotcha" that commands like "dir" can't be executed directly I'm going through the advice here on how to execute commands like "cd /home/csharp/tempdir; ls -l > tempout.txt" etc. It appears to work from Java and C when run in the CLI, which is reasonable, but not from Glassfish, so as advised a shell should be invoked given the comments above.
And yes, it's probably better to use runtime().exec(), but the server I will eventually be working on has a lot of code in C and FORTRAN, so I may still need to use JNI at some stage, and in any case it's useful to know how to do this. For the time being I'm just testing out code on my laptop.
Christopher
Roedy Green - 22 Oct 2007 06:06 GMT >Nowhere had I seen any documentation on this detail, either in one of >my books or online. I had the same problem years ago -- detailed docs, but nothing on the overall process and in general what was happening. It did not make sense until I got a textbook. See http://mindprod.com/jgloss/jni.html
 Signature Roedy Green Canadian Mind Products The Java Glossary http://mindprod.com
csharpdotcom - 22 Oct 2007 20:58 GMT Hi Roedy,
OK, I also got the code to work using the ProcessBuilder class, which is new in Java 5, using your very helpful and extensive website (plus lots of other interesting things outside of programming).
I modified my bean so that it is now:
package com.corejsf;
import java.io.*; import java.util.*;
public class UserBean { private String name; private String password;
// PROPERTY: name public String getName() { return name; } public void setName(String newValue) { System.out.println("Now in UserBean.setName()"); System.out.println(newValue); try { ProcessBuilder pb = new ProcessBuilder("/home/csharp/ shelltests/called-from-java", "first_argument", "second_argument", "third_argument"); pb.directory(new File("/home/csharp")); pb.start(); } catch (Exception e) { System.out.println("Error executing script"); } name = newValue; }
// PROPERTY: password public String getPassword() { return password; } public void setPassword(String newValue) { password = newValue; } }
with the JNI removed. It calls up the script in the path given above, and the contents of "called-from-java" are:
#!/bin/sh ls -lt > scriptout echo $0 >> scriptout echo $1 >> scriptout echo $2 >> scriptout echo $3 >> scriptout
which lists the files in the directory specified in the "directory()" method. If I don't do that, Glassfish writes to a file with root privileges in its own directory, which is highly undesirable. One thing is that the statement
pb.directory("/home/csharp");
does not compile and generates a syntax error, even though you have something like that on your website. The "new FILE(....)" has to be nested in the method call for the code to compile, at least on my computer, which has Java 6.
Anyway, although the JNI isn't required here, as was mentioned earlier, at least I now know how to use it.
Christopher
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|