首页 > 解决方案 > 使用 JNI 创建 JFrame

问题描述

我有需要从 C++ 调用的 java 库,使用 JNI 我能够成功调用 java 库上的方法,除了一个。

有问题的方法创建了一个 JFrame 并导致 jvm 在 MacOS 上运行时陷入死锁,经过一些研究,我发现任何涉及 AWT/swing 的与 JNI 的交互都不能从主线程完成。

所以我修改了代码以在辅助线程上对 JVM 进行所有调用,但我仍然遇到相同方法的问题。我得到了要显示的 JFrame,但之后 C++ 代码在 CFRunLoopRunInMode 函数调用上被阻塞,直到我关闭 JFrame,然后进程突然终止。

目前我的代码看起来像这样(这是一个简化版本,但基本相同。为简单起见,C++ 代码中也省略了错误处理):

Java 代码

import javax.swing.*;
import java.awt.*;

public class MyClass extends JFrame
{
    private Thread frameThread;

    public void createFrame()
    {
        System.out.println("Creating Frame Thread");
        frameThread = new Thread(() -> {
            setTitle("JFrameCenter Position");
            add(new JLabel("JFrame set to center of the screen", SwingConstants.CENTER), BorderLayout.CENTER);
            setSize(400, 300);
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            setLocationRelativeTo(null); // this method display the JFrame to center position of a screen
            setVisible(true);
        });

        System.out.println("Starting Frame Thread");
        frameThread.start();
    }
}


C++ 代码

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

#define JLI_DYLIB "/lib/jli/libjli.dylib"
#define JNI_CREATEVM_FUNC "JNI_CreateJavaVM"
typedef jint (JNICALL CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);

void* DyLibHandler;
JavaVM *jvm;

CFRunLoopRef currentLoopRef;
CFRunLoopSourceRef sourceRef;

void *GetStartJvmFunc(std::string javaHome) {
    std::string jliPath = javaHome.append(JLI_DYLIB);
    DyLibHandler = dlopen(jliPath.c_str(), RTLD_LAZY);
    if(!DyLibHandler)
    {
        return NULL;
    }
    void * jvmFunc = dlsym(DyLibHandler, JNI_CREATEVM_FUNC);
    if  (!jvmFunc)
    {
        dlclose(DyLibHandler);
        return NULL;
    }
    
    return jvmFunc;
}

void StartJvm(const char * javaHome, const char * classPath) {
    std::string jarsPath = "-Djava.class.path=";
    std::string home = "-Djava.home=";
    JavaVMOption* options = new JavaVMOption[3];   // JVM invocation options
    options[0].optionString = (char*)jarsPath.append(classPath).c_str();
    options[0].extraInfo = NULL;
    options[1].optionString = (char*)home.append(javaHome).c_str();
    options[1].extraInfo = NULL;
    options[2].optionString = "-Dsun.net.inetaddr.ttl=10";
    options[2].extraInfo = NULL;

    JavaVMInitArgs vm_args;
    vm_args.options = options;
    vm_args.nOptions = 3;
    vm_args.version = JNI_VERSION_1_8;
    
    JNIEnv *jniEnvironment;
    CreateJavaVM_t* createVMFunc = (CreateJavaVM_t*)GetStartJvmFunc(std::string(javaHome));
    jint rc = createVMFunc(&jvm, (void**)&jniEnvironment, &vm_args);  // YES !!
    delete[] options;    // we then no longer need the initialisation options.
    if (rc != JNI_OK) {
        createVMFunc = NULL;
        exit(EXIT_FAILURE);
    }
}

void CreateJFrame(JNIEnv *jniEnvironment)
{
    jclass localHandler = jniEnvironment->FindClass("MyClass");
    if (!localHandler)
    {
        std::cout << "Could not Find LocalClass" << std::endl;
        exit(EXIT_FAILURE);
    }
    
    jclass classHandle = (jclass) jniEnvironment->NewGlobalRef(localHandler);
    jniEnvironment->DeleteLocalRef(localHandler);
    
    jmethodID constructorHandler = jniEnvironment->GetMethodID(classHandle, "<init>", "()V");
    if (!constructorHandler)
    {
        std::cout << "Could not find Constructor" << std::endl;
        exit(EXIT_FAILURE);
    }

    jmethodID createMethodHandler = jniEnvironment->GetMethodID(classHandle, "createFrame", "()V");
    if (!createMethodHandler)
    {
        std::cout << "Could not Find CreateFrame Method" << std::endl;
        exit(EXIT_FAILURE);
    }
    
    jobject localRef = jniEnvironment->NewObject(classHandle, constructorHandler);
    if(!localRef)
    {
        std::cout << "Could not instantiate MyClass" << std::endl;
        exit(EXIT_FAILURE);
    }

    jobject classInstance = jniEnvironment->NewGlobalRef(localRef);
    jniEnvironment->DeleteLocalRef(localRef);
    if (classInstance == nullptr)
    {
        std::cout << "Could not create global ref to MyClass instance" << std::endl;
        exit(EXIT_FAILURE);
    }
    
    jniEnvironment->CallVoidMethod(classInstance, createMethodHandler);
}

void SourceCallback (  void *info  ) {
    
    std::cout << "From Source Callback! Got Event" << std::endl;
}

void * ThreadProc(void * arg)
{
    JNIEnv *env = NULL;
    jint result =jvm->AttachCurrentThread((void **)&env, NULL);
    if( result != JNI_OK)
    {
        std::cout << "Error! Could not attach JNIEnv to thread (" << result << ")"<< std::endl;
        goto clean;
    }
    
    CreateJFrame(env);
    
    std::cout << "Deataching Thread" << std::endl;
    result = jvm->DetachCurrentThread();
    if(result != JNI_OK)
    {
        std::cout << "Error! could not deatached current thread (" << result << ")" << std::endl;
    }
    else{
        std::cout << "Done Deataching Thread" << std::endl;
    }
    
clean:
    std::cout << "Done Creating Frame" << std::endl;
    CFRunLoopSourceSignal(sourceRef);
    CFRunLoopWakeUp(currentLoopRef);
    std::cout << "Done signalling RunLoop" << std::endl;
    return NULL;
}

int main(int argc, const char * argv[])
{
    currentLoopRef = CFRunLoopGetCurrent();
    CFRunLoopSourceContext sourceContext;
    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 = &SourceCallback;
    sourceRef = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
    CFRunLoopAddSource(currentLoopRef, sourceRef, kCFRunLoopCommonModes);
    
    StartJvm(/*JavaHome*/, /*ClassPaht*/);
    
    pthread_t vmThread;
    pthread_create(&vmThread, NULL, ThreadProc, NULL);
    CFRunLoopRunResult result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, TRUE);
    std::cout << "Done with Loop Run" << std::endl;
}

使用上面的代码,我希望框架能够显示并立即关闭,并在控制台上看到SourceCallback函数的打印和调用后的打印。CFRunLoopRunInMode但是发生的情况是框架没有关闭并且两次打印都没有发生,我必须手动关闭框架然后程序终止而不打印任何内容。

我也尝试使用 while 循环而不是CFRunLoopRunInModeVM 上发生了相同的死锁。即使我没有向 CFRunLoopSource 发出信号,程序也不会终止(CFRunLoop 不会超时),直到我手动关闭 JFrame 并且最后一次打印没有发生。

我已经陷入这个问题一段时间了,我对正在发生的事情一无所知。为什么程序“等待”直到 Frame 关闭才突然终止?为什么 CFRunLoop 忽略事件和超时计时器?

编辑:我通过在主线程上JNIEnv *创建指针并从辅助线程调用以获得有效的. 可悲的是,程序的行为与以前相同,JFrame 得到显示,主线程卡住,直到我关闭 JFrame。JavaVmAttachToCurrentThreadJNIEnv

我还注意到我忘记将新源添加到 CFRunLoop,这就是为什么在从辅助线程发出 CFRunLoop 信号后没有调用源回调的原因。现在我看到了来自源回调的打印,但主线程仍然卡在 CFRunLoop 上。

标签: javac++macosswingjava-native-interface

解决方案


推荐阅读