Sunday, March 30, 2014

JNI, C++ and Name Mangling


The Problem

JNI has been around for a long time but there isn't a huge amount of information out there when things go wrong. You might see a horrible exception like:


[phenry@localhost MyTest]$ java -Djava.library.path=/mnt/hgfs/Shared/Code/JNI/MyJni/Debug -cp target/classes/ com.henryp.jni.MyJni
Exception in thread "main" java.lang.UnsatisfiedLinkError: com.henryp.jni.MyJni.munge(Ljava/lang/String;)Ljava/lang/String;
at com.henryp.jni.MyJni.munge(Native Method)
at com.henryp.jni.MyJni.main(MyJni.java:22)

which doesn't tell you an awful lot about what went wrong. To make matters worse, the problem can be with your C/C++ rather than your Java - making things much harder to debug.

The Code

The Java code is very simple.

package com.henryp.jni;

public class MyJni {
    
    static {
        System.loadLibrary("MyJni");
    }
    
    private native String munge(String string);

    public static void main(String[] args) {
        MyJni app = new MyJni();
        System.out.println(app.munge("test message"));
    }
}

Note: on Linux, the actual file name defined in loadLibrary is prepended with lib and appended with .so, so in this example, the JVM is looking for something called libMyJni.so

Executing:

javah -classpath target/classes -d target com.henryp.jni.MyJni

produces a file com_henryp_jni_MyJni.h that looks like:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_henryp_jni_MyJni */

#ifndef _Included_com_henryp_jni_MyJni
#define _Included_com_henryp_jni_MyJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_henryp_jni_MyJni
 * Method:    munge
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_henryp_jni_MyJni_munge
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Writing some C++ code that does nothing but print out what it was given and return a new string seemed straightforward:

#include <iostream>
#include "com_henryp_jni_MyJni.h"
using namespace std;

JNIEXPORT jstring JNICALL Java_com_henryp_jni_MyJni_munge
  (JNIEnv * env, jobject obj, jstring str) {
        printf("Java_com_henryp_jni_MyJni_munge called!\n");
        char* msg = "this is a test";
        return (*env)->NewStringUTF(env, msg);
}

and compiling it was a breeze:

g++ -I/usr/java/latest/include/linux -I/usr/java/latest/include/ -shared src/MyJni.cpp -o Debug/libMyJni.so

(Note the -shared. This is telling it to not produce an executable but to "produce a shared object which can then be linked with other objects to form an executable." [1])

The Solution

So, why did I get the Error at the top of this post? Well, the JVM loads the library without error (it will barf on loadLibrary if there is something wrong with the shared object). So, the problem is with the method. I never see it print out this is a test so it never gets that far. It must be with the method definition itself.

[phenry@localhost MyJni]$ nm Debug/libMyJni.so | grep munge
0000060d t _GLOBAL__sub_I__Z31Java_com_henryp_jni_MyJni_mungePPK19JNINativeInterface_P8_jobjectS4_
00000598 T _Z31Java_com_henryp_jni_MyJni_mungePPK19JNINativeInterface_P8_jobjectS4_

Hmm, the method Java_com_henryp_jni_MyJni_munge doesn't actually exist in my shared object. This is due to C++ using name mangling to allow multiple usage of a method name in different namespaces.

Writing my shared object in C and compiling with:

gcc -I/usr/java/latest/include/linux -I/usr/java/latest/include/ -shared src/MyJni.c -o Debug/libMyJni.so

solved the problem. Now, I can see my method.

[phenry@localhost MyJni]$ nm Debug/libMyJni.so | grep munge
00000468 T Java_com_henryp_jni_MyJni_munge

This answer on Stackoverflow shows how to write your code in C++ and link it to a C implementation of the autogenerated JNI header file. The author doesn't say why he prefers to compile JNI in C but this could be why.

2 comments: