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 it returns 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 = 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 return a FileDescriptor that 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 jobject JNICALL Java_Example_prepare_1fd(JNIEnv *env, jclass _ignore, jstring filename) {
  jfieldID field_fd;
  jmethodID const_fdesc;
  jclass class_fdesc, class_ioex;
  jobject ret;
  int fd;
  char *fname;

  class_ioex = (*env)->FindClass(env, "java/io/IOException");
  if (class_ioex == NULL) return NULL;
  class_fdesc = (*env)->FindClass(env, "java/io/FileDescriptor");
  if (class_fdesc == NULL) return NULL;

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

  fd = open(fname, O_RDWR | O_NONBLOCK);

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

  if (fd < 0) {
    // open returned an error. Throw an IOException with the error string
    char buf[1024];
    sprintf(buf, "open: %s", strerror(errno));
    (*env)->ThrowNew(env, class_ioex, buf);
    return NULL;
  }

  // construct a new FileDescriptor
  const_fdesc = (*env)->GetMethodID(env, class_fdesc, "<init>", "()V");
  if (const_fdesc == NULL) return NULL;
  ret = (*env)->NewObject(env, class_fdesc, const_fdesc);

  // poke the "fd" field with the file descriptor
  field_fd = (*env)->GetFieldID(env, class_fdesc, "fd", "I");
  if (field_fd == NULL) return NULL;
  (*env)->SetIntField(env, ret, field_fd, fd);

  // and return it
  return ret;

}