I would to know if there is a way to call a java method from cpp with a Functional Interface and use this callback inside my java code.
This is my code :
Main.java
import java.util.function.Consumer;
public class Main {
static {
System.loadLibrary("Main");
}
public static void main(String[] args) {
Main m = new Main();
m.nativeMethod();
}
public void test() {
System.out.println("test");
}
public void getCallback(Consumer<String> consumer) {
consumer.accept("Data from Java to cpp");
}
public native void nativeMethod();
}
Main.cpp
#include <iostream>
#include <string>
#include "Main.h"
extern "C"
{
JNIEXPORT void JNICALL myCallback(JNIEnv *env, jobject obj, jstring data)
{
jboolean isCopy;
const char *cData = env->GetStringUTFChars(data, &isCopy);
std::string cppData(cData);
env->ReleaseStringUTFChars(data, cData);
std::cout << cppData << '\n';
}
JNIEXPORT void JNICALL Java_Main_nativeMethod(JNIEnv *env, jobject obj)
{
std::cout << "Hello from Java_Main_nativeMethod" << '\n';
jmethodID jTest = env->GetMethodID(
env->GetObjectClass(obj),
"test",
"()V");
env->CallVoidMethod(obj, jTest);
jmethodID jFunction = env->GetMethodID(
env->GetObjectClass(obj),
"getCallback",
"(Ljava/util.function/Consumer;)V");
env->CallVoidMethod(obj, jFunction, myCallback);
}
}
compile.bash
#!/bin/bash
cd "$(dirname "$0")"
javac -h . Main.java # generates header
javac Main.java
gcc -lstdc++ -shared -I /Library/Java/JavaVirtualMachines/temurin-20.jdk/Contents/Home/include -o libMain.jnilib Main.cpp
java Main
output of $ bash compile.bash
Hello from Java_Main_nativeMethod
test
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x0000000108554b14, pid=72842, tid=10499
#
# JRE version: OpenJDK Runtime Environment Temurin-20.0.2+9 (20.0.2+9) (build 20.0.2+9)
# Java VM: OpenJDK 64-Bit Server VM Temurin-20.0.2+9 (20.0.2+9, mixed mode, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64)
# Problematic frame:
# V [libjvm.dylib+0x514b14] jni_CallVoidMethodV+0xe0
#
# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# hs_err_pid72842.log
#
# If you would like to submit a bug report, please visit:
# https://github.com/adoptium/adoptium-support/issues
#
zsh: abort java Main
How can I make it work ?
Sure, it can be done, but it requires jumping through a few more hoops than you perhaps were hoping.
First, provide a concrete Java implementation on the Consumer
interface that can hold some sort of reference to its C++ counterpart:
package com.example.consumertest;
import java.util.function.Consumer;
public class NativeStringConsumer implements Consumer<String> {
// Holds a pointer to the native counterpart
private final long nativeThis;
private NativeStringConsumer(final long nativeThis) {
this.nativeThis = nativeThis;
}
@Override
public void accept(String s) {
// Call the native counterpart with the provided string
accept(nativeThis, s);
}
public void delete() {
// Free the memory used by the native counterpart
delete(nativeThis);
}
public static native NativeStringConsumer create();
private static native void accept(final long nativeThis, final String s);
private static native void delete(final long nativeThis);
static {
System.loadLibrary("native-lib");
}
}
Next, implement the native methods:
using StringConsumer = std::function<void(const std::string&)>;
extern "C"
JNIEXPORT jobject JNICALL Java_com_example_consumertest_NativeStringConsumer_create(
JNIEnv *env,
jclass clazz)
{
auto consumer = new StringConsumer([] (const std::string &str) {
// I'm testing this on Android, hence __android_log_write instead of std::cout
__android_log_write(ANDROID_LOG_DEBUG, "NativeStringConsumer", str.c_str());
});
auto ctor = env->GetMethodID(clazz, "<init>", "(J)V");
return env->NewObject(clazz, ctor, reinterpret_cast<jlong>(consumer));
}
extern "C"
JNIEXPORT void JNICALL Java_com_example_consumertest_NativeStringConsumer_accept(
JNIEnv *env,
jclass clazz,
jlong native_this,
jstring s)
{
const char *chars = env->GetStringUTFChars(s, nullptr);
std::string str(chars, env->GetStringLength(s));
env->ReleaseStringUTFChars(s, chars);
auto consumer = reinterpret_cast<StringConsumer*>(native_this);
(*consumer)(str);
}
extern "C"
JNIEXPORT void JNICALL Java_com_example_consumertest_NativeStringConsumer_delete(
JNIEnv *env,
jclass clazz,
jlong native_this)
{
auto consumer = reinterpret_cast<StringConsumer*>(native_this);
delete consumer;
}
This is just meant as an illustrative proof of concept, so it lacks a lot of error checking that you ought to have in real-world JNI code.
And the way you'd use all this in your Java code would be something like:
final NativeStringConsumer consumer = NativeStringConsumer.create();
consumer.accept("Hello world!");
// ... at some point later on when you no longer need the object:
consumer.delete();