timob / jnigi

Golang Java JNI library
BSD 2-Clause "Simplified" License
166 stars 44 forks source link

how to pass go func to jvm #75

Open proohit opened 1 year ago

proohit commented 1 year ago

Hi I wanted to ask how it is possible to pass a go func as an implementation to an object in JVM that implements the java.util.function.Function<String,String> interface.

Is there a way and if yes, what steps do I need for that?

I have tried the following attempt:

fn_obj, err := env.NewObject("java/util/function/Function", nil)
fn_obj.SetField(env, "apply", func() {})

This of course fails, because there is no <init> ctor for an interface. My goal is to provide an implementation in go for that interface.

timob commented 1 year ago

They way I've done this in tha past is to create a class in java that implements the interface and declare all the methods native (or at least the ones you are interested in). Then provide the go implementations using registernatives. Please see Oracle's JNI documentation on how to do this.

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

The implementing funnctionns are passed the object so you can keep track of this on the Go side.

proohit commented 1 year ago

Hey, thanks for your reply and sorry for my late response. Can registerNatives be used for objects as well? The JNI interface expects clazz: a Java class object, but the jnigi api declares a className parameter. I'm not sure, if registerNatives is used for whole classes or objects of that class.

As an example:

I have the following class, which, as you suggested, implements the said function interface:

package shared.server;

import java.util.function.Function;

public class RouteHandlerNative implements Function<String, String> {
    private String path;

    public RouteHandlerNative(String path) {
        this.path = path;
    }

    public native String apply(String arg0);
}

Now can I do the following, but for an instance of RouteHandlerNative instead?

func applyTest(arg0 string) string {
    return ""
}

rhn_obj, err := env.NewObject("shared/server/RouteHandlerNative", []string{"/test"})
env.RegisterNative("shared/server/RouteHandlerNative", "apply", jnigi.Object, []interface{}{}, applyTest) // <-- use for rhn_obj instead?

Here's a little context for what I am trying: I'm trying to provide a Java library that can be used from go (and other languages), but accepts custom function implementations from the host language (go, and other languages). The domain is a webserver (java shared lib) for which route handlers can be registered from host languages. The poc includes handlers that handle requests and respond with a response, in this case a string, hence the interface Function<String, String>. For reference, there is a Map<String, RouteHandlerNative> somewhere in the java lib that matches incoming request paths to RouteHandlerNative#path and invokes RouteHandlerNative#apply, which then is called from the host language.

I hope that clarifies my needs and request.

timob commented 1 year ago

Hi, in your example you are on the right direction but you can't use go types in your native function call back, JNI uses C, so you need to convert from the C types to go types in your call back. So something like (just going off old code of mine):

/*
#include<stdint.h>

extern uintptr_t go_callback_RouteHandlerNativeApply(void *env, uintptr_t obj, uintptr_t arg0)
*/
import"C"

//export go_callback_RouteHandlerNativeApply
func go_callback_RouteHandlerNativeApply(env unsafe.Pointer, obj uintptr, arg0 uintptr) uintptr

Then you can use the C function in the last argument to JNIGI RegisterNative.

In your callback you need to convert the argument to a go string, call your go function and convert the go string returned to a java string and return.

You need a good understanding of how Cgo works https://pkg.go.dev/cmd/cgo . Maybe if you get it working you could submit an example to the project!

proohit commented 1 year ago

Hi and thanks for the reference. I followed your advice using RegisterNative and was able to accomplish what I'm looking for in Rust, now I'm trying the same in go. This is the jni c signature:

JNIEXPORT jstring JNICALL Java_shared_server_Server_handle_1request_1external
  (JNIEnv *, jclass, jint, jstring);

This is the corresponding go function:

func handle_request_external(env unsafe.Pointer, obj uintptr, fn_id uintptr, raw_request_ptr uintptr) *jnigi.ObjectRef {
    println(fn_id, raw_request_ptr)
    req_ptr := unsafe.Pointer(raw_request_ptr)
    req_string_ptr := (*string)(req_ptr)

    request := *req_string_ptr
    println(request)

    var env_val *jnigi.Env = (*jnigi.Env)(env)
    resp, _ := env_val.NewObject("java/lang/String", []byte("test"))

    return resp
}

I'm now trying to register the function as a native method:

env.RegisterNative("shared/server/Server", "Java_shared_server_Server_handle_1request_1external", jnigi.Object, []interface{}{}, handle_request_external)
//or 
env.RegisterNative("shared/server/Server", "handle_request_external", jnigi.Object, []interface{}{}, handle_request_external)

Both variants result in the following error:

panic: interface conversion: interface {} is func(unsafe.Pointer, uintptr, uintptr, uintptr) *jnigi.ObjectRef, not unsafe.Pointer

goroutine 1 [running, locked to thread]:
tekao.net/jnigi.(*Env).RegisterNative(0x4000062040, {0x4bf675?, 0x400004ff08?}, {0x4c4a7a, 0x33}, {0x4df338?, 0x4ded5c}, {0x400004fee0, 0x0, 0x200000003?}, ...)

I assume the signature do not match somehow, but do you have any directions from here?

proohit commented 1 year ago

Okay so following up, I researched some more and came a little further. Here's the current state and I feel like I'm close:

With headers:

#include<jni.h>
#include <stdint.h>
extern uintptr_t handle_request_external(void *env, uintptr_t obj, uintptr_t fn_id, uintptr_t raw_request_ptr);
/*
#include<server.h>
*/
import "C"

//export handle_request_external
func handle_request_external(env unsafe.Pointer, obj uintptr, raw_fn_id uintptr, raw_request_ptr uintptr) uintptr {
    req_ptr := unsafe.Pointer(raw_request_ptr)
    request := *(*string)(req_ptr)

    fn_id := int32(raw_fn_id)
    route := routes[fn_id]
    response := route.Handler(request)
    println(response)

    var env_val *jnigi.Env = jnigi.WrapEnv(env)
    resp, _ := env_val.NewObject("java/lang/String", []byte(response))

    return uintptr(unsafe.Pointer(resp))
}

...

env.RegisterNative("shared/server/Server", "handle_request_external", jnigi.ObjectType("java/lang/String"), []interface{}{jnigi.Int, "java/lang/String"}, C.handle_request_external)

Everything works, so the native handle_request_external function is being found and executed natively. The only problem I'm having is the return value of the native function, which should be a java/lang/String. For that, I'm trying to create a new object in the JVM and pass that as a reference, but the []byte(response) part seems to be erroneous. I've also tried a simple env_val.NewObject("java/lang/String", []byte("test")), but I'm still getting the following error:

signal: segmentation fault (core dumped)

with some gibbery core dumps I cannot quiet understand (cannot find the c program in order to use gdb). I feel like the JVM is trying to access some memory (the go byte slice) and fails at that for some reason. @timob, do you have some directions I could take from here?

timob commented 1 year ago

Hi! Sorry about the delay real life things... The first problem I see is you are trying to derefernce a pointer from Java as a Go string request := *(*string)(req_ptr). Given the case that this is actually a Java String, you need to use the getBytes method to get the bytes and convert that into a Go string. See the readme example line 37

It might also be better for the handle_request_external function to take a String object that gets set by the function instead of returning a new object, so that JVM can manage the object reference.

timob commented 11 months ago

Hi! I'm currently doing a PR to increase test coverage of the module.

There is an test for RegisterNative that does what you were asking above if you are still interested. See commit: https://github.com/timob/jnigi/pull/78/commits/de5801930ad50846681542e819f951b8ce869b48