timob / jnigi

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

macOS AWT/Swing support #67

Closed mlaggner closed 1 year ago

mlaggner commented 2 years ago

I am just trying to use this lib in my golang launcher for my Java Swing application. I managed to start up the JVM and load the first few lines of code before the EventDispatchThread starts. My Java program looks like

public static void main(String[] args) {
  // do something before EDT
  EventQueue.invokeLater(new Runnable() {
    public void run() {
      // start the UI-application
    }
  }
}

In the logs of the application I just see the lines before the EDT start but afterwards nothing happens and the launcher is just "hung".

I found the issues #35 and the PR #36 and #43 but I did not find the right solution for the problem. According to the example of OpenJDK (https://github.com/AdoptOpenJDK/openjdk-jdk/blob/master/src/java.base/macosx/native/libjli/java_md_macosx.m#L354) I see that we need to rund main in a new thread and "park" the first thread - OpenJDK uses CoreFoundation here...

Is this still needed for using this lib? Why has the corresponding code been removed from PR #43?

I have a full setup to help here, but my knowledge with CGO is very limited (I am a Java developer :) ).

timob commented 1 year ago

Sorry for taking time to reply, real life issues.

I'm not quite sure what you are trying to do, are you running a Go program, that calls a Java method to start your Java GUI app?

Could you build a Go shared lib, and link that using native method, from Java?

Re: changes to darwin.go, if this is useful, we could add it as special funcs just for macos, last PR was changing other platforms too.

mlaggner commented 1 year ago

My intention is to build a launcher and patcher for a Java Swing application - and like the original java binary I want to embed the JVM in the same process as the launcher as been triggered (for several reasons).

With jnigi everything worked as expected, except launching the UI (which needs to have the fixes in darwin.go to launch the Java main method in a separate thread and "park" the main thread in the CFRunLoop which is provided by the CoreFoundation Framework).

timob commented 1 year ago

Can you post the darwin.go that you are using?

mlaggner commented 1 year ago

the repo with all fixes I committed is at: https://github.com/mlaggner/jnigi

especially the code for darwin.go: https://github.com/mlaggner/jnigi/blob/master/darwin.go - this contains basically the same as you removed from your PR.

timob commented 1 year ago

Below is what I think darwin.go should be, I don't have a mac to test this. If this looks good would be good if someone can go test with this file and do a PR:

// Copyright 2016 Tim O'Brien. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build darwin
// +build darwin

package jnigi

/*
#cgo LDFLAGS:-ldl -framework CoreFoundation

#include <dlfcn.h>
#include <jni.h>
#include <CoreFoundation/CoreFoundation.h>

typedef jint (*type_JNI_GetDefaultJavaVMInitArgs)(void*);

type_JNI_GetDefaultJavaVMInitArgs var_JNI_GetDefaultJavaVMInitArgs;

jint dyn_JNI_GetDefaultJavaVMInitArgs(void *args) {
    return var_JNI_GetDefaultJavaVMInitArgs(args);
}

typedef jint (*type_JNI_CreateJavaVM)(JavaVM**, void**, void*);

type_JNI_CreateJavaVM var_JNI_CreateJavaVM;

jint dyn_JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args) {
    return var_JNI_CreateJavaVM(pvm, penv, args);
}

// call back for dummy source used to make sure the CFRunLoop doesn't exit right away
// This callback is called when the source has fired.
void jnigiCFRSourceCallBack (  void *info  ) {
}
void jnigiRunCFRLoop(void) {
    CFRunLoopSourceContext sourceContext;
    //Create a a sourceContext to be used by our source that makes
    //sure the CFRunLoop doesn't exit right away
    sourceContext.version = 0;
    sourceContext.info = NULL;
    sourceContext.retain = NULL;
    sourceContext.release = NULL;
    sourceContext.copyDescription = NULL;
    sourceContext.equal = NULL;
    sourceContext.hash = NULL;
    sourceContext.schedule = NULL;
    sourceContext.cancel = NULL;
    sourceContext.perform = &jnigiCFRSourceCallBack;
    // Create the Source from the sourceContext
    CFRunLoopSourceRef sourceRef = CFRunLoopSourceCreate (NULL, 0, &sourceContext);
    // Use the constant kCFRunLoopCommonModes to add the source to the set of objects
    // monitored by all the common modes
    CFRunLoopAddSource (CFRunLoopGetCurrent(),sourceRef,kCFRunLoopCommonModes);
    // Park this thread in the runloop
    CFRunLoopRun();
}

*/
import "C"

import (
    "errors"
    "log"
    "os"
    "path/filepath"
    "unsafe"
)

const (
    JLI_LOAD_ENV   = "LIBJLI_LOAD"
    JLI_LOAD_YES   = "yes"
    JLI_LOAD_FORCE = "force"
)

func jni_GetDefaultJavaVMInitArgs(args unsafe.Pointer) jint {
    return jint(C.dyn_JNI_GetDefaultJavaVMInitArgs((unsafe.Pointer)(args)))
}

func jni_CreateJavaVM(pvm unsafe.Pointer, penv unsafe.Pointer, args unsafe.Pointer) jint {
    return jint(C.dyn_JNI_CreateJavaVM((**C.JavaVM)(pvm), (*unsafe.Pointer)(penv), (unsafe.Pointer)(args)))
}

// LoadJVMLib loads libjvm.dyo as specified in jvmLibPath
func LoadJVMLib(jvmLibPath string) error {
    // On MacOS we need to preload libjli.dylib to workaround JDK-7131356
    // "No Java runtime present, requesting install".
    // If envar LIBJLI_LOAD; = "yes": load but just log error if load fails, =
    // "force": load and exit with error if load fails.
    if jliLoadEnv := os.Getenv(JLI_LOAD_ENV); jliLoadEnv == JLI_LOAD_YES || jliLoadEnv == JLI_LOAD_FORCE {
        libjliPath := filepath.Join(filepath.Dir(jvmLibPath), "..", "libjli.dylib")
        clibjliPath := cString(libjliPath)
        defer func() {
            if clibjliPath != nil {
                free(clibjliPath)
            }
        }()

        // Do not close JLI library handle until JVM closes
        handlelibjli := C.dlopen((*C.char)(clibjliPath), C.RTLD_NOW|C.RTLD_GLOBAL)
        if handlelibjli == nil {
            if jliLoadEnv == JLI_LOAD_YES {
                log.Printf("WARNING could not dynamically load %s", libjliPath)
            } else if jliLoadEnv == JLI_LOAD_FORCE {
                log.Fatalf("ERROR could not dynamically load %s", libjliPath)
            }
        }
    }

    cs := cString(jvmLibPath)
    defer func() {
        if cs != nil {
            free(cs)
        }
    }()

    libHandle := uintptr(C.dlopen((*C.char)(cs), C.RTLD_NOW|C.RTLD_GLOBAL))
    if libHandle == 0 {
        return errors.New("could not dynamically load libjvm.dylib")
    }

    cs2 := cString("JNI_GetDefaultJavaVMInitArgs")
    defer free(cs2)
    ptr := C.dlsym(unsafe.Pointer(libHandle), (*C.char)(cs2))
    if ptr == nil {
        return errors.New("could not find JNI_GetDefaultJavaVMInitArgs in libjvm.dylib")
    }
    C.var_JNI_GetDefaultJavaVMInitArgs = C.type_JNI_GetDefaultJavaVMInitArgs(ptr)

    cs3 := cString("JNI_CreateJavaVM")
    defer free(cs3)
    ptr = C.dlsym(unsafe.Pointer(libHandle), (*C.char)(cs3))
    if ptr == nil {
        return errors.New("could not find JNI_CreateJavaVM in libjvm.dylib")
    }
    C.var_JNI_CreateJavaVM = C.type_JNI_CreateJavaVM(ptr)
    return nil
}

func RunCFRLoop() {
    C.jnigiRunCFRLoop()
}
mlaggner commented 1 year ago

the unit tests pass with your code. This is basically the same as I had in my fork (except for the last two methods, but this should not matter)

# go test
Exception in thread "main" java.lang.NoClassDefFoundError: java/foo/bar
Caused by: java.lang.ClassNotFoundException: java.foo.bar
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
Exception in thread "main" java.lang.NoClassDefFoundError: java/foo/bar
Caused by: java.lang.ClassNotFoundException: java.foo.bar
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
PASS
ok      github.com/timob/jnigi       0.177s

I can test this along with my launcher to check everything else. If that works, I will create a PR (probably this weekend)

mlaggner commented 1 year ago

I could test everything within my app and your code works as expected. Thanks!