Skip to main content

Launch Java Applications from Assembly Language Programs

October 4, 2007

{cs.r.title}







Java
Native Interface
(JNI) is a mechanism that can be used to
establish communication between native language programs and the
Java virtual machine. The documentation for JNI and the technical
literature on JNI deal extensively with interactions between the
JVM and C/C++ code. The Java SDK even provides a utility to
generate a header file to facilitate calling C/C++ programs from
Java code. However, there is hardly any mention of Java and
assembly language code working together. In an "http://today.java.net/pub/a/today/2006/10/19/invoking-assembly-language-from-java.html">
earlier article
I showed how assembly language programs can be
called from Java applications. Here I deal with the technique for
invoking Java programs from an ASM process through a demo
application that calls a Java method from assembly language code.
The Java method brings up a Swing JDialog to show that
it has, indeed, been launched.

Why Java with ASM?

JNI is essential to the implementation of Java, since the JVM
needs to interact with the native platform to implement some of its
functionality. Apart from that, however, use of Java classes can
often be an attractive supplement to applications written in other
languages, as Java offers a wide selection of APIs that makes
implementation of advanced functions very simple.

Some time ago, I was associated with an application to collect
real-time data from a number of sources and save them in circular
buffers so that new data would overwrite old data once the buffer
got filled up. If a designated trigger event was sensed through a
digital input, a fixed number of data samples would be saved in the
buffers so that a snapshot of pre- and post-trigger data would be
available. The original application was written in assembly
language. After the application was used for a few months, it was
felt that it would be very useful to have the application mail the
snapshots to authorized supervisors whenever the trigger event
occurred. Of course, it would have been possible to write this
extension in assembly, but the team felt that in that particular
instance it was easier to write that extension in Java and hook it
up with the ASM program. As I had earlier worked with ASM-oriented
JNI, I knew this could be done and, indeed, the project was
implemented quickly and successfully.

I am sure there are many legacy applications written in assembly
language that could benefit from such add-ons. However, it is not
only for old applications in need of renovation that JNI can prove
useful. Although it may seem unlikely to some of us, assembly
language is still used for writing selected portions of new
programs. In an "http://developers.sun.com/solaris/articles/x86_assembly_lang.html">
article
published not very long ago, the author says, "I have
found that many of Sun's partners still use assembly language in
their products to ensure that hot code paths are as efficient as
possible. While compilers are able to generate much more efficient
code today, the resulting code still doesn't always compete with
hand-coded assembly written by an engineer that knows how to
squeeze performance out of each microprocessor instruction.
Assembly language remains a powerful tool for optimization,
granting the programmer greater control, and with judicious use can
enhance performance." Clearly, in such "mixed language"
applications the ability to use Java with ASM can be useful.

Note that the technique shown here can also be used to call Java
code from languages other than ASM. If JInvoke is
rewritten as a .dll, code written in FORTRAN, for instance,
can link to it and call a Java method.

I have used JNI with legacy ASM code in two ways:

  • Functional enhancement: Mail-enabling an existing ASM
    application, as mentioned earlier.
  • Interface enhancement: Adding interactive user interface
    (mostly AWT, but some Swing as well).

These enhanced applications have run on Windows 2000 and XP. The
Java versions used were 1.3, 1.4, and 1.6. In all cases the
applications worked smoothly.

The version of assembly language I have used for the demo code
is MASM32. The entire
MASM32 bundle is a free download, and if you are going to
experiment with Java-ASM interaction, you will need to have it on
your computer. A set of extremely useful tutorials on MASM
programming are available on "http://www.win32asm.cjb.net/">Iczelion's site. The definitive
work on JNI is Sheng Liang's book "http://java.sun.com/docs/books/jni/index.html">The Java Native
Interface: Programmer's Guide and Specification
. This too
is a free download. This article's Java code sample,
AsmToJava, will obviously need an "https://java.sun.com/j2se/1.4.2/download.html">SDK (or, at
least, a JRE) for execution. The assembly language part of the
demo, JInvoke, has been compiled into an .exe
file and will run even without the MASM bundle; the
assembler/linker is required only if you want to modify the source
code and recompile it.

The Basics

JNI provides a comprehensive interface to a JVM. This interface
is exposed primarily through a rich set of functions. Native code
can call these functions to interact with a JVM implementation.
These functions are described in detail in Sheng Liang's book.
While most of these functions can be accessed only after a JVM has
been created, JNI also directly exports a number of native
functions. As we shall see later, a function of the second type can
be used to instantiate a JVM, so that other JNI functions can be
called.

Once a JVM has been created, an assembly language program can
access those JNI functions that need a JVM instance to implement
their functionalities. Pointers to all these JNI functions are
stored in a table, the Function Table. When the ASM code
loads a JVM, it receives a variable named JNIEnv, which
is really a pointer. JNIEnv points to a memory
location that, in turn, contains the actual pointer to the
Function Table. This chain of access is shown in Figure 1.

<br "Access to JNI functions" />

Figure 1. Access to JNI functions

As we see, each of the pointers to JNI functions is four bytes
long. So the pointer to any given function can be found at the
location defined by adding four times the index for that function to
the starting address of the function table. The function indices
are zero-based--the pointer for the first function is at index 0,
that for the second is at index 1, and so on. In his book, Sheng
Liang lists the index values for all JNI functions.

To call a Java program, ASM code needs to execute the following
steps:

  • Instantiate a JVM
  • Locate the class
  • Get the ID of the method
  • Call the method

Before we look at these steps in detail, let us check out how we
can use an include file to simplify the task of writing this
and similar programs. As interactions between Java and native codes
take place through the functions exposed by JNI, it becomes
necessary for native processes to call these functions repeatedly.
So we would like to use macros to take care of these activities.
That would reduce the need for repeatedly writing similar (and
fairly long) code and would also reduce the chances of bugs getting
into the program through typos.

The Macros

Calling a function from ASM code involves getting the pointer to
the function from the Function Table and then calling the function
using that pointer. Getting the pointer, as we know, involves
following the JNIEnv pointer chain to obtain the
starting address of the Function Table and then retrieving the
pointer to the desired function by using the index of the function.
The first part--getting the starting address of the Function
Table--would use identical code every time and can be handled by
the following macro:


    ;This macro returns the pointer to 
    ;Function Table in fnTblPtr
    GetFnTblPtr MACRO envPtr, fnTblPtr
        mov ebx, envPtr
        mov eax, [ebx]
        mov fnTblPtr, eax
    ENDM

The code shown above defines a macro that takes two parameters.
The first is the JNIEnv pointer and the second points
to the location where the macro will return the pointer to Function
Table. The macro loads the pointer to Function Table into
eax, which is then saved in fnTblPtr. One
way of using this macro would be to define it within the program
itself. Another way, adapted here, is to define all such macros
within an include file, which can then be used with the ASM
program through an include statement. The include file
used here is jav_asm.inc. This file defines not only the
GetFnTblPtr macro, but also all the others that are
required for this example. In addition to the macros,
jav_asm.inc defines the prototype of the function that
creates a JVM as well as the structs that are to be used as
parameters with that function. Finally, java_asm.inc
assigns symbolic names to all JNI function indices to simplify
their use.

Once the pointer to the Function Table has been obtained, the
pointer to the desired function needs to be retrieved. The code for
this too will be the same every time except for the index. The
following macro performs this task:


    ;This macro returns the pointer 
    ;to a function in fnPtr.
        GetFnPtr MACRO fnTblPtr, index, fnPtr
                mov eax, index
                mov ebx, 4
                mul ebx
                mov ebx, fnTblPtr
                add ebx, eax
                mov eax, [ebx]
                mov fnPtr, eax
        ENDM

The macro multiplies the value of index by 4 and
adds the result to the starting address of Function Table
(available in fnTblPtr) to get the pointer to the
function we want to access. This pointer is then saved in
fnPtr.

The three remaining macros are almost identical; the only
difference is in the number of parameters handled:


    ;The next 3 macros push parameters as per 
    ;stdcall and call the function through fnPtr
        CallFunction2 MACRO param1, param2, fnPtr
                push param2
                push param1
                call [fnPtr]
        ENDM

        CallFunction3 MACRO param1, param2, param3, fnPtr
                push param3
                push param2
                push param1
                call [fnPtr]
        ENDM

        CallFunction4 MACRO param1, param2, param3, param4, fnPtr
                push param4
                push param3
                push param2
                push param1
                call [fnPtr]
        ENDM

As we see, these macros push the parameters (except
fnPtr) in reverse order as required for
stdcall, and then call the targeted function using
fnPtr as the pointer.

Now that the basic building blocks are in place, we can take a
look at the four-step sequence followed by our demo
application.

Creating a JVM Instance

JInvoke creates an instance of JVM more or less in
the same way as the java command does when we launch a
Java application from the command line. A Java virtual machine
implementation offers a mechanism known as the Invocation
Interface
that allows a native application to load the virtual
machine. The java command calls a C program that uses
the Invocation Interface to run a Java application, and
JInvoke uses this same interface. The code for loading
the JVM is given below:


    .
    .
    .
    va          vm_args &lt;&gt;
    jvmo        JavaVMOption &lt;&gt;

    .
    .
    .

    mov jvmo.optionString, offset opzero

    mov va.options, offset jvmo
    mov va.version, 00010002h
    mov va.nOptions,1
    mov va.ignoreUnrecognized, TRUE

    invoke JNI_CreateJavaVM, offset JavaVM, offset JNIEnv, 
        offset va

Here we first declare two structs. As we have already seen,
these structs are defined in jav_asm.inc. The creation of
a JVM requires that a number of parameters be specified. These are
passed to the JNI_CreateJavaVM function through the
structs.

In our example, we want to invoke the main method of
the AsmToJava class. On my computer, the class file is
in the C:\j2sdk1.4.2_05\testjni folder. The string
opzero defines this path in accordance with the method
described in the JNI specs. Note that opzero is loaded
into the struct jvmo, and then the offset to
jvmo is loaded into the struct va. The
last parameter passed to the JNI_CreateJavaVM function
is va and, therefore, the JVM that is loaded comes to
know where to find the class that we are interested in.

When JNI_CreateJavaVM returns, it signals success
by returning zero in eax and failure by returning a
negative number in eax. If the function succeeds in
creating the JVM instance, the pointer to the JVM interface and the
corresponding JNIEnv pointer are available in
JavaVM and JNIEnv, respectively.

On return from the JNI_CreateJavaVM function,
JInvoke checks the content of eax. If it
is not zero, the JVM has not been loaded. The user is informed of
this and the process exits:


    .if eax == 0
        .
        .
        .
    .else
        invoke MessageBox, 0, addr Fail1Text, 
            addr Caption, 16; failed to create JVM
    .endif 

On the other hand, if the content of eax is zero,
then a message box is shown (Figure 2) with the corresponding
message and the next step is executed:


    .if eax == 0
        invoke MessageBox, 0, addr VmText, 
            addr Caption, 64; indicate success

<br "Message to show that JVM has been loaded" />

Figure 2. Message to show that the JVM has been loaded

I must point out here that JInvoke uses the
simplest approach to JVM instantiation since it is meant to be a
vehicle for concept demonstration only. A number of additional
parameters can be specified, as is explained by Sheng Liang in his
book.

Locating the Class

After loading the JVM, JInvoke needs to locate the
class that is the entry point for the target Java application. The
following code calls the FindClass function to do
this:


    GetFnTblPtr JNIEnv, fntblptr
    GetFnPtr fntblptr, FI_FC, fnptr ; ptr to FindClass
    CallFunction2 JNIEnv, offset ProcName, 
        fnptr ; call FindClass

    .if eax != 0
        mov classid, eax
        invoke MessageBox, 0, addr FcText, 
            addr Caption, 64; class found

Note that the path to the class was earlier loaded into the
struct jvmo (mov jvmo.optionString, offset opzero) and
is already known to the JVM. If the FindClass function
is able to locate the class, it returns the ID in eax.
Otherwise it returns zero. Once the class is located, its ID is
saved and a message box so informs the user (Figure 3).

<br "Message to show that the class has been found" />

Figure 3. Message to show that the class has been
found

If the class cannot be located, then the process exits after
showing an appropriate message. Figure 4 is an example of a message
shown when a called function does not succeed.

<br "Message to show that the class could not be found" />

Figure 4. Message to show that the class could not be
found

Getting the Method ID

In order to call the method, the corresponding ID has to be
obtained. The function that returns the ID for a static method is
GetStaticMethodID. We use this function here since we
want to call the main method of the
AsmToJava class. The parameters for this function--in addition to JNIEnv--are:

  • The ID of the class to which the method belongs; this is the
    classid variable referred to in the previous
    step.
  • The name of the method; this is the string
    methodname.
  • The descriptor of the method that specifies the parameters for the
    method and its return type; the string methodsig is
    the descriptor for our target method. In this case, the parameter is
    a String array and the return type is
    void. Sheng Liang's book on the JNI specification shows
    how to create descriptors for methods and for variables too.

The call to GetStaticMethodID is very similar to
the other function calls we have seen so far:


    GetFnPtr fntblptr, FI_GSMID, fnptr ; ptr to GetStaticMethodID
    CallFunction4 JNIEnv, classid, offset methodname, 
        offset methodsig, fnptr ; GetStaticMethodID

    .if eax != NULL
        mov methid, eax
        invoke MessageBox, 0, addr GsmiText, addr Caption, 64

GetStaticMethodID returns the ID in
eax. If the attempt to obtain the method ID is
unsuccessful then NULL is returned instead. So JInvoke
checks the content of eax to determine whether to go
on to the next step (Figure 5) or exit the process.

<br "Message to show that method ID has been obtained" />

Figure 5. Message to show that method ID has been
obtained

Calling the Target Method

The JNI function that calls a static method with the return type
void (remember, we are calling the main
method) is CallStaticVoidMethod. The following code
gets the pointer to this function and calls it with the required
parameters:


    GetFnPtr fntblptr, FI_CSVM, fnptr ; get CallStVM ptr
    CallFunction3 JNIEnv, classid, methid, fnptr; call CallStVM

The Java application displays a dialog to show that it has been
successfully launched.

<br "Java method successfully invoked" />

Figure 6. Java method successfully invoked

Once the called Java method returns, JInvoke
exits.

<br "Message to show that the process is exiting" />

Figure 7. Message to show that the process is
exiting

When the ExitProcess function is called, all
threads in the process are stopped, so the Java threads created by
the process will also be stopped. Therefore, if this approach is
used to launch a Java program, care must be taken to ensure that
the called Java method returns only after completing all
required activities. Actually, the issues involved in terminating
the calling process and the JVM need careful attention. Please
refer to the "http://msdn2.microsoft.com/en-us/library/aa383750.aspx">Windows
API documentation
and the "http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html">latest
JNI specs
.

Conclusion

The approach shown here demonstrates the basic technique used
for launching Java applications from assembly language code.
Adequate error checking should be incorporated in native programs
to ensure safe interaction with the Java environment. Sheng Liang
provides many examples of checks including handling of exceptions
in native code. ASM programs working with JNI should use such error
checking methods wherever applicable.

The jav_asm.inc file provides an easy way of specifying
JNI function indices. The use of symbolic names instead of numbers
to specify an index is less likely to lead to errors. The use of
macros, too, is useful in reducing errors. You are free to use this
file with your code and also to modify it to suit your
requirements. However, make sure that the file is not redistributed
without the disclaimer.

When running JInvoke, you may get an error message
saying that jvm.dll cannot be located. If this happens, you
need to add the path to the directory containing jvm.dll
to your PATH environment variable. This DLL is
normally located in the jre\bin\client directory under the Java
SDK root folder. For instance, on my computer, the path is
C:\j2sdk1.4.2_05\jre\bin\client for the Java 1.4 release. If
you are using Java 1.3, however, the folder would be
jre\bin\classic. So make sure you've got the right path.

Finally, I would like to point out that the paths to the files
named in the include statements of
JInvoke would be determined by the directory structure
of your computer. The paths specified in JInvoke are
valid for my computer. As far as JInvoke.exe is concerned,
it will run even without any MASM component (specifically, any
.inc or .lib file) being loaded into your
system. If you want to modify the code and recompile it, you will
have to make sure that the path details correspond to the way your
directories are set up. The AsmToJava class file will
have to be loaded in the directory specified by
opzero. Alternately, opzero will have to
be changed to reflect the path to this class. In that case, the
source file for JInvoke will need to be recompiled,
keeping in mind the need to modify other path names as mentioned
above.

Resources


width="1" height="1" border="0" alt=" " />
Biswajit Sarkar is an electrical engineer with specialization in Programmable Industrial Automation. Biswajit is the author of "LWUIT 1.1 for Java ME Developers" published by PACKT Publishing.
Related Topics >> Programming   |