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 { Vectorclasses = 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.