A blogban leírtak a szerzők saját véleménye, és nem a munkáltatójuké.

Java Native Interface tanulságok

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  */
int Init(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.

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>