c3po
c3po

Reputation: 25

swig get return type from variable in struct as string array in java

For a small Java project I needed to interact with existing code written in C, so to make things easy (I'm not a C/C++ programmer unfortunately..) I decided to use swig.

The first problem I encountered: a C function that returned a NULL-delimited string resulted in wrapped code that returned a String, containing only the first value was solved by the 3(!) possible solutions Flexo provided in: SWIG get returntype from String as String array in java

While continuing development on this project I encountered a second problem with a (similar?) pattern that puzzles me: The headerfile contains a struct "PROJECTDETAILS" that (in turn) contains a variable itemList that (should) contain a NULL-delimited String. The swig generated getter for itemList returns the first result as a String, just like the GetProjects function in my original linked question (a String containing the first result) I've tried to apply the answer Flexo provided to this problem as wel, however I'm not able to "typemap" the itemList variable

The function in the C header file states:

typedef struct _PROJECT
{
    int version;
    unsigned char vM;
    unsigned char fM;
} * PROJECT;

typedef struct _PROJECTDETAILS
{
    int infoType;
    union
    {
        char *itemList;            /* Returns a NULL-delimited string */
        char *projectName;
    } info;
} PROJECTDETAILS;

DllImport PROJECT OpenProject dsproto((int, char *));
DllImport int GetProjectDetails dsproto((PROJECT, int, PROJECTDETAILS *));

Upvotes: 0

Views: 680

Answers (1)

To start out I wrote a dummy implementation of what I assume the semantics of the two functions you referenced are:

typedef struct _PROJECT
{
    int version;
    unsigned char vM;
    unsigned char fM;
} * PROJECT;

#define INFOTYPE_ITEMLIST 0
#define INFOTYPE_PROJECTNAME 1

typedef struct _PROJECTDETAILS
{
    int infoType;
    union
    {
        char *itemList;            /* Returns a NULL-delimited string */
        char *projectName;
    } info;
} PROJECTDETAILS;

static PROJECT OpenProject(int a, char *b) {
    (void)a;(void)b;
    static struct _PROJECT p = {100, 1, 2};
    return &p;   
}

static int GetProjectDetails(PROJECT p, int a, PROJECTDETAILS *out) {
    (void)p;
    // No idea what real impl does here
    if (a == 1) {
      out->infoType = INFOTYPE_ITEMLIST; 
      out->info.itemList="Item 1\0Item 2\0Item 3\0";
    }
    else {
      out->infoType = INFOTYPE_PROJECTNAME;
      out->info.projectName = "HELLO WORLD";
    }
    return 0;
}

This, combined with the answer I gave in my previous answer is sufficient to get the itemList member working, albeit in a clunky (for a Java developer's perspective) way.

To make use of the typemaps from the previous answer correctly all we need to do is figure out what to call them (or what to write for %apply). SWIG has a really handy way of helping us figure this out, the -debug-tmsearch command line argument that for every typemap search that happens prints all the candidates which are considered and ignored because nothing was entered for them. So I ran:

swig3.0 -java -Wall -debug-tmsearch test.i

Which then shows the ways we could match that with our typemaps from the previous answer.

test.h:16: Searching for a suitable 'out' typemap for: char *_PROJECTDETAILS::_PROJECTDETAILS_info::itemList
  Looking for: char *_PROJECTDETAILS::_PROJECTDETAILS_info::itemList
  Looking for: char *itemList
  Looking for: char *
  Using: %typemap(out) char *

Which shows that char *_PROJECTDETAILS::_PROJECTDETAILS_info::itemList is the tightest match for the PROJECTDETAILS::info::itemList member that we want to apply our previous typemap to. So we could just use the previous typemaps in this answer and match differently (or even use %apply to match them to multiple usages), something like:

%module test

%{
#include "test.h"
#include <assert.h>
%}

// See part 2 for discusson of these
%rename("%(strip:[_])s") "";
%immutable _PROJECTDETAILS::infoType;

%typemap(jni) char * _PROJECTDETAILS::_PROJECTDETAILS_info::itemList "jobjectArray";
%typemap(jtype) char * _PROJECTDETAILS::_PROJECTDETAILS_info::itemList "String[]";
%typemap(jstype) char * _PROJECTDETAILS::_PROJECTDETAILS_info::itemList "String[]";
%typemap(javaout) char * _PROJECTDETAILS::_PROJECTDETAILS_info::itemList {
  return $jnicall;
}
%typemap(out) char * _PROJECTDETAILS::_PROJECTDETAILS_info::itemList {
  size_t count = 0;
  const char *pos = $1;
  while (*pos) {
    while (*pos++); // SKIP
    ++count;
  }
  $result = JCALL3(NewObjectArray, jenv, count, JCALL1(FindClass, jenv, "java/lang/String"), NULL);
  pos = $1;
  size_t idx = 0;
  while (*pos) {
    jobject str = JCALL1(NewStringUTF, jenv, pos);
    assert(idx<count);
    JCALL3(SetObjectArrayElement, jenv, $result, idx++, str);
    while (*pos++); // SKIP
  }
  //free($1); // Iff you need to free the C function's return value
}

%include "test.h"

This picked method 2 from the previous answer, mainly because it was entirely typemap based, so a better example of the -debug-tmsearch SWIG argument.

That's enough that we can use it as:

import java.util.Arrays;

public class run {
  public static void main(String[] argv) {
    System.loadLibrary("test");
    PROJECT p = test.OpenProject(1,"???");
    PROJECTDETAILS pd1 = new PROJECTDETAILS();
    test.GetProjectDetails(p, 1, pd1);
    System.out.println(Arrays.toString(pd1.getInfo().getItemList()));
  }
}

But we can do much better than that for Java users, creating a new PROJECTDETAILS object just to pass in as the argument to GetProjectDetails is a little odd.


To wrap this neatly into Java there are quite a few things you want to do, besides just the member variable with unusual semantics for char *.

First up we probably want to rename some of the structs you've got. You can do that in SWIG 2.0 and above using the advanced renaming strip operator.

Next we need to decide how to wrap the members themselves. A typical design pattern in C is to use an int to indicate which member of a union is the correct type for a given object. In Python I'd just return a different type for each case and rely on duck-typing. For Java there are a couple of different options that would be sensible:

  1. You could define a class hierarchy and use instanceof (or just the int type) to figure out how to cast to the right type.
  2. You could leave it as is and mirror the C semantics. (Technically accessing the 'wrong' member is undefined behaviour, which isn't very intuitive for Java developers).
  3. You could return a type that raise an exception or return NULL instead if you try to access the 'wrong' member.

Option 2 is what the previous part of this answer did. From my perspective option 3 is probably the most predictable behaviour to a Java programmer, so that's what I've done here.

The third fun decision we need to make is how to handle the output function argument. In this instance I'm going to pick the solution that favours more Java code over more C/JNI, but the same trade off as with previous answers applies here too.

So what I did is to tell SWIG to ignore PROJECTDETAILS::info completely, as well as renaming the underscore prefixed structs.

Then I made the version of GetProjectDetails in the header file private and added the Impl suffix to show that it's not intended for anyone other than internals of the wrapper to touch it.

Inside the module itself I used %pragma to add another, public version of GetProjectDetails that hides the fact that a new object is constructed for an output only argument, changes the return type to return this. It also switches the 'use int to indicate success' C coding style into the Java 'throw an exception if it goes wrong' mechanism. (There are more ways in SWIG to do this than just like this, but it minimizes the C/JNI learning curve to do it like that).

Then we add two extra, read-only member variables to the wrapped PROJECTDETAILS struct. These don't really exist in C directly, so they're implemented in the intermediate interface glue code by some extra C code. The point of this code is that it checks what case the union is actually in, using the extra int member that indicates the type. If the type isn't right they return null (but either C or Java glue code could make that an exception instead).

All we have to do with that is then re-use the typemaps from my previous answer to get the itemList semantics working right across the language boundary. Again I used method 2 here, other than the minor issue with functions returning null it's unchanged.

%module test

%{
#include "test.h"
#include <assert.h>
%}

%rename("%(strip:[_])s") "";
%immutable _PROJECTDETAILS::infoType;
%ignore info; // Ignore the member
%ignore _PROJECTDETAILS_info; // Ignore the anonymous type
%javamethodmodifiers GetProjectDetails "private";
%rename(GetProjectDetailsImpl) GetProjectDetails;

%typemap(jni) char *_PROJECTDETAILS::itemList "jobjectArray";
%typemap(jtype) char *_PROJECTDETAILS::itemList "String[]";
%typemap(jstype) char *_PROJECTDETAILS::itemList "String[]";
%typemap(javaout) char *_PROJECTDETAILS::itemList {
  return $jnicall;
}
%typemap(out) char *_PROJECTDETAILS::itemList {
  if (!$1) return NULL; // This fixes a possible bug in my previous answer
  size_t count = 0;
  const char *pos = $1;
  while (*pos) {
    while (*pos++); // SKIP
    ++count;
  }
  $result = JCALL3(NewObjectArray, jenv, count, JCALL1(FindClass, jenv, "java/lang/String"), NULL);
  pos = $1;
  size_t idx = 0;
  while (*pos) {
    jobject str = JCALL1(NewStringUTF, jenv, pos);
    assert(idx<count);
    JCALL3(SetObjectArrayElement, jenv, $result, idx++, str);
    while (*pos++); // SKIP
  }
  //free($1); // Iff you need to free the C function return value
}

%pragma(java) modulecode=%{
  public static PROJECTDETAILS GetProjectDetails(PROJECT p, int a) {
    PROJECTDETAILS out = new PROJECTDETAILS();
    final int ret = GetProjectDetailsImpl(p,a,out);
    if (0!=ret) {
      // assuming this is an error throw something
    }
    return out;
  }
%}

%extend _PROJECTDETAILS {
  const char *itemList const {
    if ($self->infoType != INFOTYPE_ITEMLIST) {
     // Throw a Java exception here instead? That is another question...
     return NULL;
    }
    return $self->info.itemList;
  }
  const char *projectName const {
    if ($self->infoType != INFOTYPE_PROJECTNAME) {
      // Throw exception?
      return NULL;
    }
    return $self->info.projectName;
  }
}

%include "test.h"

Which then works with:

import java.util.Arrays;

public class run {
  public static void main(String[] argv) {
    System.loadLibrary("test");
    PROJECT p = test.OpenProject(1,"???");

    System.out.println("PD1");
    PROJECTDETAILS pd1 = test.GetProjectDetails(p, 1);
    System.out.println(Arrays.toString(pd1.getItemList()));
    System.out.println(pd1.getProjectName());

    System.out.println("PD2");
    PROJECTDETAILS pd2 = test.GetProjectDetails(p, 2);
    System.out.println(Arrays.toString(pd2.getItemList()));
    System.out.println(pd2.getProjectName());
  }
}

(Note: anything that starts with _ followed by a capital letter is a reserved name, probably not your fault, but not exactly great C)

Upvotes: 1

Related Questions