Chad's Blog

Registering native methods when calling Java from SWIG

Posted in C++, Java, SWIG by chadretz on November 25, 2009

I am working on a project where my DLL is loaded as a dynamic plugin in a third party application. I wanted to use Java to accomplish my task so I chose to use SWIG with the Invocation API. Luckily, I can guarantee a single execution platform: Windoze. I am no C++ expert, but here’s how I accomplished this…

I will put all my JARs on the same path as the DLL, so I grabbed the current DLL directory using the HMODULE in the DllMain like so:

wchar_t* filename = new wchar_t[300];
GetModuleFileName((HMODULE) hModule, filename, 300);
_dllDirectory = filename;
_dllDirectory = _dllDirectory.substr(0, _dllDirectory.find_last_of('\\') + 1);

I also need all JARs in this directory, so I created a method:

string get_jars(wstring dir)
{
    WIN32_FIND_DATA findFileData;
    HANDLE hFind;
    hFind = FindFirstFile((dir + str_to_wstr("*.jar")).c_str(), &findFileData);
    if (hFind == INVALID_HANDLE_VALUE) {
        return "";
    } else {
        string str_dir = wstr_to_str(dir);
        string ret = str_dir + wstr_to_str(findFileData.cFileName);
        while(FindNextFile(hFind, &findFileData)) {
            //since we're on windows, this is the separator
            ret += ';';
            ret += str_dir + wstr_to_str(findFileData.cFileName);
        }
        FindClose(hFind);
        return ret;
    }
}

Now that I can get all JARs, I instantiate the JVM in a constructor of my “management” class. I have to first load the JVM DLL because it won’t be loaded by default. I use the JAVA_HOME environment variable to locate the DLL. The code is similar to the following:

MyNS::MyClass(wstring dllDirectory)
{
    this->_libInst = NULL;
    this->_jvm = NULL;
    this->_delegate = NULL;
    //create vm
    //get dll location
    string env = getenv("JAVA_HOME");
    if (file_exists((env + "\\bin\\client\\jvm.dll").c_str())) {
        env += "\\bin\\client\\jvm.dll";
    } else if (file_exists((env + "\\jre\\bin\\client\\jvm.dll").c_str())) {
        //prolly a JDK
        env += "\\jre\\bin\\client\\jvm.dll";
    } else {
        MyNS::MyClass->printf("JVM Dll not found; Is JAVA_HOME set?");
        return;
    }
    //load it
    if ( (this->_libInst = LoadLibrary(str_to_wstr(env).c_str())) == NULL) {
        MyNS::MyClass->printf("Can't load JVM DLL");
        return;
    }
    //grab vm creation method
    CreateJavaVM_t* createFn = (CreateJavaVM_t *)GetProcAddress(this->_libInst, "JNI_CreateJavaVM");
    if (createFn == NULL) {
        MyNS::MyClass->printf("Can't locate JNI_CreateJavaVM");
        return;
    }
    //build options
    JavaVMInitArgs initArgs;
    JavaVMOption* options = new JavaVMOption[1];
    string classpath = "-Djava.class.path=";
    //we want all jars in our directory
    string jars = get_jars(dllDirectory);
    if (jars == "") {
        return;
    }
    classpath += jars;
    options[0].optionString = (char *) classpath.c_str();
    //assuming 1.6 here
    initArgs.version = JNI_VERSION_1_6;
    initArgs.nOptions = 1;
    initArgs.options = options;
    initArgs.ignoreUnrecognized = false;
    //create vm
    if (createFn(&this->_jvm, (void **)&this->_env, &initArgs) != 0) {
        delete options;
        MyNS::MyClass->printf("Can't create VM");
        return;
    }
    delete options;
    //grab delegate class
    this->_delegate = this->_env->FindClass("my/java/package/MyDelegationClass");
    if (this->_delegate == NULL) {
        this->_jvm->DestroyJavaVM();
        this->_jvm = NULL;
        MyLog::MyLog->printf("Can't find delegate class; Is bridge JAR present?");
        return;
    }
}

Also, we must make sure we free resources in the destructor:

MyNS::~MyClass()
{
    if (this->_jvm != NULL) {
        this->_jvm->DestroyJavaVM();
    }
    if (this->_libInst != NULL) {
        FreeLibrary(this->_libInst);
    }
}

After compiling this into a DLL along w/ a test method to call back to Java, I went ahead and ran SWIG on the includes I was writing the plugin for. Unfortunately, none of the Java native methods were mapped. It turns out that, when instantiating the VM from native code, none of the JNI methods are “registered”. RegisterNatives to the rescue. The C++ API I was using generated several thousand Java methods in SWIG making it much to trivial to hand write the RegisterNatives code. So, I made an ANT task to do it for me. Here’s the code (collapsed by default):

package org.cretz.swig.ant;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

/**
 * Ant task to add "RegisterNative" code to SWIG gen'd files
 * 
 * @author Chad Retz
 */
public class RegisterNativesTask extends Task {
    
    /**
     * Get a type signature from a class
     * 
     * @param cls
     * @return
     */
    private static String getJNITypeSignature(Class<?> cls) {
        String ret = "";
        if (cls.isArray()) {
            ret += '[';
        }
        if ("boolean".equals(cls.getName())) {
            ret += 'Z';
        } else if ("byte".equals(cls.getName())) {
            ret += 'B';
        } else if ("char".equals(cls.getName())) {
            ret += 'C';
        } else if ("short".equals(cls.getName())) {
            ret += 'S';
        } else if ("int".equals(cls.getName())) {
            ret += 'I';
        } else if ("long".equals(cls.getName())) {
            ret += 'J';
        } else if ("float".equals(cls.getName())) {
            ret += 'F';
        } else if ("double".equals(cls.getName())) {
            ret += 'D';
        } else if ("void".equals(cls.getName())) {
            ret += 'V';
        } else {
            ret += 'L' + cls.getName().replace('.', '/') + ';';
        }
        return ret;
    }
    
    /**
     * Get a JNI signature for the given method
     * 
     * @param method
     * @return
     */
    private static String getJNISignature(Method method) {
        StringBuilder builder = new StringBuilder();
        builder.append('(');
        for (Class<?> parameter : method.getParameterTypes()) {
            builder.append(getJNITypeSignature(parameter));
        }
        builder.append(')');
        builder.append(getJNITypeSignature(method.getReturnType()));
        return builder.toString();
    }
    
    /**
     * Load an entire fine into a string
     * 
     * @param file
     * @return
     * @throws IOException
     */
    private static String loadFileToString(File file) throws IOException {
        BufferedReader reader = new BufferedReader(
                new FileReader(file));
        StringBuilder builder = new StringBuilder();
        try {
            String line = reader.readLine();
            while (line != null) {
                builder.append(line + "\r\n");
                line = reader.readLine();
            }
            return builder.toString();
        } finally {
            reader.close();
        }
    }
    
    /**
     * Write an entire string to a file
     * 
     * @param file
     * @param string
     * @throws IOException
     */
    private static void writeStringToFile(File file, String string) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(file));
        try {
            writer.write(string);
        } finally {
            writer.close();
        }
    }

    private String source;
    private String module;
    private String _package;
    
    @Override
    public void execute() throws BuildException {
        if (source == null) {
            throw new BuildException("source is required");
        }
        if (module == null) {
            throw new BuildException("module is required");
        }
        if (_package == null) {
            throw new BuildException("_package is required");
        }
        //get cpp and header file
        File cppFile = new File(source);
        File headerFile = new File(source.replace(".cpp", ".h"));
        if (!cppFile.exists()) {
            throw new BuildException("Can't find source file");
        }
        if (!headerFile.exists()) {
            throw new BuildException("Can't find header file");
        }
        String cppContents;
        String headerContents;
        try {
            cppContents = loadFileToString(cppFile);
            headerContents = loadFileToString(headerFile);
        } catch (IOException e) {
            throw new BuildException("Unable to load cpp or header file", e);
        }
        //find class
        Class<?> cls;
        try {
            cls = Class.forName(_package + "." + module + "JNI");
        } catch (ClassNotFoundException e) {
            throw new BuildException("Can't find JNI class", e);
        }
        //load up the methods
        List<JNIMethod> methods = new ArrayList<JNIMethod>();
        String prefix = "Java_" + cls.getName().replace('.', '_');
        for (Method method : cls.getDeclaredMethods()) {
            if (Modifier.isNative(method.getModifiers())) {
                //get the part before the underscore
                String methodName = method.getName();
                String jniMethod;
                jniMethod = prefix + '_' + methodName.replace("_", "_1");
                if (!cppContents.contains("JNICALL " + jniMethod + "(")) {
                    log("Can't find JNI method, skipping: " + jniMethod, 
                            Project.MSG_WARN);
                } else {
                    methods.add(new JNIMethod(methodName, getJNISignature(method), 
                            jniMethod));
                }
            }
        }
        //write pieces to header and cpp
        headerContents = "#pragma once\r\n#include <jni.h>\r\n\r\n" +
                headerContents + "\r\n\r\nclass SwigUtils {\r\npublic:\r\n\t" +
                "static int registerNatives(JNIEnv* env);\r\n};";
        StringBuilder cppMethods = new StringBuilder();
        cppMethods.append("int SwigUtils::registerNatives(JNIEnv* env)\r\n{\r\n");
        cppMethods.append("\tJNINativeMethod methods[" + methods.size() + "];\r\n");
        cppMethods.append("\tjclass cls = env->FindClass(\"");
        cppMethods.append(cls.getName().replace('.', '/') + "\");\r\n");
        for (int i = 0; i < methods.size(); i++) {
            JNIMethod method = methods.get(i);
            cppMethods.append("\r\n\tmethods[" + i + "].name = \"" +
                    method.name + "\";\r\n");
            cppMethods.append("\tmethods[" + i + "].signature = \"" +
                    method.signature + "\";\r\n");
            cppMethods.append("\tmethods[" + i + "].fnPtr = (void*)&" +
                    method.cppSignature + ";\r\n");
        }
        cppMethods.append("\r\n\treturn (int) env->RegisterNatives(cls, methods, " +
                methods.size() + ");\r\n}");
        try {
            writeStringToFile(headerFile, headerContents);
            writeStringToFile(cppFile, cppContents + "\r\n\r\n" + 
                    cppMethods.toString());
        } catch (IOException e) {
            throw new BuildException("Unable to write cpp or header file", e);
        }
    }
    
    /**
     * The source cpp file to alter (also assumes the
     * header file is there)
     *  
     * @return
     */
    public String getSource() {
        return source;
    }
    
    public void setSource(String source) {
        this.source = source;
    }
    
    /**
     * The name of the SWIG module
     * 
     * @return
     */
    public String getModule() {
        return module;
    }

    public void setModule(String module) {
        this.module = module;
    }

    /**
     * The package the source is in
     * 
     * @return
     */
    public String getPackage() {
        return _package;
    }

    public void setPackage(String _package) {
        this._package = _package;
    }

    /**
     * Simple POJO for holding method information
     * 
     * @author Chad Retz
     */
    private static class JNIMethod {
        private final String name;
        private final String signature;
        private final String cppSignature;
        
        private JNIMethod(String name, String signature, String cppSignature) {
            this.name = name;
            this.signature = signature;
            this.cppSignature = cppSignature;
        }
    }
}

NOTE: I wouldn’t use the JNI signature generator above in other situation; it doesn’t handle arrays.

It accepts three arguments. The “source” argument is the path to the .cpp file (the -o argument passed to SWIG). The “module” argument is the SWIG module name in your .i file. The “package” argument is the package you told SWIG about (the -package argument for SWIG).

It reflectively obtains all native methods in the ‘module + JNI’.java file. This means the generated SWIG code must be on the classpath when running this task. It creates a SwigUtils class in the header file with one method: registerNatives which accepts a JNIEnv:

class SwigUtils {
public:
    static int registerNatives(JNIEnv* env);
};

In the cpp file, it implements this method at the bottom with all the other SWIG code. The method returns the value returned by RegisterNatives. Now all you have to do is call this once you are done creating your JVM:

int regRes = SwigUtils::registerNatives(this->_env);
if (regRes != 0) {
    MyLog::MyLog->printf("Couldn't register natives");
}

I hope this code helps someone. License: WTFPL.

Advertisements
Tagged with: , , ,

One Response

Subscribe to comments with RSS.

  1. Renny said, on December 23, 2010 at 10:08 pm

    Nice! Pretty elegant way to do this…


Leave a Reply

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

%d bloggers like this: