Kapitel 9 Java Native Interface (JNI)

Et af de kritikpunkter, der er blevet fremført mod Java, er dets platformsuafhængighed. Selvom dette samtidigt er blandt Javas største forcer, er der mange, der mener, at det begrænser programmørens muligheder. Det skyldes blandt andet den opfattelse, at Java kun understøtter de funktioner, som understøttes af alle operativsystemer. Tidligere har især brugergrænsefladen været skydeskive for megen kritik, da Windows indeholdt mange komponenter, der ikke kunne anvendes fra Java. Dette er blevet væsentligt forbedret med Swing. Der er imidlertid stadig en række punkter, hvor operativsystemet stiller flere funktioner til rådighed, end Java kan udnytte. For at kunne tilgå disse funktioner, må man benytte Java Native Interface (JNI).

Du læser en gammel bog
Den bog, du læser her, er fra 1998, og mange ting kan have ændret sig siden da.
Vi håber, at du stadig kan finde relevant information i den.
Hvis du vil læse aktuelle oplysninger om de avancerede dele af Java, anbefaler vi
bogen Core Java – Advanced Features

Med JNI er det muligt at tilgå metoder, der er skrevet i et andet programmeringssprog og kompileret til operativsystemets binære kode. Det er ikke kun anvendeligt, når man skal tilgå funktioner i operativsystemet, men også hvis man vil kombinere Java-programmer med programmer skrevet i andre programmeringssprog. Man kan for eksempel forestille sig, at realtidsdelen af en applikation skrives i C eller C++, mens den del, der foretager netværkskommunikation, skrives i Java.

 

I dette kapitel vil det først blive gennemgået, hvordan man laver et simpelt program, der kalder en metode i et C++-program. Herefter vil de mere avancerede muligheder blive gennemgået.

 

Kapitlet kræver et vist kendskab til C++, da alle eksempler på native kode vil blive skrevet i det sprog. Som compiler er Microsoft Visual C++ 4.2 benyttet.

 

Et simpelt kald til en native metode

Processen med at implementere native kode i sit Java-program kan opdeles i fem trin:

  1. Skriv Java-koden.
  2. Kompilér Java-koden.
  3. Opret en headerfil.
  4. Skriv implementeringen af den native metode.
  5. Kompilér den native kode.

Java-koden skal bestå af to ting. Dels skal den eller de native metoder defineres, og der skal være en statisk programblok i klassen, der indlæser det library, der indeholder den native metode. Et library er det, der i Windows kendes som en DLL-fil (Dynamic Link Library).

Ved den native metode angives kun metodens hoved. Dette markeres med det reserverede ord native. Indlæsningen af library’et sker ved hjælp af

 

metoden System.loadLibrary. Det betyder, at en klasse med native metoder kan komme til at se således ud:

public class SigHej

{

public native void hejsa();

 

static

{

System.loadLibrary (“hej”);

}

}

 

Fordelen ved at placere loadLibrary-metodekaldet i en statisk blok er, at man er sikker på, at det bliver udført, hvadenten klassen initialiseres eller ej. Hvor constructors kun bliver udført, når en klasse instantieres, bliver den statiske blok udført, så snart klassen bliver indlæst.

 

Det er imidlertid ikke nok bare at definere den native metode. Den skal også kaldes. Dette sker – i dette eksempel – fra en klasse, der hedder NativeHej:

 

public class NativeHej

{

public static final void main (String args[])

{

new SigHej().hejsa();

}

}

 

Som det ses, kaldes den native metode ligesom alle andre metoder. Det er således ikke muligt for brugeren at se, om en metode er skrevet i Java eller et andet sprog.

 

Ligeledes kompileres programmet på præcis samme måde som andre Java-programmer.

 

Når Java-koden er kompileret, kan man begynde at koncentrere sig om C++-koden. Første skridt i den forbindelse er at oprette en headerfil. Det gør man ved hjælp af programmet javah, der er inkluderet i Suns JDK. Dette program arbejder på den klassefil, der indeholder definitionen af den native metode – i dette eksempel SigHej – og opretter på den baggrund en headerfil, der kan inkluderes i C++-programmet. javah kaldes i Windows 95 med følgende kommando:

 

javah –jni SigHej

 

javah genererer en headerfil ved navn SigHej.h. Den fil ser således ud:

 

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class SigHej */

 

#ifndef _Included_SigHej

#define _Included_SigHej

#ifdef __cplusplus

extern “C” {

#endif

/*

 

* Class:     SigHej

* Method:    hejsa

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_SigHej_hejsa

(JNIEnv *, jobject);

 

#ifdef __cplusplus

}

#endif

#endif

 

Java_SigHej_hejsa er den funktion, der skal implementeres i C++. Man bruger signaturen fra header-filen, når man implementerer funktionen. Funktionen navngives efter følgende regler: Først sættes navnet Java. Derefter kommer navnet på den pakke, klassen befinder sig i. Det næste komponent er navnet på klassen, og det sidste er navnet på selve metoden. Elementerne adskilles med en understregning (_). Hvis klassen ikke befinder sig i en pakke – som i dette tilfælde – undlades dette element i funktionsnavnet.

 

I headerfilen har funktionen 2 parametre, selvom den oprindelige Java-metode ingen parametre havde. JNI kræver, at alle native metoder har disse to parametre. Det første parameter er en pointer til et JNIEnv-objekt. Dette objekt bruges til at tilgå variabler og parametre, der gives den native metode fra Java-systemet. Det andet parameter, er et jobject. Dette er selve det objekt, metoden befinder sig i. Hvis metoden i Java er statisk, vil det være en reference til selve klassen. jobject-referencen minder en smule om this-pointeren i C++. Blot skal man være opmærksom på, at man i en statisk metode for en reference til klassen i stedet for til et objekt.

 

Da dette er et simpelt eksempel, vil ingen af de to parametre blive brugt, men de vil blive gennemgået senere i kapitlet.

 

Med udgangspunkt i den genererede headerfil, kan man nu skrive implementeringen i et andet programmeringssprog end Java – i dette eksempel C++. Her kommer C++-koden til at se således ud:

 

#include <jni.h>

#include <stdio.h>

 

#include <sighej.h>

 

JNIEXPORT void JNICALL Java_SigHej_hejsa (JNIEnv *, jobject)

{

printf (“Hejsa verden!\n”);

}

 

Implementeringen af selve funktionen er simpel. printf-funktionen bruges til at udskrive beskeden – derfor bliver stdio.h inkluderet. Naturligvis bliver også sighej.h inkluderet. Hver gang man implementerer en native metode, skal man inkludere filen jni.h. Denne findes i biblioteket include\ under roden af Java-installationen. Denne fil inkluderer jni_md.h, der findes i biblioteket include\win32.

 

Med Microsoft Visual C++ 4.2 skal følgende kommando bruges til at kompilere C++-programmet

 

 

cl -Ic:\jdk12\include -Ic:\jdk12\include\win32 -If:\msdev\include -LD -ML hejImpl.cpp -Fehej.dll

 

Man skal naturligvis ændre placeringen af include-filerne, hvis JDK ikke er installeret i c:\jdk12.

 

Når C++-filen er kompileret, er Java-applikationen klar til at blive kørt. Det gør man – som vanligt – ved at skrive

 

java NativeHej

 

Nu hvor det grundlæggende i JNI er blevet gennemgået, kan man gå videre med de mere avancerede muligheder.

 

Parametre

 

I ovenstående eksempel havde den native metode ingen parametre og heller ingen returværdi. I dette afsnit vil det blive gennemgået, hvordan man håndterer parametre, og hvordan man returnerer værdier.

 

Som eksempel vil blive brugt en klasse – FileInfo – der har en metode, som hedder fileSize. fileSize returnerer størrelsen af en given fil i kilobytes. Navnet på filen gives med som parameter. Det betyder, at klassen kommer til at se således ud:

 

public class FileInfo

{

public native int fileSize (String filename);

 

static

{

System.loadLibrary (“File”);

}

}

 

Baseret på den kompilerede klassefil genererer javah følgende headerfil:

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class FileInfo */

#ifndef _Included_FileInfo

#define _Included_FileInfo

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     FileInfo

* Method:    fileSize

* Signature: (Ljava/lang/String;)I

*/

JNIEXPORT jint JNICALL Java_FileInfo_fileSize

(JNIEnv *, jobject, jstring);

 

#ifdef __cplusplus

}

 

#endif

#endif

 

Her ses det, at signaturen af metoden er ændret. Udover de to parametre, der blev diskuteret i forrige afsnit, er String i parameterlisten blevet erstattet af jstring, mens returtypen int er blevet til jint. Dette skyldes, at javah foretager en automatisk konvertering fra Java-typer til JNI-typer.

 

Nedenstående skema viser sammenhængen mellem Javas primitive type og JNI typerne.

 

java-type jni-type størrelse i bits
Boolean jboolean 8, unsigned
byte jbyte 8
Char jchar 16, unsigned
Short jshort 16
Int jint 32
Long jlong 64
Float jfloat 32
Double jdouble 64
Void Void ingen

 

Objekter overføres som referencer. Alle objekter overføres som jobject. De mest anvendte Java-objekter er imidlertid defineret som subklasser af jobject. Dette skema viser, hvordan bestemte Java-klasser er defineret i JNI:

 

Java-klasse JNI-klasse
Java.lang.object Jobject
java.lang.Class Jclass
java.lang.String Jstring
java.lang.Object[] Jobjectarray
boolean[] Jbooleanarray
byte[] Jbytearray
char[] Jchararray
double[] Jdoublearray
float[] Jfloatarray
int[] Jintarray
long[] jlongarray
short[] jshortarray

 

Strenge

 

Når man bruger variabler fra Java i eksempelvis C++, skal man være opmærksom på, at de ikke nødvendigvis er defineret på samme måde som tilsvarende variabler i sproget. Hvis man eksempelvis forsøger at udskrive en jstring, resulterer det sandsynligvis i, at programmet går ned.

 

I stedet skal man bruge de funktioner, JNI stiller til rådighed til at konvertere jstring til en C++-streng (char *).

 

For at konvertere parametret i dette eksempel til et char-array skal den følgende kode bruges:

 

JNIEXPORT jint JNICALL Java_FileInfo_fileSize (JNIEnv *env, jobject me, jstring filename)

{

const char *fname = env->GetStringUTFChars (filename, 0);

.

.

.

env->ReleaseStringUTFChars(filename, fname);

}

 

Funktionen GetStringUTFChars konverterer en jstring til et char-array. Når man er færdig med at bruge strengen, skal man huske at frigive den. Det gør man ved hjælp af funktionen ReleaseStringUTFChars. Kalder man ikke denne funktion, frigives hukommelsen ikke, og det vil med tiden medføre, at der ikke er mere ledig hukommelse. Dette skyldes, at garbage collector’en ikke virker i native metoder. Her er man overladt til at bruge de metoder, der findes i det sprog, man benytter.

 

Hvis man får brug for at konvertere et char-array til en jstring, giver JNI også mulighed for det. Det sker gennem af NewStringUTF-funktionen. Den bruges således:

 

char tekst[] = “Her er lidt tekst”;

env->NewStringUTF(tekst);

 

JNI stiller også andre funktioner til strenghåndtering til rådighed:

 

  • GetStringChars tager en Java-streng som parameter og returnerer en pointer til et array af de Unicode-tegn, der udgør strengen.
  • ReleaseStringChars frigiver den hukommelse, GetStringChars låste.
  • NewString opretter et jstring-objekt baseret på et array af Unicode-tegn.
  • GetStringLength returnerer længden af en streng, der består af et array af Unicode-tegn.
  • GetStringUTFLength returnerer længden af en UTF-streng.

 

Det er ikke nødvendigt at benytte disse funktioner i fileInfo-eksemplet. Her behøver man blot at finde filstørrelsen og returnere denne:

 

#include <jni.h>

#include <io.h>

 

#include “FileInfo.h”

JNIEXPORT jint JNICALL Java_FileInfo_fileSize (JNIEnv *env, jobject me, jstring filename)

{

const char *fname = env->GetStringUTFChars (filename, 0);

int f = _open (fname, 0);

int filesize = _filelength (f) >> 10;

_close (f);

env->ReleaseStringUTFChars(filename, fname);

 

 

return filesize;

}

 

Som det ses, kan filesize returneres uden videre. Mellem Javas primitive typer og de tilsvarende typer i JNI er ingen konvertering nødvendig. FileInfo-klassen er nu klar til brug. Følgende klasse viser, hvordan den kan bruges:

 

public class FileApp

{

public static final void main (String args[])

{

System.out.println (“Størrelsen af ” + args[0] + ” er ” + new FileInfo().fileSize(args[0]) + ” Kb.”);

}

}

 

Arrays

 

Ligesom strenge skal behandles på en speciel måde, gælder der også særlige regler for arrays. Som nævnt tidligere bliver arrays overført til en JNI-type, der svarer til den type elementer, der findes i arrayet.

 

Hvis man vil have en native metode til at udskrive samtlige elementer i et int-array, kan Java-filen se således ud:

 

public class ArrayPrint

{

public native void print (int array[]);

 

static

{

System.loadLibrary (“array”);

}

}

 

javah vil så generere en headerfil, der ser således ud:

 

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class ArrayPrint */

 

#ifndef _Included_ArrayPrint

#define _Included_ArrayPrint

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     ArrayPrint

* Method:    print

* Signature: ([I)V

*/

JNIEXPORT void JNICALL Java_ArrayPrint_print

(JNIEnv *, jobject, jintArray);

 

#ifdef __cplusplus

}

#endif

#endif

 

Det ses, at parametret int array[] som forventet er blevet til jintArray.

 

Det første, man gør, når man vil benytte et array i JNI, er at finde ud af arrayets længde. Dette kan man gøre, fordi arrays i Java – i modsætning til arrays i C++ ­– indeholder information om deres længde. Man finder et arrays længde ved at benytte funktionen GetArrayLength, der stilles til rådighed af JNIEnv-objektet.

 

I C++ er et array blot en pointer til det første element i arrayet. Det vil sige, at man for at konvertere et Java-array til et C++-array kun behøver at skaffe en pointer til element 0. Det kan man gøre ved hjælp af funktionen GetIntArrayElements. Denne funktion tager array’et og indekset som parametre. I dette tilfælde vil indekset være 0. Herefter kan array’et behandles som alle andre C++-arrays. Når man er færdig med at benytte array’et skal man frigive den hukommelse, det har optaget. Det gør man ved at kalde ReleaseIntArrayElements. Grunden til dette er, at Javas garbage collector normalt har mulighed for at flytte objekter i hukommelsen. I forbindelse med JNI bliver det imidlertid garanteret, at objekterne ikke flyttes. Dette gøres enten ved at oprette en kopi af dem, i et hukommelsesområde, der ikke tillader flytning, eller ved at mærke objekterne.

 

Følgende eksempel viser, hvordan et Java-array kan håndteres i C++:

 

#include <jni.h>

#include <stdio.h>

 

#include “ArrayPrint.h”

 

JNIEXPORT void JNICALL Java_ArrayPrint_print (JNIEnv *env, jobject me, jintArray array)

{

int len = env->GetArrayLength (array);

jint *cpp_array = env->GetIntArrayElements(array,0);

 

for (int i = 0; i < len; i++)

printf (“%i\n”, cpp_array[i]);

 

env->ReleaseIntArrayElements(array, cpp_array, 0);

}

 

Som det ses, håndteres array’et som et array af jint i stedet for et array af int. Det skyldes, af de variabler, der er int i Java er long i C++. For at undgå denne forvirring, kan man bruge JNI’s typer i stedet.

 

Nedenstående klasse viser, hvordan ArrayPrint kan anvendes:

 

public class ArrayApp

{

public static final void main (String args[])

{

int i[] = {1,2,3,4,5,6,7,8,9};

 

 

 

new ArrayPrint().print (i);

}

}

 

I dette eksempel er array’et ikke særlig stort, men hvis man arbejder med store arrays, kan det være et problem, at GetIntArrayElements returnerer hele array’et. Det vil sige, at det er alle elementerne i array’et, der skal kopieres i hukommelsen. Hvis man kun skal bruge en lille del af array’et og gerne vil undgå unødig kopiering, bør man anvende GetIntArrayRegion i stedet.

 

Sålænge man arbejder på arrays af primitive typer, foregår det relativt smertefrit. Hvis man arbejder på et array af typen Object i stedet, kan man imidlertid ikke bruge en get-funktion til at hente hele array’et. Her må man bruge GetObjectArrayElement, der kun kan returnere et element ad gangen.

 

Kald af Java-metoder

 

Som det er vist tidligere, kan native metoder fra Java kaldes på samme måde som andre Java-metoder. Det er imidlertid ikke helt så simpelt at kalde Java-funktioner fra andre programmeringssprog.

 

For at vise, hvordan en Java-metode kan blive kaldt fra C++, gennemgås her et eksempel, hvor en Java-metode kalder en C++-metode, der kalder en Java-funktion.

 

Java-klassen ser således ud:

 

public class MethodCall

{

public native void call();

 

public void callback()

{

System.out.println (“callback() udført”);

}

 

static

{

System.loadLibrary (“Method”);

}

}

 

Ved at bruge javah på den kompilerede kode af ovenstående klasse får man denne headerfil:

 

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class MethodCall */

 

#ifndef _Included_MethodCall

#define _Included_MethodCall

#ifdef __cplusplus

extern “C” {

 

#endif

/*

* Class:     MethodCall

* Method:    call

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_MethodCall_call

(JNIEnv *, jobject);

 

#ifdef __cplusplus

}

#endif

#endif

 

Før man kan kalde en Java-metode, er man nødt til at vide, hvilken klasse den er en del af. I dette tilfælde er det den samme klasse, som metoden selv er del af. Derfor kan man bruge jobject-objektet som gives med som parameter til at bestemme klassen. Funktionen GetObjectClass returnerer den klasse, et givent objekt er af.

 

Herefter skal man have en reference til den metode, der skal kaldes. Dette opnår man gennem funktionen GetMethodID. Denne metode skal som parametre have den klasse, metoden skal findes i, navnet på metoden og dens signatur. Navnet på metoden er som regel ligetil, men man skal være opmærksom på, at constructors – der jo normalt ikke har noget navn – gives navnet <init>.

 

Signaturerne formuleres på en lidt speciel måde. Den generelle form er

 

(parametre)returntype

 

Hver af Javas primitive typer har en et-bogstavsforkortelse, der bruges i både parameterlisten og returtypen. Forkortelserne er defineret som følger:

 

Z boolean
B byte
C char
S short
I int
L long
F float
D double
V void

Vil man anvende objekter i stedet for primitive typer, skriver man

 

Lklassenavn

For eksempel angiver

 

Ljava.lang.String

 

et objekt af typen String.

 

 

Arrays angives med en åben kantet parentes ([) efterfulgt af den type, array’ets elementer er af. Eksempelvis vil

 

[Ljava.lang.String

 

angive et array af tekststrenge.

 

Hvis man har brug for at generere mange signaturer, kan man vælge at bruge programmet javap. Ved at angive –s som parameter vises de interne signaturer, der blandt andet anvendes i JNI. Skriver man for eksempel

 

Javap –s java.lang.reflect.Array

 

får man følgende oversigt over Array-klassen. javap viser både den kendte Java-signatur og den interne signatur, så man har mulighed for at sammenligne og på den måde få mere erfaring med det interne format.

 

Compiled from Array.java

public final class Array extends java.lang.Object

/* ACC_SUPER bit set */

{

public static native java.lang.Object get(java.lang.Object, int);

/*   (Ljava/lang/Object;I)Ljava/lang/Object;   */

public static native boolean getBoolean(java.lang.Object, int);

/*   (Ljava/lang/Object;I)Z   */

public static native byte getByte(java.lang.Object, int);

/*   (Ljava/lang/Object;I)B   */

public static native char getChar(java.lang.Object, int);

/*   (Ljava/lang/Object;I)C   */

public static native double getDouble(java.lang.Object, int);

/*   (Ljava/lang/Object;I)D   */

public static native float getFloat(java.lang.Object, int);

/*   (Ljava/lang/Object;I)F   */

public static native int getInt(java.lang.Object, int);

/*   (Ljava/lang/Object;I)I   */

public static native int getLength(java.lang.Object);

/*   (Ljava/lang/Object;)I   */

public static native long getLong(java.lang.Object, int);

/*   (Ljava/lang/Object;I)J   */

public static native short getShort(java.lang.Object, int);

/*   (Ljava/lang/Object;I)S   */

public static java.lang.Object newInstance(java.lang.Class, int);

/*   (Ljava/lang/Class;I)Ljava/lang/Object;   */

public static java.lang.Object newInstance(java.lang.Class, int[]);

/*   (Ljava/lang/Class;[I)Ljava/lang/Object;   */

public static native void set(java.lang.Object, int, java.lang.Object);

/*   (Ljava/lang/Object;ILjava/lang/Object;)V   */

public static native void setBoolean(java.lang.Object, int, boolean);

/*   (Ljava/lang/Object;IZ)V   */

 

public static native void setByte(java.lang.Object, int, byte);

/*   (Ljava/lang/Object;IB)V   */

public static native void setChar(java.lang.Object, int, char);

/*   (Ljava/lang/Object;IC)V   */

public static native void setDouble(java.lang.Object, int, double);

/*   (Ljava/lang/Object;ID)V   */

public static native void setFloat(java.lang.Object, int, float);

/*   (Ljava/lang/Object;IF)V   */

public static native void setInt(java.lang.Object, int, int);

/*   (Ljava/lang/Object;II)V   */

public static native void setLong(java.lang.Object, int, long);

/*   (Ljava/lang/Object;IJ)V   */

public static native void setShort(java.lang.Object, int, short);

/*   (Ljava/lang/Object;IS)V   */

}

 

Det at hente et metode-ID er en tidsmæssig dyr operation, og derfor er den adskilt fra selve metodekaldet, så man kan hente ID’et én gang, og herefter kalde metoden flere gange. Man skal blot tænke på, at ID’et kun er gyldigt, så længe den klasse, der indeholder metoden, ikke bliver fjernet fra hukommelsen.

 

Når man har metode-ID, kan man kalde metoden ved hjælp af CallVoidMethod-funktionen. Parametre til denne funktion er det objekt, metoden skal kaldes på samt metode-ID. Hvis der også er parametre til den metode, der skal kaldes, kan disse gives med som ekstra parametre til CallVoidMethod.

 

Nedenstående kode viser, hvordan et kald af en Java-metode foretages fra et C++ program.

 

#include <jni.h>

#include <stdio.h>

 

#include “MethodCall.h”

 

JNIEXPORT void JNICALL Java_MethodCall_call (JNIEnv *env, jobject me)

{

jclass classname = env->GetObjectClass (me);

jmethodID id = env->GetMethodID (classname, “callback”, “()V”);

 

if (id == 0)

return;

 

printf (“Native metode kaldt.\n”);

 

 

env->CallVoidMethod(me, id);

}

 

Ovenstående eksempel viste, hvordan man kaldte en instansmetode – en ikke-statisk metode. Hvis man vil kalde en statisk metode, er fremgangsmåden lidt anderledes. I stedet for GetMethodID skal man bruge GetStaticMethodID, og i stedet for CallVoidMethod skal man bruge CallStaticVoidMethod.

 

Hvis man vil kalde en funktion, der returnerer andet end void, skal man bruge en af de andre Call<type>Method-funktioner – for eksempel CallBooleanMethod osv.

 

Hvis en klasse har overskrevet en af superklassens metoder, betyder det ikke, at man ikke har mulighed for at kalde superklassens metoder. Det gør man ved at få et metode-ID fra superklassen ved hjælp af GetMethodID-funktionen. Dernæst kalder man metoden gennem CallNonvirtualVoidMethod  eller lignende, alt efter hvilken returtype metoden har. Parametrene til funktionen er objektet, superklassen, ID’et og eventuelle parametre, der skal sendes med til den metode man kalder.

 

Exceptions

 

Når man kalder Java-metoder, risikerer man, at der opstår en exception. Selvom C++ har et system, der kan håndtere exceptions, er der ingen garanti for, at alle programmeringssprog, der kan benytte JNI, har det. Derfor er der til JNI udviklet et specielt system til håndtering af exceptions i andre programmeringssprog end Java. For at vise, hvordan man kan håndtere exceptions, vil der blive oprettet en klasse med en funktion, hvis eneste formål er at lave en exception. Klassen vil også have en native funktion, der kalder exception-funktionen.

 

public class ThrowException

{

public void doThrow() throws NullPointerException

{

throw new NullPointerException (“Denne funktion er tom”);

}

 

public native void testException();

 

static

{

System.loadLibrary(“Exception”);

}

}

 

 

Det giver en headerfil, der ser således ud:

 

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class ThrowException */

 

 

#ifndef _Included_ThrowException

#define _Included_ThrowException

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     ThrowException

* Method:    testException

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_ThrowException_testException

(JNIEnv *, jobject);

 

#ifdef __cplusplus

}

#endif

#endif

 

Den native metode skal først kalde metoden doThrow. Det gøres på den måde, der blev beskrevet tidligere i kapitlet. Herefter kontrolleres det, om der er opstået en exception. Denne kontrol foregår ved, at der oprettes en jthrowable-variabel. Denne sættes lig med returværdien af ExceptionOccurred. Hvis denne variabel har en værdi forskellig fra nul, er der opstået en exception. Man kan herefter få informationer om exception’en udskrevet ved at kalde ExceptionDescribe. Når man har håndteret den pågældende exception, skal man kalde ExceptionClear for at fjerne exception’en fra listen over opståede exceptions. Nedenstående C++ program viser, hvordan det kan gøres.

 

#include <jni.h>

 

#include “ThrowException.h”

 

JNIEXPORT void JNICALL Java_ThrowException_testException (JNIEnv *env, jobject me)

{

jclass thisclass = env->GetObjectClass (me);

jmethodID methodID = env->GetMethodID (thisclass, “doThrow”, “()V”);

env->CallVoidMethod (me, methodID);

 

jthrowable exception;

exception = env->ExceptionOccurred();

if (exception)

{

env->ExceptionDescribe();

env->ExceptionClear();

}

}

 

Det er uhyre vigtigt, at man håndterer eventuelle exceptions, før man kalder andre JNI-metoder. Ellers risikerer man, at disse opdager den pågældende exception

 

og tror, at det er deres skyld, at den er opstået. Det er således udefineret, hvad der sker, hvis man kalder en JNI-metode med en exception liggende i listen, men det er næppe det, man ønskede, der skulle ske.

 

Der er kun tre metoder, der risikofrit kan kaldes, sålænge der eksisterer en uhåndteret exception. Disse er

  • ExceptionOccured
  • ExceptionDescribe
  • ExceptionClear.

 

Nedenstående klasse kan bruges til at teste implementeringen af exception-eksemplet.

 

public class ExceptionApp

{

public static final void main (String args[])

{

new ThrowException().testException();

}

}

 

Kørsel af ovenstående program giver følgende udskrift:

 

java.lang.NullPointerException: Denne funktion er tom

at ThrowException.doThrow(ThrowException.java:5)

at ThrowException.testException(Native Method)

at ExceptionApp.main(ExceptionApp.java:5)

Exception in thread “main” Process completed successfully

 

Udskriften viser tydeligt, at exception’en først opstår i ThrowException, hvorefter den sendes til testException, der er den native metode.

 

Det, der nu er blevet gennemgået, er imidlertid kun halvdelen af emnet omkring exceptions. Det kan jo også ske, at den native metode får brug for at lade en exception opstå og blive sendt tilbage til den kaldende metode. Nedenstående kode viser definitionen af en Java-klasse, der indeholder en native metode, der genererer en exception.

 

public class ExceptionCreator

{

public native void create() throws NullPointerException;

 

static

{

System.loadLibrary (“Creator”);

}

}

 

javah-programmet genererer følgende headerfil baseret på ovenstående klassefil.

 

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class ExceptionCreator */

 

#ifndef _Included_ExceptionCreator

 

 

#define _Included_ExceptionCreator

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     ExceptionCreator

* Method:    create

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_ExceptionCreator_create

(JNIEnv *, jobject);

 

#ifdef __cplusplus

}

#endif

#endif

 

C++-programmet skal implementere funktionen, der blot opretter en exception. Exceptions i Java er som bekendt objekter af en given klasse, og første punkt i at oprette en exception er således at oprette en reference til exception-klassen. Det gør man ved hjælp af funktionen FindClass. Hvis denne funktion returnerer andet end 0, betyder det, at klassen blev fundet. Herefter kan man generere en ny exception ved at bruge funktionen ThrowNew. I kaldet til ThrowNew kan man blandt andet angive den tekst, exception’en skal indeholde. Nedenstående C++-program genererer en exception:

 

#include <jni.h>

 

#include “exceptioncreator.h”

 

JNIEXPORT void JNICALL Java_ExceptionCreator_create (JNIEnv *env, jobject me)

{

jclass exception = env->FindClass(“java/lang/NullPointerException”);

 

if (!exception) // Klassen kunne ikke findes

return;

 

env->ThrowNew (exception, “C++ programmer kan også lave exceptions”);

}

 

Klassen CreatorApp kan bruges til at afprøve ExceptionCreator-klassen.

 

public class CreatorApp

{

public static final void main (String args[]) throws Exception

{

new ExceptionCreator().create();

}

}

 

Programmet giver følgende udskrift:

 

java.lang.NullPointerException: C++ programmer kan også lave exceptions

 

at ExceptionCreator.create(Native Method)

at CreatorApp.main(CreatorApp.java:5)

 

Der er nu blevet gennemgået tilstrækkeligt meget af JNI til, at der kan vises et eksempel. Der vil her blive set på, hvordan man kan bruge Windows INI-filer fra Java.


 

INI-filer

 

I Windows API findes der en række funktioner, som giver adgang til INI-filer. INI-filerne minder meget om Javas property-filer og har den følgende opbygning:

 

[sektion]

nøgle = værdi

 

Mængden af funktioner, der kan bruges til at manipulere med disse filer, er stor, og de vil ikke alle blive inkluderet i dette eksempel. Eksemplet vil kun give mulighed for at skrive og læse en streng og for at skrive og læse en integer. Det gør man ved hjælp af Windows-funktionerne WritePrivateProfileString, GetPrivateProfileString og GetPrivateProfileInt. Som det ses, findes der ikke nogen funktion til at skrive en integer. Det gør man ved hjælp af WritePrivateProfileString. Hver af funktionerne har en parameter, der angiver navnet på INI-filen. Dette er ikke særligt objektorienteret, og derfor bliver det lavet om i Java-klassen. Her angives filnavnet kun én gang – i constructoren. Java-klassen, der stiller funktionaliteten til rådighed, ser således ud:

 

public class IniFile

{

private String filename;

 

private String getFilename ()

{

return filename;

}

 

public IniFile (String filename)

{

this.filename = filename;

}

 

public native void writeString (String section, String key, String value);

public native String readString  (String section, String key) throws KeyNotFoundException;

 

public native void writeInteger (String section, String key, int value);

 

public native int readInteger (String section, String key) throws KeyNotFoundException;

 

static

{

System.loadLibrary (“ini”);

}

}

 

I de to read-funktioner kan der opstå en KeyNotFoundException. Det sker, hvis den nøgle, man søger efter, ikke findes. De oprindelige funktioner i Windows API’et giver ikke mulighed for exceptions. Ved at skrive en Java-klasse rundt om operativsystemfunktioner opnår man altså ikke bare adgang til funktionerne, man har også mulighed for at give dem et mere objektorienteret design.

 

Klassen KeyNotFoundException er defineret specielt til brug ved IniFile-klassen. Det er en almindelig excpetion-klasse, der ser således ud:

 

public class KeyNotFoundException extends Exception

{

public KeyNotFoundException ()

{

super();

}

 

public KeyNotFoundException (String besked)

{

super (besked);

}

}

 

Da IniFile indeholder native metoder, skal der oprettes en headerfil baseret på den. Programmet javah genererer følgende headerfil:

 

/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class IniFile */

 

#ifndef _Included_IniFile

#define _Included_IniFile

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     IniFile

* Method:    readInteger

* Signature: (Ljava/lang/String;Ljava/lang/String;)I

*/

JNIEXPORT jint JNICALL Java_IniFile_readInteger

(JNIEnv *, jobject, jstring, jstring);

 

/*

* Class:     IniFile

* Method:    readString

 

* Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

*/

JNIEXPORT jstring JNICALL Java_IniFile_readString

(JNIEnv *, jobject, jstring, jstring);

 

/*

* Class:     IniFile

* Method:    writeInteger

* Signature: (Ljava/lang/String;Ljava/lang/String;I)V

*/

JNIEXPORT void JNICALL Java_IniFile_writeInteger

(JNIEnv *, jobject, jstring, jstring, jint);

 

/*

* Class:     IniFile

* Method:    writeString

* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V

*/

JNIEXPORT void JNICALL Java_IniFile_writeString

(JNIEnv *, jobject, jstring, jstring, jstring);

 

#ifdef __cplusplus

}

#endif

#endif

 

Headerfilen indeholder signaturerne på alle native metoder i klassen. Bemærk, at javah sorterer funktionerne i alfabetisk orden. Af den grund vil rækkefølgen af funktionerne i C++-implementeringen være anderledes end i Java-programmet. Det er naturligvis programmørens eget valg, hvilken rækkefølge funktionerne skal implementeres i.

 

For at få adgang til de ønskede funktioner fra Windows-API’et, skal man inkludere flere filer end vanligt. Det betyder, at #include-delen af C++-programmet kommer til at se således ud:

 

#include <windows.h>

#include <winbase.h>

 

#include <jni.h>

 

#include “IniFile.h”

 

De to første filer – windows.h og winbase.h – giver adgang til API-funktionerne, mens jni.h giver adgang til JNI-systemet. IniFile.h er den headerfil, der blev genereret af javah.

 

Den første funktion i C++-programmet er readInteger, der kommer til at hedde Java_IniFile_readInteger:

 

JNIEXPORT jint JNICALL Java_IniFile_readInteger (JNIEnv *env, jobject me, jstring section, jstring key)

{

// Find filnavnet

 

 

jclass classname = env->GetObjectClass (me);

jmethodID id = env->GetMethodID (classname, “getFilename”, “()Ljava/lang/String;”);

jstring filename = (jstring) env->CallObjectMethod (me,id);

 

 

// Konverter Java-strenge til tekststrenge

 

const char *fname = env->GetStringUTFChars (filename, 0);

const char *sction = env->GetStringUTFChars (section, 0);

const char *k = env->GetStringUTFChars (key, 0);

 

 

// Læs ini-filen

 

int result = GetPrivateProfileInt (sction, k, -1234, fname);

 

// Kontroller om værdien fandtes

 

if (result == -1234)

{

jclass exception = env->FindClass(“KeyNotFoundException”);

 

if (!exception)

return result;

 

char exceptiontext[256];

sprintf (exceptiontext,”Værdien %s i sektionen [%s] findes ikke i filen %s”, k, sction, fname);

env->ThrowNew (exception, exceptiontext);

}

 

 

// Frigiv hukommelse

 

env->ReleaseStringUTFChars (filename, fname);

env->ReleaseStringUTFChars (section, sction);

env->ReleaseStringUTFChars (key, k);

 

 

// Returner resultatet

 

return result;

}

 

Funktionens første opgave er at finde navnet på den fil, der skal bruges. Det sker ved at kalde Java-metoden getFilename. Som vist tidligere, kalder man en Java-metode ved først at lave en reference til klassen, dernæst en reference til metoden, hvorefter metodekaldet kan udføres. Da getFilename returnerer String, der som bekendt er et objekt, skal metoden CallObjectMethod bruges. Denne returnerer naturligvis et jobject – dette skal efterfølgende castes til jstring.

 

Den næste opgave i funktionen er at konvertere de forskellige jstring til

 

char-arrays, så de kan bruges i C++. Når dette er på plads, kan kaldet til API-funktionen foretages. Den mystiske parameter – -1234 – er en default værdi, der returneres, hvis nøglen ikke kunne findes. Den returnerede værdi sammenlignes med denne – er de ens, betyder det altså, at nøglen ikke fandtes, og derfor laves en KeyNotFoundException. Kan klassen KeyNotFoundException ikke findes, returnerer funktionen.

 

Er alt gået godt indtil videre, kan funktionen frigive den hukommelse, der er blevet låst af tekststrengene. Herefter kan resultatet returneres.

 

Java-funktionen readString bliver i C++ kaldt java_IniFile_readString og ser således ud:

 

JNIEXPORT jstring JNICALL Java_IniFile_readString (JNIEnv *env, jobject me, jstring section, jstring key)

{

// Find filnavnet

 

jclass classname = env->GetObjectClass (me);

jmethodID id = env->GetMethodID (classname, “getFilename”, “()Ljava/lang/String;”);

jstring filename = (jstring) env->CallObjectMethod (me,id);

 

 

// Konverter Java-strenge til tekststrenge

 

const char *fname = env->GetStringUTFChars (filename, 0);

const char *sction = env->GetStringUTFChars (section, 0);

const char *k = env->GetStringUTFChars (key, 0);

 

 

// Læs ini-filen

 

const char *defaultstring = “StandardVærdi”;

char result[1024];

 

GetPrivateProfileString (sction, k, defaultstring, result, sizeof (result), fname);

 

 

// Kontroller om værdien fandtes

 

if (!strcmp (defaultstring, result))

{

jclass exception = env->FindClass(“KeyNotFoundException”);

 

if (!exception)

return NULL;

 

char exceptiontext[256];

sprintf (exceptiontext,”Værdien %s i sektionen [%s] findes ikke i filen %s”, k, sction, fname);

env->ThrowNew (exception, exceptiontext);

 

}

 

 

// Frigiv hukommelse

 

env->ReleaseStringUTFChars (filename, fname);

env->ReleaseStringUTFChars (section, sction);

env->ReleaseStringUTFChars (key, k);

 

 

// Returner resultatet

 

return env->NewStringUTF (result);

}

 

Funktionen starter – ligesom den foregående – med at finde navnet på INI-filen, og konvertere tekststrenge. Kaldet til API’et er imidlertid anderledes. Igen skal der gives en defaultværdi med i funktionskaldet. Da det er en streng, der forsøges indlæst, skal defaultværdien naturligvis også være en streng. Til at modtage den fundne streng bliver der oprettet et char-array ved navn result. Sammenligningen af det modtagne resultat og defaultværdien sker ved hjælp af strcmp­-funktionen, der returnerer nul, hvis teksterne er ens. Er dette tilfældet, oprettes en KeyNotFoundException efter samme opskrift som i readInteger-metoden.

 

Herefter frigiver funktionen hukommelsen og er klar til at returnere resultatet. Dette skal imidlertid først konverteres, da det er af typen char *, og funktionen returnerer en jstring. Konverteringen sker ved hjælp af JNI’s funktion NewStringUTF.

 

Der er ikke meget ved at have læsefunktioner, hvis ikke man også har skrivefunktioner. Den første – WriteString – er defineret således:

 

JNIEXPORT void JNICALL Java_IniFile_writeString (JNIEnv *env, jobject me, jstring section, jstring key, jstring value)

{

// Find filnavnet

 

jclass classname = env->GetObjectClass (me);

jmethodID id = env->GetMethodID (classname, “getFilename”, “()Ljava/lang/String;”);

jstring filename = (jstring) env->CallObjectMethod (me,id);

 

 

// Konverter Java-strenge til tekststrenge

 

const char *fname = env->GetStringUTFChars (filename, 0);

const char *sction = env->GetStringUTFChars (section, 0);

const char *k = env->GetStringUTFChars (key, 0);

const char *vlue = env->GetStringUTFChars (value, 0);

 

// Skriv ini-filen

 

WritePrivateProfileString (sction, k, vlue, fname);

 

// Frigiv hukommelse

 

env->ReleaseStringUTFChars (filename, fname);

 

 

env->ReleaseStringUTFChars (section, sction);

env->ReleaseStringUTFChars (key, k);

env->ReleaseStringUTFChars (value, vlue);

}

 

Udover de parametre, de andre funktioner har, har skrivefunktionerne også en parameter, der angiver den værdi, der skal skrives.

 

Igen starter funktionen med at hente filnavnet og konvertere tekststrenge. Dernæst skrives til INI-filen, hukommelsen frigives, og funktionen afsluttes. Hvis INI-filen ikke allerede eksisterer, vil den blive oprettet.

 

Den anden skrivefunktion – writeInteger – er stort set identisk med writeString. Dog er der den krølle, at værdien skal konverteres fra en integer til en streng, da API’et kun stiller WritePrivateProfileString til rådighed. Dette gøres ved hjælp af sprintf-funktionen. writeString er implementeret således:

 

JNIEXPORT void JNICALL Java_IniFile_writeInteger (JNIEnv *env, jobject me, jstring section, jstring key, jint value)

{

// Find filnavnet

 

jclass classname = env->GetObjectClass (me);

jmethodID id = env->GetMethodID (classname, “getFilename”, “()Ljava/lang/String;”);

jstring filename = (jstring) env->CallObjectMethod (me,id);

 

 

// Konverter Java-strenge til tekststrenge

 

const char *fname = env->GetStringUTFChars (filename, 0);

const char *sction = env->GetStringUTFChars (section, 0);

const char *k = env->GetStringUTFChars (key, 0);

 

 

// Skriv ini-filen

 

char valuestring [128];

sprintf (valuestring, “%i”, value);

WritePrivateProfileString (sction, k, valuestring, fname);

// Frigiv hukommelse

env->ReleaseStringUTFChars (filename, fname);

env->ReleaseStringUTFChars (section, sction);

env->ReleaseStringUTFChars (key, k);

}

Når C++-filen er blevet kompileret til en DLL-fil, er systemet klar til brug. Nedenstående klasse – IniApp – viser, hvordan systemet kan bruges:

public class IniApp

{

public static final void main (String args[])

{

 

 

IniFile ini = new IniFile (“.\\test.ini”);

 

try

{

System.out.println (“By: ” + ini.readString (“Byer”, “7430”));

}

catch (KeyNotFoundException ex1)

{

System.err.println (ex1.toString());

}

 

try

{

System.out.println (“Postnummer: ” + ini.readInteger (“Postnumre”, “Ikast”));

}

catch (KeyNotFoundException ex2)

{

System.err.println (ex2.toString());

}

 

ini.writeString (“Byer”, “7430”, “Ikast”);

ini.writeInteger (“Postnumre”, “Ikast”, 7430);

 

try

{

System.out.println (“By: ” + ini.readString (“Byer”, “7430”));

}

catch (KeyNotFoundException ex3)

{

System.err.println (ex3.toString());

}

 

try

{

System.out.println (“Postnummer: ” + ini.readInteger (“Postnumre”, “Ikast”));

}

catch (KeyNotFoundException ex4)

{

System.err.println (ex4.toString());

}

}

}

 

Programmet starter med at instantiere et IniFile-objekt. Navnet på filen angives som .\\test.ini. At der skal angives et bibliotek, selvom filen skal ligge i det aktuelle bibliotek, skyldes, at Windows automatisk placerer INI-filer i Windows-bibliotek, hvis der ikke angives andet.

 

Dernæst forsøger programmet at indlæse en tekststreng fra nøglen 7430. Hvis denne ikke findes, opstår en exception, der udskrives. Det samme gør

 

sig gældende, når programmet forsøger en integer med nøglen Ikast.

 

Herefter skriver programmet de to nøgler og indlæser dem igen. Denne gang burde der ikke være nogen exceptions, da nøglerne jo netop er skrevet.

 

Hvis filen test.ini ikke eksisterer, inden programmet udføres, får man følgende udskrift:

 

KeyNotFoundException: Værdien 7430 i sektionen [Byer] findes ikke i filen .\test.ini

KeyNotFoundException: Værdien Ikast i sektionen [Postnumre] findes ikke i filen .\test.ini

By: Ikast

Postnummer: 7430

 

Efter kørsel af programmet er filen test.ini blevet oprettet. Filen har – som forventet – følgende indhold:

 

[Byer]

7430=Ikast

 

[Postnumre]

Ikast=7430

 

Tråde

 

Java er som bekendt et flertrådet programmeringssprog. Dette gælder også, når man bruger native metoder, men der er nogle ting, man skal være opmærksom på.

 

  • JNIEnv-pointeren er kun gyldig i den aktuelle tråd. Man må ikke overføre JNIEnv-pointeren fra en tråd til en anden.
  • Man må ikke overføre lokale variable fra en tråd til en anden. En lokal variabel kan nå at blive ugyldig, inden den modtagende tråd får mulighed for at benytte den.

 

I almindelige Java-programmer kan man nemt anvende Javas monitor til synkronisering. Dette kan eksempelvis gøres således:

 

synchronized (obj)

{

… // Kritisk region

}

 

Så er det garanteret, at der ikke er andre tråde, der låser obj. Man kan ikke forvente, at andre programmeringssprog stiller en monitor til rådighed for programmørerne – i særdeleshed gør C++ ikke. Derfor har JNI-systemet to funktioner til at sikre synkronisering. Det drejer sig om funktionen MonitorEnter, der kaldes, når den programmet indtræder i den kritiske region, og MonitorExit, der kaldes, når programmet forlader den kritiske region. Det betyder, at ovenstående Java-eksempel kan omformuleres til følgende C++-kode:

env->MonitorEnter (obj);

…  // Kritisk region

env->MonitorExit (obj);

 

 

 

Tråde benytter også metoder som wait, stop og notify. Disse er ikke umiddelbart tilgængelige i JNI-systemet, men man kan naturligvis bruge teknikkerne til at kalde en Java-metode som beskrevet tidligere i kapitlet.

 

Den virtuelle Java-maskine

 

Fra og med JDK version 1.1 bliver den virtuelle Java-maskine leveret som en DLL-fil. Det betyder, at man fra andre ikke-Java-programmer kan tilgå den virtuelle maskine ved blot at kalde metoder i DLL’en. Faktisk er programmet java, der bruges til at afvikle Java-programmer, ikke mere end et C-program, der behandler parametrene og kalder den virtuelle maskine gennem DLL-filen.

 

Fra C++ kan man foretage kaldene til DLL’en gennem jni.h-filen. Det første, man skal gøre, er at hente standardindstillingerne for den virtuelle maskine – herefter kan man tilpasse dem, så de passer til ens eget formål. Funktionskaldet, der henter indstillinger, har ét parameter – det er en struktur, der indeholder indstillingerne. Koden, der henter indstillingerne for den virtuelle Java-maskine, ser således ud:

 

JDK1_1InitArgs vm_args;

JNI_GetDefaultJavaVMInitArgs (&vm_args);

 

Det præcise udseende af strukturen afhænger såvel af den platform, programmet afvikles på, som af udbyderen af den enkelte Java-maskine. På Suns virtuelle Java-maskine til Windows 95 ser definitionen således ud:

 

typedef struct JDK1_1InitArgs {

jint version;

 

char **properties;

jint checkSource;

jint nativeStackSize;

jint javaStackSize;

jint minHeapSize;

jint maxHeapSize;

jint verifyMode;

char *classpath;

 

jint (JNICALL *vfprintf)(FILE *fp, const char *format, va_list args);

void (JNICALL *exit)(jint code);

void (JNICALL *abort)();

 

jint enableClassGC;

jint enableVerboseGC;

jint disableAsyncGC;

jint verbose;

jboolean debugging;

jint debugPort;

} JDK1_1InitArgs;

 

Indholdet af strukturen kan ændres efter behov.

 

 

 

Det næste punkt på listen er at oprette selve den virtuelle maskine. Det sker ved et kald af funktionen JNI_CreateJavaVM. Denne funktion tager tre parametre. Den første er en reference til en javaVM-struktur. Den næste er en reference til en JNIEnv-struktur, mens den sidste er de argumenter, JNI_GetDefaultJavaVMInitArgs returnerede. De to første parametre bruges til at gemme returværdier i. Funktionen returnerer en jint. Er denne mindre end 0, er der opstået en fejl.

 

Koden, der opretter den virtuelle Java-maskine, kan se således ud:

 

JavaVM *javamaskine;

JNIEnv *env;

jint resultat;

resultat = JNI_CreateJavaVM (&javamaskine, &env, &vm_args);

if (resultat < 0)

{

printf (“Java-maskinen kunne ikke oprettes.”);

exit(1);

}

 

Når man foretager et kald til JNI_CreateJavaVM, bliver den tråd, der foretager kaldet, automatisk oprettet i den virtuelle maskine som en native metode. Den JNIEnv-pointer, der modtages ved kaldet af JNI_CreateJavaVM, svarer til den pointer, der normalt bliver overført som parameter til native metoder. Man skal dog være opmærksom på, at programmer, der selv starter den virtuelle maskine, ikke har mulighed for at returnere til den virtuelle maskine ligesom ordinære native metoder. Det betyder, at lokale variable ikke bliver frigivet, før man selv frigiver dem.

 

Når den virtuelle maskine er startet, kan man kalde Java-metoder på samme måde som fra native metoder. Når man kompilerer sit C++-program, skal man huske at inkludere filen jvm.lib. Derudover skal filen jvm.dll være tilgængelig for programmet, når det afvikles. jvm.lib kan findes i biblioteket lib under Java-installationen, mens jvm.dll kan findes i bin\classic.

 

Nedenstående eksempel viser, hvordan man kan bruge JNI til at starte et Java-program.

 

#include <jni.h>

#include <malloc.h>

 

int main (int argc, char *argv[])

{

// Kontroller syntaks

 

if (argc != 2)

{

printf (“Syntaks: JavaVM <klasse>”);

return 1;

}

 

 

// Lokale variable

 

JDK1_1InitArgs vm_args;

 

JavaVM *javamaskine;

JNIEnv *env;

jclass theclass;

jmethodID methodID;

char classpath[1024];

 

 

// Hent standardindstillinger

 

JNI_GetDefaultJavaVMInitArgs (&vm_args);

 

 

// Indstil classpath

sprintf (classpath, “%s;.;c:\\programmer\\jdk12\\lib\\classes.zip”, vm_args.classpath);

vm_args.classpath = classpath;

 

 

// Opret Java-maskine

 

jint resultat = JNI_CreateJavaVM (&javamaskine, &env, &vm_args);

 

if (resultat < 0)

{

printf (“Java-maskinen kunne ikke oprettes.”);

return 2;

}

 

// Find den klasse, der skal udføres

 

theclass = env->FindClass (argv[1]);

if (!theclass)

{

printf (“Klassen %s kunne ikke findes.”, argv[1]);

return 3;

}

 

// Find main-metoden

 

methodID = env->GetStaticMethodID (theclass, “main”, “([Ljava/lang/String;)V”);

if (!methodID)

{

printf (“Klassen %s har ikke en korrekt defineret main-metode.”);

return 4;

}

 

 

// Kald main-metoden

 

env->CallStaticVoidMethod (theclass, methodID);

 

 

// Afslut programmet

 

return 0;

}

 

 

 

Programmet starter med at kontrollere, at det bliver kaldt med den rigtige syntaks. Den rigtige syntaks er JavaVM klasse. Dernæst bliver der oprettet en række lokale variabler, der skal bruges senere i programmet. Herefter bliver standardindstillinger for den virtuelle Java-maskine hentet, og classpath bliver sat.

 

Det første parameter til programmet er som bekendt den klasse, der skal udføres, og derfor er det denne klasse, der forsøges fundet med FindClass-funktionen. Herefter findes den statiske main-metodes ID, og funktionen kaldes uden parametre. I dets nuværende form understøtter JavaVM-programmet ikke, at der skal gives parametre med til main-metoden.

 

Sammenfatning

 

I dette kapitel er det blevet gennemgået, hvordan man bruger Javas Native Interface. Det er blevet forklaret, hvordan man ved hjælp af javah-programmet kan oprette headerfiler, der gør det let at implementere funktioner i C og C++. Derudover er det blevet gennemgået, hvordan man kalder Java-metoder fra native metoder, ligesom håndteringen af Java-objekter og variable samt exceptions er blevet beskrevet. Disse emner er blevet belyst yderligere gennem et eksempel, hvor det blev vist, hvordan man kunne bruge Windows API til at tilgå INI-filer fra Java.

 

Endeligt har kapitlet beskrevet, hvordan man gennem JNIEnv-pointeren kan opnå synkronisering, og hvordan JNI kan bruges til at skrive en virtuel Java-maskine.

 

FAQ

 

Hvorfor får jeg en java.lang.UnsatisfiedLinkError, når jeg prøver at køre mit program?

 

Der kan være to grunde til, at man får en UnsatisfiedLinkError. Den ene er, at man har glemt den statiske blok, der indlæser DLL-filen. Den anden er, at man ikke har sat sin library path rigtigt op. Library path kan kun være et problem i Unix. I Windows søger loadLibrary-metoden efter DLL-filer de samme steder som Windows selv. Hvis man vil ændre sin library path i Unix, kan det gøres med følgende kommando:

% setenv LD_LIBRARY_PATH sti

Er der forskel på, hvordan JNI bruges fra C og C++

Ja. Selvom C og C++ på mange måder minder om hinanden, er der forskel på den måde, man kalder funktioner gennem JNIEnv-pointeren. I C++ kan man gøre det ved hjælp af

env->NewStringUTF (…);

 

mens man i C skal skrive

 

(*env)->NewStringUTF (env, …);

 

 

 

 

Min headerfil ligner slet ikke de headerfiler, der er blevet vist her i kapitlet. Hvad er der galt?

 

Hvis man ikke husker –jni-parametret, genererer javah-compileren ikke headerfiler til brug ved JNI.

 

Jeg bruger den samme DLL-fil i flere forskellige klasser. Behøver jeg at indlæse den i hver klasse?

 

Nej. Hvis man bruger den samme DLL-fil til at gemme funktionaliteten af flere klasser i, behøver man ikke at indlæse DLL’en flere gang. Blot skal man være sikker på, at alle klasserne indlæses med den samme class loader.

 

Hvordan overføres parametre fra Java til native metoder?

 

Parametre overføres på præcis samme måde som ved almindelige funktionskald i Java. Det vil sige, at primitive typer kopieres, mens objekter overføres som referencer.

 

Hvordan sender man en exception videre til den Java-metode, der kaldte den native metode?

 

Hvis man returnerer fra en native metode, mens exceptionOccured er forskellig fra 0, vil den exception, der er opstået, men endnu ikke håndteret, blive sendt videre til den Java-metode, der kaldte den native metode. Her vil exception’en opstå ligesom en almindelig Java-exception.

 

Du læser en gammel bog
Den bog, du læser her, er fra 1998, og mange ting kan have ændret sig siden da.
Vi håber, at du stadig kan finde relevant information i den.
Hvis du vil læse aktuelle oplysninger om de avancerede dele af Java, anbefaler vi
bogen Core Java – Advanced Features

Comments are closed.