Interop mini-series – Calling C and C++ Callbacks from Java (Part 4)


Continuing from the last post, I will show how we can use JNA to allow C (and C++) code to invoke callback functions implemented in pure Java.

In case you are a bit rusty on callbacks, I have a whole series of posts on that, as part of this series. Feel free to check those out, starting with Callbacks.

Let’s jump right into it then!

Contents

  1. Demo
  2. Conclusion

Demo

For this demo, let’s pick a very simple example. (This is the same example as covered in the post covering Common Lisp callbacks from C/C++ code.).

We have a person type which has the following slots/fields – name, gender, and age. From our Java code, we want to instantiate an instance of person, and then use a function in a native library, prefix_name to append either “Mr.” or “Miss” in front of the person’s name, depending on the value of the gender slot (0 for female, anything else for male).

First we define the interface for the native library (in callback_demo.h):

#ifndef __CALLBACK_DEMO_H__
#define __CALLBACK_DEMO_H__ "callback_demo.h"

typedef struct person {
    char* name;
    int gender;
    int age;
} person;

#ifdef __cplusplus
extern "C" {
#endif
    void prefix_name(person*, void (*)(person*));
#ifdef __cplusplus
}
#endif
#endif

We then write the code containing the prefix_name function that will invoke our callback function (in callback_demo.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "callback_demo.h"

#define MAXSIZE 50

char* concatenate_names(const char* prefix, char* name)
{
    int len = strlen(prefix) + strlen(name) + 1;

    char* full_name = (char*)malloc(len * sizeof(char));

    if (full_name != NULL) {
        char* cp = full_name;

        while (*prefix != '\0')
            *cp++ = *prefix++;

        *cp++ = 0x20;

        while (*name != '\0')
            *cp++ = *name++;
   
         *cp = '\0';

        return full_name;
    }
   return name;
}
   

void prefix_name(person* p, void (*cb)(person*))
{
    const char* MISTER = "Mr.";
    const char* MISS = "Ms.";
    char* res = NULL;

    // 0 - female, anything else male
    res = p->gender == 0 ? concatenate_names(MISS, p->name) :
                           concatenate_names(MISTER, p->name);
    strcpy(p->name, res);
    
    (*cb)(p);
}

void sample_callback(person* p)
{
    printf("%s, %s, %d\n", p->name, p->gender == 0 ? "Female" : "Male", p->age);
}

int main()
{
    person rich;

    rich.name = (char*)malloc(MAXSIZE * sizeof(char));
    strcpy(rich.name, "Rich");
    rich.gender = 1;
    rich.age = 49;

    prefix_name(&rich, &sample_callback);
    
    return 0;
}

Explanation: The code is relatively straightforward. As can be seen from the header file, prefix_name is the entry point to the library (and the one which gets invoked from the Java code).

The prefix_name function takes an instance of the person structure as well as a callback function. Note the signature of the callback function:

void (*)(person*).

This callback function expects to be passed a modified instance of the person instance that is the first parameter of the prefix_name function.

The logic is very simple – simply check for the gender field, and then depending on whether it is 0 or some other way, update the name field of the person instance by prepending “Miss” or “Mr.” respectively.

Finally, the callback function cb is invoked, passing control back to the client code.

All right, now we compile the code into a library, libcallbackdemo.dylib:

Timmys-MacBook-Pro:Demo z0ltan$ clang -dynamiclib -o libcallbackdemo.dylib callback_demo.c

Timmys-MacBook-Pro:Demo z0ltan$ ls
callback_demo.c		callback_demo.h		libcallbackdemo.dylib

Excellent!

The Java code to plug into the native libraries surprisingly concise and simple (in CallbackDemo.java):

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Callback;
import com.sun.jna.Platform;
import com.sun.jna.Structure;

import java.util.List;
import java.util.Arrays;

public class CallbackDemo {
    private static final String lib;

    static {
        if (Platform.isMac())
            lib = "libcallbackdemo.dylib";
        else if (Platform.isWindows())
            lib = "libcallbackdemo.dll";
        else
            lib = "libcallbackdemo.so";
    }

    public interface CallbackDemoLib extends Library {
        CallbackDemoLib INSTANCE = (CallbackDemoLib)Native
                                    .loadLibrary(lib, CallbackDemoLib.class);

        interface PrefixCallback extends Callback {
            void print(Person person);
        }

        void prefix_name(Person person, PrefixCallback func);
    }

    public static class Person extends Structure {
       public String name;
       public int gender;
       public int age;

        @Override
        public List<String> getFieldOrder() {
            return Arrays.asList(new String[] {"name", "gender", "age"});
        }
    }

    public static void main(String[] args) {
        CallbackDemoLib.PrefixCallback callback = new CallbackDemoLib.PrefixCallback() {
                                                        @Override
                                                        public void print(Person person) {
                                                            System.out.format("Name: %s, Gender: %s, Age: %d\n",
                                                                person.name, person.gender == 0 ? "Female" : "Male",
                                                                person.age);
                                                        }
                                                    };

        Person rich = new Person();
        rich.name = "Rich";
        rich.gender = 1;
        rich.age = 49;

        Person vigdis = new Person();
        vigdis.name = "Vigdis";
        vigdis.gender = 0;
        vigdis.age = 28;

        CallbackDemoLib.INSTANCE.prefix_name(rich, callback);
        CallbackDemoLib.INSTANCE.prefix_name(vigdis, callback);
    }
}

And the output:

Timmys-MacBook-Pro:Java-to-C z0ltan$ javac -cp "./:./jna.jar" JavaToC.java

Timmys-MacBook-Pro:Java-to-C z0ltan$ java -cp "./:./jna.jar" JavaToC
System information:
Arch: x86_64, Model: MacBookPro11,2, Memory: 16GB, CPUs: 8, Logical CPUs: 8

10 25 -100 199 0 1 1 98 99 100 
-100 0 1 1 10 25 98 99 100 199 

Perfect! Now let’s do a brief rundown on the Java code.

Explanation: The modus operandi is very similar to that used for normal interaction with native libraries.

We define an interface CallbackDemoLib that holds proxies for the native functions in the libcallbackdemo.dylib shared library. This library exposes a single function prefix_name that expects a pointer to a person struct instance, and a callback function.

To implement the callback function, we declare another interface PrefixCallback that contains a single method print. Note that this name can be any name that you desire. The only contract is that the signature of this method should match that of the callback defined in the native function.

The native prefix_name function expects a callback that takes a pointer to a person instance. In Java, this maps nicely to a Person class. This class does have to extend the JNA class Structure.

The Person class, which maps exactly onto the native person struct needs to have one overridden method getFieldOrder(). This is important because other JNA cannot determine the layout of the object to map onto the native struct. So, in this example, we take care to ensure that the field order is specified exactly in this order – name, gender, and age. What would happen if we had mixed the order around? Let’s try that and see.

First let’s swap out the name and age fields:

public static class Person extends Structure {
       public String name;
       public int gender;
       public int age;

        @Override
        public List<String> getFieldOrder() {
            return Arrays.asList(new String[] {"age", "gender", "name"});
        }
    }

And the output:

Timmys-MacBook-Pro:C-to-Java z0ltan$ java -cp "./:./jna.jar" CallbackDemo
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007fff94886132, pid=1232, tid=2823
#
# JRE version: Java(TM) SE Runtime Environment (9.0+131) (build 9-ea+131)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (9-ea+131, mixed mode, tiered, compressed oops, g1 gc, bsd-amd64)
# Problematic frame:
# C  [libsystem_c.dylib+0x1132]  strlen+0x12
#
# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/z0ltan/Rabota/Blogs/Cffi/JNA/C-to-Java/hs_err_pid1232.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
Abort trap: 6

It crashed the whole darned JVM! Hmmm… before we speculate on what’s happening, let’s try another variation – swapping the gender and age fields:

public static class Person extends Structure {
       public String name;
       public int gender;
       public int age;

        @Override
        public List<String> getFieldOrder() {
            return Arrays.asList(new String[] {"name", "age", "gender"});
        }
    }

And the output:

Timmys-MacBook-Pro:C-to-Java z0ltan$ java -cp "./:./jna.jar" CallbackDemo
Name: Mr. Rich, Gender: Male, Age: 49
Name: Mr. Vigdis, Gender: Female, Age: 28

Eh? So what’s going on here? My deduction is that this is because of the memory layout that JNA needed to do to accommodate the native person struct. In the first case, we swapped out two fields of different types whereas in the second case, the swapped fields were the same size, and so it did not affect the memory layout – it was still in the same order – String, int, and int.

So it’s appears like the name of the fields don’t matter as much as the actual data types they represent (makes sense) even if JNA does do checks to ensure we can’t give names different from the actual field names. To test out this hypothesis, let’s try out one more variation and see if that crashes the JVM again – let’s swap out name and gender instead this time:

public static class Person extends Structure {
       public String name;
       public int gender;
       public int age;

        @Override
        public List<String> getFieldOrder() {
            return Arrays.asList(new String[] {"gender", "name", "age"});
        }
    }

And the output?

Timmys-MacBook-Pro:C-to-Java z0ltan$ java -cp "./:./jna.jar" CallbackDemo
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007fff94886132, pid=1247, tid=2823
#
# JRE version: Java(TM) SE Runtime Environment (9.0+131) (build 9-ea+131)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (9-ea+131, mixed mode, tiered, compressed oops, g1 gc, bsd-amd64)
# Problematic frame:
# C  [libsystem_c.dylib+0x1132]  strlen+0x12
#
# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/z0ltan/Rabota/Blogs/Cffi/JNA/C-to-Java/hs_err_pid1247.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
Abort trap: 6

And just taking a peek at the crash file:

---------------  S U M M A R Y ------------

Command Line: CallbackDemo

Host: MacBookPro11,2 x86_64 2200 MHz, 8 cores, 16G, Darwin 15.6.0
Time: Wed Sep  7 10:41:53 2016 IST elapsed time: 0 seconds (0d 0h 0m 0s)

---------------  T H R E A D  ---------------

Current thread (0x00007f9e3080c000):  JavaThread "main" [_thread_in_native, id=2823, stack(0x000070000011a000,0x000070000021a000)]

Stack: [0x000070000011a000,0x000070000021a000],  sp=0x0000700000218b00,  free space=1018k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [libsystem_c.dylib+0x1132]  strlen+0x12
C  [libcallbackdemo.dylib+0xcc6]  concatenate_names+0x26
C  [libcallbackdemo.dylib+0xe28]  prefix_name+0x68
C  [jna5393012570626767002.tmp+0xe134]  ffi_call_unix64+0x4c
C  0x00007000002195c8

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  com.sun.jna.Native.invokeVoid(JI[Ljava/lang/Object;)V+0
j  com.sun.jna.Function.invoke([Ljava/lang/Object;Ljava/lang/Class;Z)Ljava/lang/Object;+29
j  com.sun.jna.Function.invoke(Ljava/lang/reflect/Method;[Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Object;Ljava/util/Map;)Ljava/lang/Object;+249
j  com.sun.jna.Library$Handler.invoke(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;+348
j  com.sun.proxy.$Proxy0.prefix_name(LCallbackDemo$Person;LCallbackDemo$CallbackDemoLib$PrefixCallback;)V+20
j  CallbackDemo.main([Ljava/lang/String;)V+63
v  ~StubRoutines::call_stub

siginfo: si_signo: 11 (SIGSEGV), si_code: 1 (SEGV_MAPERR), si_addr: 0x0000000000000000

Indeed – it looks like the strlen is being attempted on an int! No wonder it crashed and burnt.

However, I have a suspicion that this behaviour might again differ depending on the platform. So the takeaway here is – always use the field names (with the correct types) in the same order as in the native struct.

Conclusion

Top

The JNA library is extremely useful because it is pure Java (unlike JNI, which is hardly convenient to use). In addition, as we have seen in the last post and in the current post, the APIs for JNA are very well-designed indeed.

I would recommend exploring further using the resources mentioned in the References section of the last post.

Next up, a small mini-project as the conclusion of this interop mini-series – a less-than-trivial essay at embedding a JVM instance within Common Lisp! Stay tuned.

Advertisements
Interop mini-series – Calling C and C++ Callbacks from Java (Part 4)

Speak your mind!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s