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?
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);
}