Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

Use native methods to expand the Java environment

Learn how to interact with libraries and applications written in other languages

  • Print
  • Feedback

Page 7 of 7

So now assume that we could access the correct interface and open the serial port. The question now is: How do you set baud rate and parity? You can use native methods until the Java VM supports device drivers.

The following steps can be used to build an application in Java that wishes to use some of the abilities of the underlying operating system that are not directly available to the Java VM. We are only going to cover the points that are germane to the serial port interface we are going to build.

  1. Design the application and clearly define the interface boundary. For example, if you are going to design an interface to a serial device, you need to decide what you can and can't do from the Java environment. Don't just assume that whenever you need to talk to a device, you need native methods. If you just want to read and write bytes to the serial port without changing the baud rate you can do this without native methods on some of the Java VMs. However, if you need to set baud rate and parity, then you would have to use native methods.

  2. Write the Java application and declare the native methods. The example we'll work through in this section goes through these methods in greater detail.

  3. Compile the code to produce a class file.

  4. Using the class file produced in the previous step, create an h file by executing javah -jni "classname", open the h file in an editor, and copy and paste the signature definitions from the .h file to your C file.

  5. Using the signatures generated in step 4, write the native code that will implement the desired functionality. To access the method arguments supplied on the Java method call, you need to use the JNI support functions for accessing the arguments.

  6. Create a shared library that contains your program. You can verify that the names you are using are going to be found at runtime by running the following utilities:

    In Unix, run:

    • dump -- This utility dumps the symbol information in Unix libraries.

    • nm -- An earlier version of dump.

    • LD -- In combination with debug, LD provides a description of actions being taken with libraries.

    • truss -- Dynamic runtime without need to start a debug version.


    In Windows 95/NT, run:

    • dumpbin -- This utility displays everything you ever wanted to see about a DLL.


  7. Test and debug your program. If you get an unsatisfied linker exception, your paths or your method signatures are incorrect.


Whenever you implement a native method interface, it is important that you try to reuse existing classes. Serial devices present us with a pretty clear-cut choice for determining what existing classes might be of help. Because we want to write and read to streams, we should model the output to look like a stream. Therefore, it would make sense for us to use the Java classes that implement stream interfaces. Specifically, we're going to extend Java.ion.InputStream and Java.io.OutputStream.

Luckily for us, these classes were originally designed in quite a clever fashion; you need only replace a few methods to reuse all of the existing code. So with very little work on our part, we have SerialInputStream.java and SerialOutputStream.java. Both of these new classes require that the constructor have a PlainSerial object. The PlainSerial class provides the interface, which we'll call the Serial Device interface. The Serial Device interface has been abstracted for all serial devices supporting the basic read and write of single bytes, with no time or pacing requirements. Serial devices can have very demanding requirements, which are found most often in real-time environments. For example, some serial lines my require paced output or rapid response within given and predictable time frames. (The Resources section of this article provides several links to articles that discuss real-time issues with Java.)

As you might expect, we need to implement open, close, read, and write methods for the PlainSerial class. The SerialInputStream and SerialOutputStream classes use the native methods shown next by calling the appropriate method.

public class PlainSerial {

public int openResult = -1;
private final native int OpenDevice( String device, int baud);
private native void writeAByte( int theFd, int theByte);
private final native int readAByte( int theFd );
private final native void close( int theFd );
public void write( int b ) { writeAByte( openResult, b); }
public void close() { close(openResult); } public int read( ) { return ( readAByte( openResult ) ); } /** * Open the device by name at the requested baud rate */
public PlainSerial(String device, int baud) throws IOException { System.out.println("About to call open"); openResult = OpenDevice(device, baud); System.out.println("Open result:" + openResult ); if ( openResult < 0 ) { switch (openResult) { default: throw new IOException("Error Opening Device " + openResult); } } } }


Note that the JNI requires the signatures of the native methods to match the name of the calling class and package. For this reason, you see PlainSerial twice in the signature. The C code does the actual interfacing to the underlying operating system's serial file. The following code segment demonstrates the method signatures required to interface PlainSerial to the underlying operating system.

/**
 * The descriptor for the device is returned, if the
 * device can be opened. If it cannot then a -1 error
 * is returned.
 * Class:     Java_commerce_PlainSerial_PlainSerial
 * Method:    OpenDevice
 * Signature: (Ljava/lang/String;I)I
 */
JNIEXPORT jint JNICALL Java_Java_commerce_PlainSerial_PlainSerial_OpenDevice
  (JNIEnv *env , jobject obj, jstring tty, jint lBaud) {
/*
 * Write a single byte
 * Class:     Java_commerce_PlainSerial_PlainSerial
 * Method:    writeAByte
 * Signature: (II)V
 */
JNIEXPORT void JNICALL Java_Java_commerce_PlainSerial_PlainSerial_writeAByte
  (JNIEnv *env, jobject obj, jint theFd, jint theByte) {
/*
 * Read a single byte
 * Class:     Java_commerce_PlainSerial_PlainSerial
 * Method:    readAByte
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_Java_commerce_PlainSerial_PlainSerial_readAByte
  (JNIEnv *env, jobject obj, jint theFd) {


Complete the interface to the operating system using the following guidelines:

  • On the Unix target, use the open and ioctl calls.

  • On the Windows 95/NT target, use the CreateFile methods with SetCommState described in the Windows 95/NT answerbooks for the IDE you are using.


Beyond time-out
Because serial devices often do not respond (either they're not connected, not turned on, or something else is plugged in into the port), time-out support alone is not enough. To address this situation head on, I have also incorporated a bounded buffer, which provides the ability to read with a maximum limit on the wait time. Although I could have implemented this buffer by using the underlying operating system, I chose not to because that functionality can be performed in Java.

Much of the design and implementation for SynchronizedBuffer comes from Concurrent Programming in Java -- Design Principles and Patterns by Doug Lea. Here's how it works: A thread (producer) continuously reads the input device and when characters arrive they are given to the SynchonizedBuffer object using synchronized buffers to implement controlled access. We can't have the producer (the thread getting the bytes from the serial port) and the consumer (the application requesting the data) updating the counters at the same time. We also use wait and notify so that the consumer is informed whenever there is data from the producer. Be sure to thoroughly examine SynchronizedBuffer.java, SerialInputStreamThreaded.java, and SerialThreadReader.java for all the details. You can view the entire serial package here.

Conclusion

If you've made it to this point, congratulations! You've digested quite a lot of information. You should now be comfortable (well, maybe with some more practice) with calling Java from C and calling C from Java. Remember you need not wait (or be enslaved) by the Java VM if it doesn't allow you to get the job done. However, once you take the plunge with native methods, you should be aware of several disadvantages:

  • If you use native methods, you're not 100% Pure.

  • You must maintain multiple versions of your apps for the various target systems that your users will be using. This means serial drivers for Windows 95/NT, Unix, and Macs ... and, oh yes, the NC. Your code will not be portable.

  • Native methods require more programming skill and time to get them right.

  • A malicious attack against your program could introduce a security risk by replacing the native method code.

  • It is easier to crash a system with native methods due to a bug.


This article lays the foundation for my upcoming smart card series. In order to use smart cards, you need to talk to them, and one of the cheaper ways of talking to them is with a serial card reader; thus, the need for serial device support. This series of articles will cover the techniques for using a smart card with your applications. We'll work closely with the Java Electronic Commerce Framework (JECF), which I will use as the foundation layer for the examples we'll build.

About the author

Rinaldo Di Giorgio is a staff engineer for Sun Microsystems in New York City. He currently is working on the use of smart cards in solving business problems.

Read more about Core Java in JavaWorld's Core Java section.

  • Print
  • Feedback

Resources