Chapter:C extra JNI

From Juneday education
Jump to: navigation, search

Introduction

This chapter shows you a small example on how to use C code from Java using JNI (Java Native Interface) and vice versa. This page serves as a practical introduction to JNI (with C and Java) and is not intended as a reference or a complete tutorial. With that disclaimer said I think we're ready to go.

What is JNI

Your java programs run inside a virtual machine and not directly on the OS (and hardware). A program written in C (when built) on the other hand runs on the OS. Let's briefly look at how a Java program and a program written in C interacts with your computer:

 +--------------+
 | Java program |
 +--------------+
      |
      \/
 +---------+  +-----------+
 | Java VM |  | C program |
 +---------+  +-----------+
      |         |         
      \/        \/
  +-----------------+
  |      OS         |
  +-----------------+
      |
      \/
  +--------------+
  |      HW      |
  +--------------+

How do we use software written in one language from another language? When it comes to Java and C (or C++) we can use JNI.

What is JNI - Using C from Java

There are tons of nice libraries (software modules) written in languages other than Java. Wouldn't it be great if you could use these libraries? And of course you can. Let's say you want to use a library to decode a video file in mkv (Matroska) format and you would like to use a library written in C. Enter JNI - with JNI your Java program can use this library. We will try to give an overview of how this works with the following picture:

 +------------------+
 | Java MKV program |
 +------------------+
      |        |
      \/       \/
 +---------+--------+  
 | Java VM |  JNI   |
 +---------+--------+
     |         | 
     |         \/
     |      +--------------+
     |      | mkv library  |
     |      +--------------+
     |         |         
     \/        \/
  +-----------------+
  |      OS         |
  +-----------------+
      |
      |
      \/
  +--------------+
  |      HW      |
  +--------------+

What is JNI - Using Java from C

Can you do the opposite? That is can you use Java code from a C program (a program written in C)? Wes we can. Let' say you want to use some Java code to parse JSON (you can of course do this using a C library, but let's do it the cumbersome way) than the following overview should give you a rough idea.

   +------------+
   | C program  |
   +------------+
     |       |
     |       \/
     |    +---------+--------+  
     |    | Java VM |  JNI   |
     |    +---------+--------+
     |         |       | 
     |         |       \/
     |         |    +--------------+
     |         |    | JSON classes |
     |         |    +--------------+
     |         | 
     \/        \/
  +-----------------+
  |      OS         |
  +-----------------+
      |
      |
      \/
  +--------------+
  |      HW      |
  +--------------+

Setting up your environment

Install required programs

To install the required programs, do the following:

$ curl -LJO https://raw.githubusercontent.com/progund/utils/master/bin/jd-jni.sh
$ chmod a+x jd-jni.sh
$ ./jd-jni.sh

Source code used in this page

Source code used in this page can be found over at github:

Building and executing the examples

Automatic building and execution

We have a script, build-and-run.sh you can use to build and execute the JNI examples. Here's an example run of the script:

$ ./build-and-run.sh 

--=== Using C code from Java ===---
Compiling java code: OK
Creating C header files from Java source code: OK
Compiling C code: OK
Executing Java program (using C lib): In Java, calling C function with "Krega"
[c/lovin_c.c:16]  ----- Entering C code ------
[c/lovin_c.c:21] C code got this from Java: 
[c/lovin_c.c:22] Krega
[c/lovin_c.c:46] The following string will be returned back to Java:
[c/lovin_c.c:47] From C code with love, we got "Krega" from Java.
[c/lovin_c.c:52]  ----- Leaving C code ------
In Java again: From C code with love, we got "Krega" from Java.
OK


--=== Using Java code from C ===---
Compiling Java source code: OK
Compiling C source code: OK
Executing C program (using Java code): jvm status:          ok
find class:          ok
find gimmeSum:       ok
 ** Result of int method: 25
find gimmeString:    ok
 ** Result of String method: Hi there, I am a stupid class!
OK


--=== Wrapping it up ===---
Using C code from Java worked fine
Using Java code from C worked fine

Manual building and execution

If you want to dig a bit deeper into JNI we suggest you play around with the compilation and execution yourself (there is help for you in each section). It is rather tough for us to write generic instructions for each supported operating system and every JDK version you may have install in whatever location so we have decided to go for a solution where we try our best to find (and use) a JDK on your computer. We do this using bash and some knowledge about usual places where your JDK might been installed. In order for this to work it is important that you follow the instructions on how to Set up environment variables.

Set up environment variables

The following environment varables are required if you want to compile and execute yourself (rather than using the script build_and_run.sh) in the sections below:

  • CC - c compiler to use
  • JNI_C_FLAGS - used when compiling c code
  • JNI_LD_FLAGS - used when linking c programs
  • JDK_LIB_DIR - used to point out location of dynamically linked jvm library

Find header files and libs

Copy and paste the following lines in your terminal running bash. Note that there are three different sections, one per each supported platform.

Cygwin

CC=x86_64-w64-mingw32-gcc
JDKS_DIR=$(find /cygdrive/c/Program\ Files/Java/jdk* -prune -type d) 
JDK_INC_DIR=$(dirname "$(find "$JDKS_DIR" -name "jni.h" | tail -1)")
JDK_C_FLAGS="-I\"$JDK_INC_DIR\" -I\"$JDK_INC_DIR/win32\" -D__int64=\"long long\""
JDK_LD_FLAGS=" -shared   -Wl,--add-stdcall-alias -o c/love.dll"
JDK_LIB_DIR1="$(find "$JDK_INC_DIR"/../ -name 'jvm.dll')"
JDK_LIB_DIR="$(dirname "$JDK_LIB_DIR1")"

MacOS

CC=gcc
JDKS_DIR=$(find /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacO* -prune -type d)
JDK_INC_DIR=$(dirname $(find $JDKS_DIR -name "jni.h" | tail -1));
JDK_C_FLAGS=" -I\"$JDK_INC_DIR\"  -fPIC "
JDK_LD_FLAGS=" -dynamiclib -o c/liblove.jnilib"
JDK_LIB_DIR=$(dirname $(find $(/usr/libexec/java_home) -name "libjvm.dylib"))

GNU/Linux

CC=gcc
JDKS_DIR=$(find /usr/lib/jvm/* -prune -type d); 
JDK_INC_DIR=$(dirname $(find $JDKS_DIR -name "jni.h" | tail -1)); 
JDK_C_FLAGS="-I\"$JDK_INC_DIR\" -I\"$JDK_INC_DIR/linux\"  "
JDK_LD_FLAGS=" -shared -fPIC -o c/liblove.so"
JDK_LIB_DIR=$(dirname $(find $JDK_INC_DIR/../ -name "libjvm.so"))

Listing and manually choosing JDKs

We're trying our best to find a jdk and setup the path to your JNI headers files. Don't be too hard on us if we fail. We're using bash and some tools to find the location of (one) JDK. If you want to use another JDK than the onw we're finding you need to figure out how to do this your self, but to give you a hint on what JDKs we're finding try this:

$ echo $JDKS_DIR

If you want to know which JDK (if you have many installed) we're using, try this:

$ find $JDKS_DIR -name "jni.h" | tail -1

The above uses the last of the listed JDKs. If you want to use the second in the list (echo $JDKS_DIR) you can use head and tail:

$ find $JDKS_DIR -name "jni.h" | head -2 | tail -1

with this information I think you have enough information to adapt the settings to use the JDK of your choice.

Putting JNI into practice

In the examples below we show you how to compile, link and more using openjdk (not Oracle's java) and gcc on a GNU/Linux based computer running bash. Since you might have a different setup we have provided an example (jni) where we 've tried hard creating a so called Makefile that should assist you compiling etc. Check out the README how to get the example working on your computer.

Using C from Java

We willl write code that:

  • java code invoking a method (defined in c code)
  • c code that returns a new string (including the one passed to it) to java
  • java code to print the string returned from c

This involves a couple of techniques/tools:

Here's a brief overview of the files involved:

 +------------------+
 |   JNIDemo.java   |
 +------------------+
      |        |
      |        |
      |      +--------------------+
      |      | FromCWithLove.java |
      |      +--------------------+
      |        |
      |        |
      |        |
      \/       \/
 +---------+--------+  
 | Java VM |  JNI   |
 +---------+--------+
     |         | 
     |         \/
     |      +--------------+
     |      |  lovin_c.c   |
     |      +--------------+
     |         |         
     \/        \/
  +-----------------+
  |      OS         |
  +-----------------+
      |
      |
      \/
  +--------------+
  |      HW      |
  +--------------+

The overview diagram above is not accurate but should give you a rough idea of how things are connected.


Java code

Let's begin with the Java part. We have two files JNIDemo.java and FromCWithLove.java. Let's begin looking at the first file.

JNIDemo.java

 1 package se.juneday;
 2 
 3 public class JNIDemo {
 4 
 5   public static void main(String[] args){
 6 
 7     FromCWithLove c = new FromCWithLove();
 8 
 9     String msg = c.getSome("Krega");
10 
11     System.out.println("In Java again: " + msg);    
12   }  
13 }

Explanations to the important parts of the soruce code:

  1. Create a FromCWithove object
  2. Store the string returned from C code, after having passed "Krega". to the C code
  3. Print the string out

Source code of the file cand be found over at GitHub: JNIDemo.java

FromCWithLove.java

 1 package se.juneday;
 2 
 3 public class FromCWithLove {
 4 
 5   static {
 6     System.loadLibrary("love");
 7   }
 8   
 9   public native String getSome(String msg);
10   
11 }

Explanations to the important parts of the soruce code:

  1. declares a static initializer
  2. load native library love (on UNIX based systems liblove.so)
  3. end of static initializer
  4. declares a native (i e not defined in Java) method

Source code of the file cand be found over at GitHub: FromCWithLove.java

Build and run

To build the Java files above you type:

$ javac se/juneday/FromCWithLove.java
$ javac se/juneday/JNIDemo.java

and to run the Java program (JNIDemo) you type:

$ java  se.juneday.JNIDemo
Exception in thread "main" java.lang.UnsatisfiedLinkError: no love in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)
	at se.juneday.FromCWithLove.<clinit>(FromCWithLove.java:6)
	at se.juneday.JNIDemo.main(JNIDemo.java:7)

Uh oh. What's wrong? It's not that strange if we think about it. We used the statuc initializer to load the library love. Java can't find that library and throws an Exception. What if we skip the loading of the library, typically like this:

1   static {
2   //  System.loadLibrary("love");
3   }
4 }

Let's compile and run again:

1 $ javac se/juneday/FromCWithLove.java
2 $ javac se/juneday/JNIDemo.java
3 $ java  se.juneday.JNIDemo
4 Exception in thread "main" java.lang.UnsatisfiedLinkError: se.juneday.FromCWithLove.getSome(Ljava/lang/String;)Ljava/lang/String;
5 	at se.juneday.FromCWithLove.getSome(Native Method)
6 	at se.juneday.JNIDemo.main(JNIDemo.java:9)

Uh oh one more time. What's wrong now? Java can't link (think of it as Java can't find) the method getSome. This is not so strange since we have not written any code for this method. We've only said that there is a method. So let's bring back the code that loads the library again:

1   static {
2     System.loadLibrary("love");
3   }
4 }

and write some C code.

C code

Ok, so in Java we used the keyword native to say that we wanted the implementation of a method to be defined outside of Java. How should the coresponding C code look like? Fact is, there are a lot of details to deal with here. But one Java tool is here to help us. Enter javah. With javah we get a header file created for us. This header files contains the prototypeof the function we should implement in a separate C file. Let's to all these things, one by one.

Generate header file

Just as we start the java by passing the name of the classs (with a a main method) we want java to start javah wants a class name.

$ javah se.juneday.FromCWithLove

This genrates a header files. But to keep a good order we want the header file to be placed in a directory for all our c files. Let's call this directory c/. Then we invoke javah like this:

$ javah -d c/ se.juneday.FromCWithLove

What was created? Can we see it? Of course we can.

$ ls -al c/se_juneday_FromCWithLove.h 
-rw-rw-r--. 1 hesa hesa 498  4 dec 20.07 c/se_juneday_FromCWithLove.h

Note that the file se_juneday_FromCWithLove.h is named using the complete class name se_juneday_FromCWithLove followed by .h. Let's look at the content:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class se_juneday_FromCWithLove */

#ifndef _Included_se_juneday_FromCWithLove
#define _Included_se_juneday_FromCWithLove
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     se_juneday_FromCWithLove
 * Method:    getSome
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_se_juneday_FromCWithLove_getSome
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

We should focus on the following prototype:

JNIEXPORT jstring JNICALL Java_se_juneday_FromCWithLove_getSome
  (JNIEnv *, jobject, jstring);

This is the function we should implement in C. So let's proceed with this.

Implementing the C code

Let's not do anything too fancy now. Let's just write a simple C function that can not (famous last words) crash. we will write the implementation in three steps, since there are three major things we should do:

  • make sure we can invoke the function
  • make sure we get data (string) from Java
  • make sure we can pass data back to Java
Dummy implementation

So open up an editor and write a C files like this:

#include "se_juneday_FromCWithLove.h"
#include <stdio.h>
#include <string.h>

#define LOG(a) fprintf(stderr, "[%s:%d] %s\n", __FILE__, __LINE__, a);

JNIEXPORT jstring JNICALL Java_se_juneday_FromCWithLove_getSome
  (JNIEnv *env, jobject obj, jstring msg)
{
  LOG("C code running");
  return NULL;    
}

We call this file lovin_c.c since we simply adore and love programming in C and we thought about the band Lovin' spoonful while writing ths page.

Let's compile and build all this. It is no important that you have the environment variabels (see earlier section) set:

$ javac se/juneday/FromCWithLove.java
$ javac se/juneday/JNIDemo.java
$ javah -d c/ se.juneday.FromCWithLove
$ eval $CC -c $JDK_C_FLAGS c/lovin_c.c -o c/lovin_c.o

In case you would like to see the C compilation command line, type the following:

$ echo $CC  $JDK_C_FLAGS $JDK_LD_FLAGS c/lovin_c.c 
gcc -I"/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-11.fc28.x86_64/include" -I"/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-11.fc28.x86_64/include/linux" -shared -fPIC -o c/liblove.so c/lovin_c.c

The printout above is from the authors' computer and will most likely be different on your computer.

Gluing Java and C

First of all we need to create a shared library from the C file:

$ eval $CC  $JDK_C_FLAGS $JDK_LD_FLAGS c/lovin_c.c

This compiles the C files c/lovin.c and creates a shared library. This shared library can hopefully be used Java now.

Examine library

The shared library is named differently depending on the platform. For completeness we list the names and what the command file tells us about the file: Cygwin/Windows

$ file c/love.dll 
c/love.dll: PE32+ executable (DLL) (console) x86-64, for MS Windows

MacOS

$ file c/liblove.jnilib 
c/liblove.jnilib: Mach-O 64-bit dynamically linked shared library x86_64

GNU/Linux

$ file c/liblove.so 
c/liblove.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=640cbd3c5c51b015635b0849d63275da55bea00d, not stripped

For more details about the above, we ask you to read [Chapter:C_libraries].

Let's now try to invoke the Java class:

$ java se.juneday.JNIDemo
Exception in thread "main" java.lang.UnsatisfiedLinkError: no love in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)
	at se.juneday.FromCWithLove.<clinit>(FromCWithLove.java:6)
	at se.juneday.JNIDemo.main(JNIDemo.java:7)

Shit!! It did not work. Java telss us No love in java.library.path. So there's no love in the air at the moment. Java can't find the library. Let's give Java some places to look for libraries in. We do this by using java.library.path:

$ java -Djava.library.path=c se.juneday.JNIDemo
[c/lovin_c.c:10] C code running
In Java again: null

YES!!!! We can see that our code has been executed ([c/lovin_c.c:10] C code running) and that the Java code is executing after the C code (In Java again: null ).

Let's proceed by writing C code to use the string passed to c from Java.

Passing data from Java

The C function receives data (a string) from Java. If we look at the prototype of the function

JNIEXPORT jstring JNICALL Java_se_juneday_FromCWithLove_getSome
  (JNIEnv *env, jobject obj, jstring msg)

it probably comes as no surprise that the string can be found in the last argument, msg. Let's start with just printing (in C code) the string from Java.

JNIEXPORT jstring JNICALL Java_se_juneday_FromCWithLove_getSome
  (JNIEnv *env, jobject obj, jstring msg)
{
  const char *from_java =
    (*env)->GetStringUTFChars(env, msg, NULL);
  if (from_java==NULL)
    {
      return NULL;
    }
  LOG(from_java);
  return NULL;
}

The first two highlighted lines of code stores a pointer to a byte array representing msg encoed in UTF8. The pointer is stored in from_java. This is done using the JNI function GetStringUTFChars: The manual says: Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding. This array is valid until it is released by ReleaseStringUTFChars().

The third highlighted line simply outputs the string to stdout.

Let's compile and build all this:

$ javac se/juneday/FromCWithLove.java
$ javac se/juneday/JNIDemo.java
$ javah -d c/ se.juneday.FromCWithLove
$ eval $CC  $JDK_C_FLAGS $JDK_LD_FLAGS c/lovin_c.c
$ java -Djava.library.path=c se.juneday.JNIDemo
[c/lovin_c.c:16] Krega
In Java again: null

Passing data to Java

We've now added some more code to copy some characters back to Java. Let's look at the code and discuss it.

JNIEXPORT jstring JNICALL Java_se_juneday_FromCWithLove_getSome
  (JNIEnv *env, jobject obj, jstring msg)
{
#define BUF_SIZE 100
  char buf[BUF_SIZE];
  char *res;
  int len;
  jstring result;

  const char *from_java =
    (*env)->GetStringUTFChars(env, msg, NULL);
  if (from_java==NULL)
    {
      return NULL;
    }
  LOG(from_java);

  res=strcpy(buf, "I can C for miles: ");
  LOG(buf);

  res=strncat(buf, from_java, BUF_SIZE-strlen(buf)-1);
  if (res==NULL)
    {
      return NULL;
    }
  LOG(buf);

  (*env)->ReleaseStringUTFChars(env, msg, from_java);

  result = (*env)->NewStringUTF(env, buf);

  return result;
}

Most of the code in the function is building up an array to return to Java. This is done i C which is not the subject here so let's look at the JNI relating code, which is the highlighted code.

ReleaseStringUTFChars tells the JVM that our C code (native code) is not in need of <msg> anymore. Manual ([https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html JNI Functions) says: Informs the VM that the native code no longer needs access to utf. The utf argument is a pointer derived from string using GetStringUTFChars().

NewStringUTF creates a JavaString (java.lang.String) from the array (native code). ([https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html JNI Functions) says: Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.

Ok, we're done. Let's try it all...

Note: if you would like to see an example using a dynamically allocated memory for the string, check out: lovin_c.c

Compiling and running it all

$ javac se/juneday/FromCWithLove.java
$ javac se/juneday/JNIDemo.java
$ javah -d c/ se.juneday.FromCWithLove
$ eval $CC  $JDK_C_FLAGS $JDK_LD_FLAGS c/lovin_c.c
$ java -Djava.library.path=c se.juneday.JNIDemo
[c/lovin_c.c:22] Krega
[c/lovin_c.c:25] I can C for miles: 
[c/lovin_c.c:32] I can C for miles: Krega
In Java again: I can C for miles: Krega

Using Java from C

Here's a brief overview of the files involved:

   +------------+
   |c-program.c |
   +------------+
     |       |
     |       \/
     |    +---------+--------+  
     |    | Java VM |  JNI   |
     |    +---------+--------+
     |         |       | 
     |         |       \/
     |         |    +--------------+
     |         |    | Stupid.java  |
     |         |    +--------------+
     |         | 
     \/        \/
  +-----------------+
  |      OS         |
  +-----------------+
      |
      |
      \/
  +--------------+
  |      HW      |
  +--------------+

The overview diagram above is not accurate but should give you a rough idea of how things are connected.


Java code

Let's begin with the Java part. We have one file se/juneday/Stupid.java

package se.juneday;

public class Stupid {

  public static String gimmeString() {
    return "Hi there, I am a stupid class!";
  }

  public static int gimmeSum(int a, int b) {
    return a+b;
  }

  public static void main(String[] args) {
    System.out.println(gimmeSum(1234,5678));
    System.out.println(gimmeString());
  }
  
}

This class has two static methods:

  • gimmeString() - always returns the string Hi there, I am a stupid class!"
  • gimmeSum(int a, int b) - returns the sum of the two int parameters
  • main(String[] args) - a main method used to verify things work

These are the methods we will call from C code (which we will write soon).

Build and run

To build the Java file above you type:

$ javac javac se/juneday/Stupid.java

and to run the Java program (Stupid) you type:

$ java se.juneday.Stupid 
6912
Hi there, I am a stupid class!

Ok, so the Java code seems to work. Let's continue with writing C code invoking the methods in the Java class.

C code

This is a bit cumbersome, but we'll try to give a short introduction to a small example. We will assume you're familiar with pointers (otherwise check out: C pointers).

When using Java from C you need to create a Java VM in your C code so Here's a function to create and setup a JVM:

static long
setup_jvm(JavaVMOption *options, JavaVM **jvm, JNIEnv **env)
{
  JavaVMInitArgs vm_args;
  long vm_status;

  options[0].optionString = (char*) "-Djava.class.path=." ;
  memset(&vm_args, 0, sizeof(vm_args));
  vm_args.version = JNI_VERSION_1_2;
  vm_args.nOptions = 1;
  vm_args.options = options;
  vm_status = JNI_CreateJavaVM(jvm, (void**)env, &vm_args);

  return vm_status;
}

This function sets up the CLASSPATH while creating the JVM using the function JNI_CreateJavaVM.

Note: we use static to make the function not usable outside this file. We do this since the function is kind of specific to our program. Some will complain and say this is unnecessary since this file contains a main function and as such is more or less useless .... anyhow, we use static since the function is used in this file alone.

We call this function from the main function like this:

int main()
{
  JavaVMOption options[1];
  JNIEnv *env;
  JavaVM *jvm;

  jclass class;
  jmethodID method;
  jstring message;
  jint sum;
  long vm_status;

  /**                                                                                                                     
   *                                                                                                                      
   * Check jvm                                                                                                            
   *                                                                                                                      
   */
  CHECK_START("jvm status:");
  vm_status = setup_jvm(options, &jvm, &env);
  if (vm_status == JNI_ERR)
    {
      fprintf(stderr, " fail\n");
      exit (1);
    }
  fprintf(stderr, " ok\n");

We cowardly bail out (actually, there's no other thing to do) if we can't create and set up a JVM. If we reach the printout (to stderr) " ok\n" we have a JVM. We now have an environment and a jvm.

Note: the macro CHECK_START is used to make debug printouts look the same and still be easily changed.

Ok, we have a JVM. What's next (<a href="https://en.wikipedia.org/wiki/Powerage">to the moon</a>)? Next thing to do is to locate the class. We do this using the method FindClass:

  /**                                                                                                                     
   *                                                                                                                      
   * Find class                                                                                                           
   *                                                                                                                      
   */
  CHECK_START("find class: ");
  class = (*env)->FindClass(env, "se/juneday/Stupid");
  if(class ==0)
    {
      fprintf(stderr, " fail\n");
      exit (2);
    }
  fprintf(stderr, " ok\n");

We invoke the function with the environment (env) with the class name (</code>se/juneday/Stupid</code>) and get a reference to a class back (as a jclass). If we don't get a reference to the class we bail out.

Note: it may look like a type but you should write the class name using / (e g se/juneday/Stupid)instead of using . (e g se.juneday.Stupid.

So, we have a reference to the class. What do we need to make a call to a method? Well, we need the method. So let's find the method gimmeSum using the function GetStaticMethodID which can be found using the environment (env). To make things more fun we will also invoke the method (if found) using CallStaticIntMethod:

  /**                                                                                                                     
   *                                                                                                                      
   * Find and use gimmeSum                                                                                                
   *                                                                                                                      
   */
  CHECK_START("find gimmeSum: ");
  method = (*env)->GetStaticMethodID(env, class, "gimmeSum", "(II)I");
  if(method==0)
    {
      fprintf(stderr, " fail\n");
      exit (3);
    }
  fprintf(stderr, " ok\n");

  sum = (*env)->CallStaticIntMethod(env, class, method, 12, 13);
  printf(" ** Result of int method: %d\n", sum);

We try to find the method using the environment and by passing the environment, the class reference, the name og the method in the Java class (i e gimmesum) and the parameter types (int and int becomes II and return type (I). If we can't find the method (you can try this by misspelling the method name) we bail out. If we found the method, we invoke the method using CallStaticIntMethod. There are numerous functions in the JNI API for C. For now we will tell you, and you will probably think it is ok since the name of the function kind of relates to what we want to do, to use the method CallStaticIntMethod.

Let's invoke the method (in the Java class) called gimmeString which is slightly more complicated since it returns String. An int in Java is a primitive type as in C so that was probably ok to understand. How about the class java.lang.String in Java. It is not primitive. So we need to use the JNI notation for a Java String (Ljava/lang/String). This notation is used to find the method - and the method will be invoked if found.

  /**                                                                                                                     
   *                                                                                                                      
   * Find and use gimmeString                                                                                             
   *                                                                                                                      
   */
  CHECK_START("find gimmeString: ");
  method = (*env)->GetStaticMethodID(env, class, "gimmeString", "()Ljava/lang/String;");
  if(method==0)
    {
      fprintf(stderr, " fail\n");
      exit (4);
    }
  fprintf(stderr, " ok\n");
  message = (jstring) (*env)->CallStaticObjectMethod(env, class, method);
  const char *from_java =
    (*env)->GetStringUTFChars(env, message, NULL);
  printf(" ** Result of String method: %s\n", from_java);

Fidning it is similar to finding the previous method, apart from the Ljava/lang/String. Since this method takes no parameters we simply skip passing such. The returned String from the Java code need to be converted into a C string (char*) using the function GetStringUTFChars. After this conversion is done we print the string from Java to stdout.

Ok, we're done aren't we? Well, almost. We need to close the JVM using the function DestroyJavaVM like this:

  (*jvm)->DestroyJavaVM(jvm);

Let's look at the entire code:

#include <jni.h>
#include <string.h>
#include <stdlib.h>


static long
setup_jvm(JavaVMOption *options, JavaVM **jvm, JNIEnv **env)
{
  JavaVMInitArgs vm_args;
  long vm_status;

  options[0].optionString = (char*) "-Djava.class.path=." ;
  memset(&vm_args, 0, sizeof(vm_args));
  vm_args.version = JNI_VERSION_1_2;
  vm_args.nOptions = 1;
  vm_args.options = options;
  vm_status = JNI_CreateJavaVM(jvm, (void**)env, &vm_args);

  return vm_status;
}

#define CHECK_START(s) fprintf(stderr, "%-20s", s)

int main()
{
  JavaVMOption options[1];
  JNIEnv *env;
  JavaVM *jvm;

  jclass class;
  jmethodID method;
  jstring message;
  jint sum;
  long vm_status;

  /**
   *
   * Check jvm
   *
   */
  CHECK_START("jvm status:");
  vm_status = setup_jvm(options, &jvm, &env);
  if (vm_status == JNI_ERR)
    {
      fprintf(stderr, " fail\n");
      exit (1);
    }
  fprintf(stderr, " ok\n");




  /**
   *
   * Find class
   *
   */
  CHECK_START("find class: ");
  class = (*env)->FindClass(env, "se/juneday/Stupid");
  if(class ==0)
    {
      fprintf(stderr, " fail\n");
      exit (2);
    }
  fprintf(stderr, " ok\n");
  
  /**
   *
   * Find and use gimmeSum
   *
   */
  CHECK_START("find gimmeSum: ");
  method = (*env)->GetStaticMethodID(env, class, "gimmeSum", "(II)I");
  if(method==0)
    {
      fprintf(stderr, " fail\n");
      exit (3);
    }
  fprintf(stderr, " ok\n");
  
  sum = (*env)->CallStaticIntMethod(env, class, method, 12, 13);
  printf(" ** Result of int method: %d\n", sum);
  
          
  /**
   *
   * Find and use gimmeString
   *
   */
  CHECK_START("find gimmeString: ");
  method = (*env)->GetStaticMethodID(env, class, "gimmeString", "()Ljava/lang/String;");
  if(method==0)
    {
      fprintf(stderr, " fail\n");
      exit (4);
    }
  fprintf(stderr, " ok\n");
  message = (jstring) (*env)->CallStaticObjectMethod(env, class, method);
  const char *from_java =
    (*env)->GetStringUTFChars(env, message, NULL);
  printf(" ** Result of String method: %s\n", from_java);

  (*jvm)->DestroyJavaVM(jvm);
  return 0;
}

Build and run

To buld this program, using the environment variables set in in an earlier section:

$ eval $CC $JDK_C_FLAGS -ljvm c/c-program.c -o c/c-program
/usr/bin/ld: cannot find -ljvm
collect2: error: ld returned 1 exit status

The linker ld (part of the C compiler suite) compplains that it can't find a library called jvm. As you might have guessed this is the library containing the Java Virtual Machine. Let's try again with a hint to the compiler (and linker) where to find the library:

$ eval $CC -L\"$JDK_LIB_DIR\" $JDK_C_FLAGS -ljvm c/c-program.c -o c/c-program
$ echo $?
0

Note: if you get warnings (not errors) from the command can be ignored. Just make sure echo $? returned 0


You will hopefully not need to adjust these paths to fit your system. And to run the program, type ./main. Problem is you will rnu into a problem which will look differently on the various platforms.

Execution errors

Cygwin/Windows
$ ./c/c-program
C:/cygwin64/home/Pro Grammer/opt/programming-with-c/jni/c/c-program.exe: error while loading shared libraries: jvm.dll: cannot open shared object file: No such file or directory
MacOS
$ ./c/c-program 
dyld: Library not loaded: @rpath/libjvm.dylib
  Referenced from: /Users/xshenc/progund/programming-with-c/jni/./main
  Reason: image not found
Abort trap: 6
GNU/Linux
$ ./c/c-program
Segmentation fault (core dumped)

What? Segmentation fault? Problem is that we need to make sure that the (dynamic) linker can find the jvm library. You need to set this a bit differently:

Execution

Cygwin/Windows
PATH="$JDK_LIB_DIR":PATH ./c/c-program
jvm status:          ok
find class:          ok
find gimmeSum:       ok
 ** Result of int method: 25
find gimmeString:    ok
 ** Result of String method: Hi there, I am a stupid class!
MacOS and GNU/Linux
LD_LIBRARY_PATH="$JDK_LIB_DIR" ./c/c-program
jvm status:          ok
find class:          ok
find gimmeSum:       ok
 ** Result of int method: 25
find gimmeString:    ok
 ** Result of String method: Hi there, I am a stupid class!


... et voila, we're done.

Links

Links