Július környékén kicsit írtam egy alkalmazást, ami JNI-n keresztül ért el natív C-s könyvtárakat. A kísérletből származó tapasztalataimat most megosztom veletek.
Könyvtárak betöltése
Az első feladat, hogy betöltsük a szükséges natív könyvtárakat. Erre a legegyszerűbb mód:
public class LibLoader {
static {
// Load native library
String nativeLibPath = System.getProperty("nativeLib");
if (nativeLibPath == null) {
throw new RuntimeException("The native library could not be loaded, nativeLib system" +
"property missing");
}
else {
try {
System.loadLibrary(nativeLibPath);
}
catch (Throwable ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
...
}
Azonban, ha a library-k tartalmaznak körkörös függőséget (pl egy plugin visszahív az őt betöltő könyvtárba), akkor ennyi nem elég. Ugyan sehol sem találtam meg az egzakt leírást, de kiderítettem sok kutatással, hogy a System.loadLibrary() RTLD_LOCAL-lal nyitja meg a könyvtárakat. Emiatt csa előrefele működik a névfeloldás. A legfontosabb forrás ez a blog bejegyzés volt: http://blogs.sun.com/rie/entry/c_dynamic_linking_symbol_visibility
A megoldás az lett, hogy írtam egy olyan könyvtárat, ami elkapja a dlopen() hívást, és megváltoztatja a RTLD_LOCAL-t RTLD_GLOBAL-ra. Plusz ezt a könyvtárat a java előtt kell betölteni. A kód magáért beszél:
/*
* native_lib_loader.c
*
* dlopen() interceptor
*
* (C) Andras Kovi
*/
#include
#include
#include
#include
#ifndef RTLD_NEXT
#define RTLD_NEXT ((void *) -1L)
#endif
// The dlopen trap function
extern void *dlopen (__const char *__file, int __mode);
static const char *native_lib_name="lib_native.so";
static int isDebugMode = 0;
void* dlopen(const char *__file, int __mode)
{
static void *(*dlopen_original) (__const char *__file, int __mode);
if (strstr(__file, native_lib_name) != NULL) {
if (isDebugMode)
fprintf(stderr, "DLOPEN INTERCEPTOR dlopen native library "
"loading (%s), modifying mode\n",
native_lib_name),fflush(stderr);
__mode = RTLD_GLOBAL | RTLD_NOW;
}
dlopen_original = (void*(*)(const char *file, int mode)) dlsym(RTLD_NEXT, "dlopen");
return (*dlopen_original)( __file, __mode );
}
void __attribute__ ((constructor)) native_lib_loader_init(void) {
char *env_debug = getenv("LD_DEBUG");
if (env_debug != NULL)
isDebugMode = 1;
if (isDebugMode)
fprintf(stderr, "DLOPEN INTERCEPTOR INITIALIZED\n"),fflush(stderr);
}
A futtatás a következőképpen zajlik
LD_PRELOAD=nativeloader.so java SomeJniUserClass
Java visszahívása
A JNI oda-vissza lehetővé teszi a kommunikációt a Java és a natív világ között. Azonban a Java-ba visszahívás kódja sok esetben nagyjából ugyanolyan, és egyszerű automatikusan generálni is. Ehhez nyújt kis segítséget a köv osztály.
Bemeneti argumentumként várja a java osztályokat és a kimenetet a stdout-ra írja. Végigmegy az összes megadott class összes fieldjén és metódusán, és legenerálja hozzá a kódot.
package hu.bme.mit;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Logger;
public class JNIAccessorGenerator {
Vector classes = new Vector();
Logger log = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
/**
* @param args
*/
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: JNIAccessorGenerator className [className...]");
System.exit(-1);
}
String[] classes = Arrays.copyOf(args, args.length);
JNIAccessorGenerator generator = new JNIAccessorGenerator(classes);
generator.generate();
}
public JNIAccessorGenerator(String[] classNames) {
for (String className : classNames)
loadClass(className);
}
public void loadClass(String className) {
try {
Class c = Class.forName(className);
classes.add(c);
log.info("Loaded class " + className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public void generate() {
StringBuilder sb = new StringBuilder();
// Create variables
sb.append(" /* Variable declarations */\n");
for (Class c : classes) {
String cn = createVariableName(c);
sb.append("\njclass " + cn + " = NULL;");
sb.append("\t // " + c.toString());
sb.append("\n");
for (Field field : c.getDeclaredFields()) {
sb.append("jfieldID " + createVariableName(field) + " = NULL;");
sb.append("\t // " + field.toGenericString());
sb.append("\n");
}
for (Constructor constructor : c.getDeclaredConstructors()) {
sb.append("jmethodID " + createVariableName(constructor) + " = NULL;");
sb.append(" // " + constructor.toGenericString());
sb.append("\n");
}
for (Method method : c.getDeclaredMethods()) {
if (method.getName().startsWith("jni"))
continue;
sb.append("jmethodID " + createVariableName(method) + " = NULL;");
sb.append("\t // " + method.toString());
sb.append("\n");
}
}
sb.append("\n\n/* Initialize */\n");
sb.append("int Init(JNIEnv *env) {\n");
for (Class c : classes) {
String tempStr;
String cn = createVariableName(c);
sb.append("\n\t//" + c.toString() + "\n");
tempStr = String.format("\tif (NULL == (%s = goJniNewGlobalClassRef(env, \"%s\")))\n\t\tgoto error;\n",
cn, c.getName().replace('.', '/'));
sb.append(tempStr);
for (Field field : c.getDeclaredFields()) {
tempStr = String.format(
"\tif (NULL == (%s = (*env)->GetFieldID(env, %s, \"%s\", \"%s\")))\n\t\tgoto error;\n",
createVariableName(field), cn, field.getName(), javaType2jniType(field.getType()));
sb.append(tempStr);
}
for (Constructor constructor : c.getDeclaredConstructors()) {
tempStr = String.format(
"\tif (NULL == (%s = (*env)->GetMethodID(env, %s, \"\", \"%s\")))\n\t\tgoto error;\n",
createVariableName(constructor), cn, javaConstructor2jniSignature(constructor));
sb.append(tempStr);
}
for (Method method : c.getDeclaredMethods()) {
tempStr = String.format(
"\tif (NULL == (%s = (*env)->GetMethodID(env, %s, \"%s\", \"%s\")))\n\t\tgoto error;\n",
createVariableName(method), cn, method.getName(), javaMethod2jniSignature(method));
sb.append(tempStr);
}
}
sb.append("\n}\n\n");
System.out.println(sb.toString());
}
private String createVariableName(Field f) {
return "FID_" + f.getDeclaringClass().getSimpleName() + "_"+ f.getName();
}
private String createVariableName(Constructor c) {
StringBuilder sb = new StringBuilder();
sb.append("CID_" + c.getDeclaringClass().getSimpleName() + "_constructor");
for (Class param : c.getParameterTypes()) {
sb.append("_" + param.getSimpleName().replace('.', '_'));
}
return sb.toString();
}
private Map methodSignatureToVersion = new HashMap();
private Map methodVersions = new HashMap();
private String createVariableName(Method m) {
String signature= "MID_" + m.getDeclaringClass().getSimpleName() + "_" + m.getName();
String extendedSignature = signature;
for (Class param : m.getParameterTypes())
extendedSignature = extendedSignature + "_" + param.getName();
//Integer overloads = methodVersions.get(m.getName());
Integer overloads = methodVersions.get(signature);
if (overloads != null) {
// There are overloading methods
Integer version = methodSignatureToVersion.get(extendedSignature);
if (version == null) {
// This overload has not been registered yet
overloads += 1;
methodSignatureToVersion.put(extendedSignature, new Integer(overloads));
//methodVersions.put(m.getName(), overloads);
methodVersions.put(signature, overloads);
version = overloads;
}
if (version == 1)
return signature;
else
return signature + "_" + version;
}
overloads = new Integer(1);
methodVersions.put(signature, overloads);
methodSignatureToVersion.put(extendedSignature, new Integer(overloads));
return signature;
}
private String createVariableName(Class c) {
return "CLS_" + c.getSimpleName();
}
private String javaType2jniType(Class c) {
String type = c.getName();
if (type == "boolean") return "Z";
if (type == "byte") return "B";
if (type == "char") return "C";
if (type == "short") return "S";
if (type == "int") return "I";
if (type == "long") return "J";
if (type == "float") return "F";
if (type == "double") return "D";
if (type == "void") return "V";
if (type.contains(".")) type = "L" + type + ";";
return type.replace('.', '/');
}
private String javaConstructor2jniSignature(Constructor c) {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (Class p : c.getParameterTypes())
sb.append(javaType2jniType(p));
sb.append(")V");
return sb.toString();
}
private String javaMethod2jniSignature(Method m) {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (Class p : m.getParameterTypes())
sb.append(javaType2jniType(p));
sb.append(")");
sb.append(javaType2jniType(m.getReturnType()));
return sb.toString();
}
}
Pl a java.lang.Long-ra a következő kimenetet adja:
/* Variable declarations */ jclass CLS_Long = NULL; // class java.lang.Long jfieldID FID_Long_MIN_VALUE = NULL; // public static final long java.lang.Long.MIN_VALUE jfieldID FID_Long_MAX_VALUE = NULL; // public static final long java.lang.Long.MAX_VALUE ... jmethodID CID_Long_constructor_String = NULL; // public java.lang.Long(java.lang.String) throws java.lang.NumberFormatException jmethodID CID_Long_constructor_long = NULL; // public java.lang.Long(long) jmethodID MID_Long_hashCode = NULL; // public int java.lang.Long.hashCode() jmethodID MID_Long_reverseBytes = NULL; // public static long java.lang.Long.reverseBytes(long) ... /* Initialize */ intInit(JNIEnv *env) { //class java.lang.Long if (NULL == (CLS_Long = goJniNewGlobalClassRef(env, "java/lang/Long"))) goto error; if (NULL == (FID_Long_MIN_VALUE = (*env)->GetFieldID(env, CLS_Long, "MIN_VALUE", "J"))) goto error; if (NULL == (FID_Long_MAX_VALUE = (*env)->GetFieldID(env, CLS_Long, "MAX_VALUE", "J"))) goto error; ... if (NULL == (CID_Long_constructor_String = (*env)->GetMethodID(env, CLS_Long, " ", "(Ljava/lang/String;)V"))) goto error; if (NULL == (CID_Long_constructor_long = (*env)->GetMethodID(env, CLS_Long, " ", "(J)V"))) goto error; if (NULL == (MID_Long_hashCode = (*env)->GetMethodID(env, CLS_Long, "hashCode", "()I"))) goto error; ... }
A javap java.lang.Long parancsot is lehet használni, hogy az osztály változóit kitaláljuk, de a használható kódhoz még sokat kell szöszmötölni. Nekem minden esetre hasznos volt. Talán más is tudja majd használni.
Ha valami advancedabb megoldásra vágysz, akkor ott van a GlueGen. Ezzel kreálták a Java OpenGL kódjának nagy részét. Nekem nem igazán sikerült felfogni egyelőre, plusz sokkal bonyolultabb, mint ami nekem kellett.