Search code examples
java-native-interfacerossigint

ROS NodeHandle inside JNI callback


I need to receive ROS messages inside a Spring Boot application. For that I have setup some JNI classes. It works but as soon as I create a NodeHandle, I can no longer close the app with a normal SIGINT, it requires a SIGKILL.

Here's the code:

CppBridge.java

public class CppBridge {

    static {
        System.load(new File("backend/src/main/cpp/libcppbridge.so").getAbsolutePath());
    }

    public native void start();
}

CppBridge.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_ifremer_rospipe_CppBridge */

#ifndef _Included_org_ifremer_rospipe_CppBridge
#define _Included_org_ifremer_rospipe_CppBridge
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_ifremer_rospipe_CppBridge
 * Method:    start
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_ifremer_rospipe_CppBridge_start
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

CppBridge.cpp

JNIEXPORT void JNICALL Java_org_ifremer_rospipe_CppBridge_start(JNIEnv* env, jobject obj)
{
    // Init ROS:
    int argc = 0;
    ros::init(argc, nullptr, "ifr_mimosa_bridge");

    // Create the NodeHandle, this causes SIGINT to hang:
    ros::NodeHandle nh("~");
}

RosPipe.java

@Component
public class RosPipe {

    private final CppBridge mCppBridge = new CppBridge();

    @PostConstruct()
    public void onStart() {

        mCppBridge.start();
    }
}

Why does the creation of a NodeHandle blocks the destruction of the CppBridge instance?


Solution

  • Turned out that internally ROS intercepts the SIGINT signal, and thus Spring no longer receives it. So when pressing CTRL-C the C++ process terminates gracefully while the Java stays alive.

    To fix it I overrode ROS SIGINT handling in order to notify Spring after ROS is terminated:

    void Node::init(JNIEnv* javaBridgeEnv)
    {
        // Get a reference to the JVM:
        if (javaBridgeEnv == nullptr || javaBridgeEnv->GetJavaVM(&mJvm) != 0)
        {
            mJvm = nullptr;
            std::cerr << "Unable to get Java Virtual Machine." << std::endl;
        }
    
        // Init ROS:
        int argc = 0;
        ros::init(argc, nullptr, "test", ros::init_options::NoSigintHandler);
        signal(SIGINT, Node::onSigint);
    
        ros::NodeHandle nh_private;
    
        [...]
    
        ros::spin();
    }
    
    void Node::onSigint(int)
    {
        // Shutdown ROS:
        ros::shutdown();
    
        // Notify Spring, otherwise it won't catch the SIGINT and won't exit:
        if (mJvm != nullptr)
        {
            JNIEnv* env;
            mJvm->AttachCurrentThread((void **)&env, nullptr);
    
            // To get the methods signatures: javap -s -p path/to/File.class
            jclass classCppBridge = env->FindClass("org/ifremer/rospipe/RosPipe");
            jmethodID stopMethod = env->GetStaticMethodID(classCppBridge, "stop", "()V");
    
            env->CallStaticVoidMethod(classCppBridge, stopMethod);
        }
    }
    

    In the Java side:

    public static void stop() {
        int exitCode = SpringApplication.exit(mApplicationContext);
        System.exit(exitCode);
    }