Sharing file descriptors between Java and Native code

Sometimes you can't use Java to open a file. In my case, I needed to open a serial port in non-blocking mode and set the communications parameters. You can't really do that with Java, unfortunately. So you need to open the device within the context of a JNI method (in C). But once you open the file.... then what? How do you get a Java object, such as FileInputStream to use that open file descriptor?

Java does have a FileDescriptor class. At first glance, that would seem to be the answer. The only trouble is, there is only one constructor, and it gives you an 'invalid' one. Why did they bother?

The answer is that just because there is no apparent way to get a FileDescriptor to point to an arbitrary open file, that doesn't mean there is no way to do it.

Set up your native method in the usual manner, but make sure one of the arguments is a FileDescriptor. Something like this:


public class myDevice {
  private native static prepare_fd(String filename, FileDescriptor fdobj);

  private FileInputStream in, out;

  public myDevice() {
    FileDescriptor myDev = new FileDescriptor();
    prepare_fd("/dev/tty.modem", myDev);
    in = new FileInputStream(myDev);
    out = new FileOutputStream(myDev);
  }
}

So far, so good. We now have the Java side set up to call our native code, which will modify the FileDescriptor so that it refers to the device we will be opening.

I'm not going to detail the vagueries of JNI and how you write it. The trick in prepare_fd is to just assume that the FileDescriptor object has an integer field called fd. If you stuff the number you get from the open() system call, or from calling fileno() on a FILE *, it will magically work.

Here's an example:

JNIEXPORT void JNICALL Java_Example_prepare_1fd(JNIEnv *env, jclass _ignore, jstring filename, jobject fdobj) {
  jfieldID field_fd;
  jclass class_fdesc;
  int fd;
  char *fname;

  fname = (*env)->GetStringUTFChars(env, filename, NULL);

  fd = open(fname, O_RDWR | O_NONBLOCK);

  (*env)->ReleaseStringUTFChars(env, filename, fname);

  class_fdesc = (*env)->GetObjectClass(env, fdobj);
  field_fd = (*env)->GetFieldID(env, class_fdesc, "fd", "I");
  (*env)->SetIntField(env, fdobj, field_fd, fd);

}