Search code examples
javac++java-native-interface

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


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 ?


Solution

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