JNI - Call java method with a Functional Interface parameter from cpp

44 Views Asked by At

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 ?

1

There are 1 best solutions below

1
Michael On BEST ANSWER

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();