You can achieve what you’re looking for with SWIG+Java using “Directors”, however it’s not quite as straightforward mapping from the C++ abstract classes onto Java as you might hope. My answer therefore is split into three parts – firstly the simple example of implementing a C++ pure virtual function in Java, secondly an explanation of why the output is like that and thirdly a “work-around”.
Implementing a C++ interface in Java
Given a header file (module.hh
):
#include <string>
#include <iosfwd>
class Interface {
public:
virtual std::string foo() const = 0;
virtual ~Interface() {}
};
inline void bar(const Interface& intf) {
std::cout << intf.foo() << std::endl;
}
We’d like to wrap this and make it work intuitively from the Java side. We can do this by defining the following SWIG interface:
%module(directors="1") test
%{
#include <iostream>
#include "module.hh"
%}
%feature("director") Interface;
%include "std_string.i"
%include "module.hh"
%pragma(java) jniclasscode=%{
static {
try {
System.loadLibrary("module");
} catch (UnsatisfiedLinkError e) {
System.err.println("Native code library failed to load. \n" + e);
System.exit(1);
}
}
%}
Here we’ve enabled directors for the whole module, and then requested they be used for class Interface
specifically. Other than that and my favourite “load the shared object automatically” code there’s nothing particularly noteworthy. We can test this with the following Java class:
public class Run extends Interface {
public static void main(String[] argv) {
test.bar(new Run());
}
public String foo() {
return "Hello from Java!";
}
}
We can then run this and see it’s working as expected:
ajw@rapunzel:~/code/scratch/swig/javaintf > java Run
Hello from Java!
If you’re happy with it being neither abstract
nor an interface
you can stop reading here, directors do everything you need.
Why does SWIG generate a class
instead of an interface
?
SWIG has however made what looked like an abstract class into a concrete one. That means on the Java side we could legally write new Interface();
, which makes no sense. Why does SWIG do this? The class
isn’t even abstract
, let alone an interface
(See point 4 here), which would feel more natural on the Java side. The answer is twofold:
- SWIG supplies mechanics for calling
delete
, manipulating thecPtr
etc. on the Java side. That couldn’t be done in aninterface
at all. -
Consider the case where we wrapped the following function:
Interface *find_interface();
Here SWIG knows nothing more about the return type than that it’s of type
Interface
. In an ideal world it would know what the derived type is, but from the function signature alone there’s no way for it to figure this out. This means that in the generated Java somewhere there’s going to have to be a call tonew Interface
, which wouldn’t be possible/legal ifInterface
were abstract on the Java side.
Possible workaround
If you were hoping to provide this as an interface in order to express a type hierarchy with multiple inheritance in Java this would be quite limiting. There’s a workaround however:
-
Manually write the interface as a proper Java interface:
public interface Interface { public String foo(); }
-
Modify the SWIG interface file:
- Rename the C++ class
Interface
to beNativeInterface
on the Java side. (We ought to make it visible only to the package in question too, with our wrapped code living in a package of its own to avoid people doing “crazy” things. - Everywhere we have an
Interface
in C++ code SWIG will now be usingNativeInterface
as the type on the Java side. We need typemaps to map thisNativeInterface
in function parameters onto theInterface
Java interface we added manually. - Mark
NativeInterface
as implementingInterface
to make the Java side behaviour natural and believable to a Java user. - We need to supply a little bit of extra code that can act as a proxy for things which implement the Java
Interface
without being aNativeInterface
too. - What we pass to C++ must always be a
NativeInterface
still, not allInterface
s will be one though (although allNativeInterfaces
will), so we provide some glue to makeInterface
s behave asNativeInterfaces
, and a typemap to apply that glue. (See this document for a discussion of thepgcppname
)
This results in a module file that now looks like:
%module(directors="1") test %{ #include <iostream> #include "module.hh" %} %feature("director") Interface; %include "std_string.i" // (2.1) %rename(NativeInterface) Interface; // (2.2) %typemap(jstype) const Interface& "Interface"; // (2.3) %typemap(javainterfaces) Interface "Interface" // (2.5) %typemap(javain,pgcppname="n", pre=" NativeInterface n = makeNative($javainput);") const Interface& "NativeInterface.getCPtr(n)" %include "module.hh" %pragma(java) modulecode=%{ // (2.4) private static class NativeInterfaceProxy extends NativeInterface { private Interface delegate; public NativeInterfaceProxy(Interface i) { delegate = i; } public String foo() { return delegate.foo(); } } // (2.5) private static NativeInterface makeNative(Interface i) { if (i instanceof NativeInterface) { // If it already *is* a NativeInterface don't bother wrapping it again return (NativeInterface)i; } return new NativeInterfaceProxy(i); } %}
- Rename the C++ class
Now we can wrap a function like:
// %inline = wrap and define at the same time
%inline %{
const Interface& find_interface(const std::string& key) {
static class TestImpl : public Interface {
virtual std::string foo() const {
return "Hello from C++";
}
} inst;
return inst;
}
%}
and use it like:
import java.util.ArrayList;
public class Run implements Interface {
public static void main(String[] argv) {
ArrayList<Interface> things = new ArrayList<Interface>();
// Implements the interface directly
things.add(new Run());
// NativeInterface implements interface also
things.add(test.find_interface("My lookup key"));
// Will get wrapped in the proxy
test.bar(things.get(0));
// Won't get wrapped because of the instanceOf test
test.bar(things.get(1));
}
public String foo() {
return "Hello from Java!";
}
}
This now runs as you’d hope:
ajw@rapunzel:~/code/scratch/swig/javaintf > java Run
Hello from Java!
Hello from C++
And we’ve wrapped an abstract class from C++ as an interface in Java exactly as a Java programmer would expect!