【问题标题】:How to wrap callbacks in Java with SWIG如何使用 SWIG 在 Java 中包装回调
【发布时间】:2016-10-26 09:45:11
【问题描述】:

从这个线程跟进: How should I write the .i file to wrap callbacks in Java or C#

我意识到我的问题很相似,但该问题的答案是针对 void* 用户数据参数量身定制的,而我的回调需要一个枚举和一个 char*

这就是我的回调在头文件中的定义和使用方式

typedef void (*Callback_t)(const Log SomeLog, const char *Text);

virtual void SetCallback(const Callback_t SomeCallback, const Log SomeLog) = 0;

Log 是一个枚举。

我对 JNI 和 SWIG 比较陌生,因此需要一个关于如何包装它的具体指南,类似于我在上面提到的线程中提供的指南。

提前致谢。

【问题讨论】:

  • 看起来你的回调 userdata 参数实际上是Log typedef。你也可以给我们看看吗?它内部可能有一个空隙*,或者可以通过按摩给你一个空隙*。大概因为它是关于记录 char * 实际上必须是一个字符串,并且是您需要记录的消息吗?
  • Log 实际上是一个枚举,对不起我的错。 enum Log : uint32_t { blah = 1, blahblah = 2, blahblahblah = 3 }; 是的,char * 是空终止的。
  • 对,但您无法控制char * 字符串的内容——这就是应用程序将信息传递给您的回调的方式?你能改变它吗?如果是这种情况,这是一个设计得很糟糕的界面,但有一些可能的解决方法
  • 是的,正确的。如果这样更清楚,也由_In_z_ 注释。很遗憾,您无权访问源文件来更改任何内容。
  • 还有一个非常令人费解的问题——为什么SetCallback 函数是虚拟的?我怀疑它不会改变任何东西,但是这里有一些奇怪的设计决定,图书馆作者真的应该在这里使用std::function 来避免痛苦的世界。假设 SWIG 的导演不在画面中,有两个选项可供选择:使用全局变量(丑陋)或使用 libffi(非常简洁)。稍后我会尝试将两者都写出来。

标签: java java-native-interface swig


【解决方案1】:

简单的解决方案是调整我在上一个问题中的回答,只使用一个全局变量来存储jobject,我们不能存储在回调期间传递给我们的某个参数中。 (库作者的设计很糟糕,在回调发生时,似乎没有办法在设置可用于函数的回调时传递参数。通常是@987654326 @ 或者只是 this。鉴于它是 C++,如果是我设计我个人会使用 std::function 的库,那么我们可以在这里简单地依赖 SWIG 导演,但这似乎不是一个选项情景)

为了解决这个问题,我编写了 test.h:

typedef enum { 
  blah = 1,
  blahblah = 2,
} Log;

typedef void (*Callback_t)(const Log SomeLog, const char *Text);

void SetCallback(const Callback_t SomeCallback, const Log SomeLog);

void TestIt(const char *str);

和 test.c:

#include "test.h"
#include <assert.h>

static Callback_t cb=0;
static Log log=0;

void SetCallback(const Callback_t SomeCallback, const Log SomeLog)  {
  cb = SomeCallback;
  log = SomeLog;
}

void TestIt(const char *str) {
  assert(cb);
  cb(log, str);
}

(请注意,我只是将其作为 C 中的练习来完成,因为您必须使用的 C++ 接口对我们来说也可能是 C)。

有了这些,您就可以为 SWIG 编写一个接口文件,例如:

%module test 

%{
#include "test.h"
#include <assert.h>

// NEW: global variables (bleurgh!)
static jobject obj;
static JavaVM *jvm;

// 2:
static void java_callback(Log l, const char *s) {
  printf("In java_callback: %s\n", s);
  JNIEnv *jenv = 0;
  // NEW: might as well call GetEnv properly...
  const int result = (*jvm)->GetEnv(jvm, (void**)&jenv, JNI_VERSION_1_6);
  assert(JNI_OK == result);
  const jclass cbintf = (*jenv)->FindClass(jenv, "Callback");
  assert(cbintf);
  const jmethodID cbmeth = (*jenv)->GetMethodID(jenv, cbintf, "Log", "(LLog;Ljava/lang/String;)V");
  assert(cbmeth);
  const jclass lgclass = (*jenv)->FindClass(jenv, "Log");
  assert(lgclass);
  const jmethodID lgmeth = (*jenv)->GetStaticMethodID(jenv, lgclass, "swigToEnum", "(I)LLog;");
  assert(lgmeth);
  jobject log = (*jenv)->CallStaticObjectMethod(jenv, lgclass, lgmeth, (jint)l);
  assert(log);
  (*jenv)->CallVoidMethod(jenv, obj, cbmeth, log, (*jenv)->NewStringUTF(jenv, s));
}

%}

// 3:
%typemap(jstype) Callback_t "Callback";
%typemap(jtype) Callback_t "Callback";
%typemap(jni) Callback_t "jobject";
%typemap(javain) Callback_t "$javainput";

// 4: (modified, not a multiarg typemap now)
%typemap(in) Callback_t {
  JCALL1(GetJavaVM, jenv, &jvm);
  obj = JCALL1(NewGlobalRef, jenv, $input);
  JCALL1(DeleteLocalRef, jenv, $input);
  $1 = java_callback;
}

%include "test.h"

一般来说,将 1-1 映射到 earlier answer,除了将持有回调信息的 struct 替换为全局变量和 improving the way we get JNIEnv inside the callback

使用我们手动编写的Callback.java:

public interface Callback {
  public void Log(Log log, String str);
}

这足以让这个测试用例成功编译并运行:

public class run implements Callback {
  public static void main(String[] argv) {
    System.loadLibrary("test");
    run r = new run();
    test.SetCallback(r, Log.blah);
    test.TestIt("Hello world");
  }

  public void Log(Log l, String s) {
    System.out.println("Hello from Java: " + s);
  }
}

哪个有效:

swig -Wall -java test.i
gcc -Wall -Wextra -o libtest.so -shared -I/usr/lib/jvm/java-1.7.0-openjdk-amd64/include/ -I/usr/lib/jvm/java-1.7.0-openjdk-amd64/include/linux/ test.c test_wrap.c  -fPIC 
javac *.java && LD_LIBRARY_PATH=. java run
In java_callback: Hello world
Hello from Java: Hello world

由于我们不喜欢使用这样的全局变量(从 Java 端多次调用 SetCallback 不会像我们期望的那样运行)我首选的解决方案(在纯 C 世界中)是使用 libffi为我们生成一个闭包。从本质上讲,这让我们可以为每个活动回调创建一个新的函数指针,以便每次回调发生时都可以隐式传递有关正在调用哪个 Java 对象的知识。 (这是我们正在努力解决的问题。Libffi has an example of closures 非常适合我们的场景。

为了说明这一点,测试用例没有改变,SWIG 接口文件变成了这样:

%module test 

%{
#include "test.h"
#include <assert.h>
#include <ffi.h>

struct Callback {
  ffi_closure *closure;
  ffi_cif cif;
  ffi_type *args[2];
  JavaVM *jvm;
  void *bound_fn;
  jobject obj;
};

static void java_callback(ffi_cif *cif, void *ret, void *args[], struct Callback *cb) {
  printf("Starting arg parse\n");
  Log l = *(unsigned*)args[0];
  const char *s = *(const char**)args[1];
  assert(cb->obj);
  printf("In java_callback: %s\n", s);
  JNIEnv *jenv = 0;
  assert(cb);
  assert(cb->jvm);
  const int result = (*cb->jvm)->GetEnv(cb->jvm, (void**)&jenv, JNI_VERSION_1_6);
  assert(JNI_OK == result);
  const jclass cbintf = (*jenv)->FindClass(jenv, "Callback");
  assert(cbintf);
  const jmethodID cbmeth = (*jenv)->GetMethodID(jenv, cbintf, "Log", "(LLog;Ljava/lang/String;)V");
  assert(cbmeth);
  const jclass lgclass = (*jenv)->FindClass(jenv, "Log");
  assert(lgclass);
  const jmethodID lgmeth = (*jenv)->GetStaticMethodID(jenv, lgclass, "swigToEnum", "(I)LLog;");
  assert(lgmeth);
  jobject log = (*jenv)->CallStaticObjectMethod(jenv, lgclass, lgmeth, (jint)l);
  assert(log);
  (*jenv)->CallVoidMethod(jenv, cb->obj, cbmeth, log, (*jenv)->NewStringUTF(jenv, s));
}

%}

// 3:
%typemap(jstype) Callback_t "Callback";
%typemap(jtype) Callback_t "long";
%typemap(jni) Callback_t "jlong";
%typemap(javain) Callback_t "$javainput.prepare_fp($javainput)";

// 4:
%typemap(in) Callback_t {
  $1 = (Callback_t)$input;
}

%typemap(javaclassmodifiers) struct Callback "public abstract class"
%typemap(javacode) struct Callback %{
  public abstract void Log(Log l, String s);
%}

%typemap(in,numinputs=1) (jobject me, JavaVM *jvm) {
  $1 = JCALL1(NewWeakGlobalRef, jenv, $input);
  JCALL1(GetJavaVM, jenv, &$2);
}

struct Callback {
  %extend {
    jlong prepare_fp(jobject me, JavaVM *jvm) {
      if (!$self->bound_fn) {
        int ret;
        $self->args[0] = &ffi_type_uint;
        $self->args[1] = &ffi_type_pointer;
        $self->closure = ffi_closure_alloc(sizeof(ffi_closure), &$self->bound_fn);
        assert($self->closure);
        ret=ffi_prep_cif(&$self->cif, FFI_DEFAULT_ABI, 2, &ffi_type_void, $self->args);
        assert(ret == FFI_OK);
        ret=ffi_prep_closure_loc($self->closure, &$self->cif, java_callback, $self, $self->bound_fn);
        assert(ret == FFI_OK);
        $self->obj = me;
        $self->jvm = jvm;
      }
      return *((jlong*)&$self->bound_fn);
    }
    ~Callback() {
      if ($self->bound_fn) {
        ffi_closure_free($self->closure);
      }
      free($self);
    }
  }
};

%include "test.h"

这实现了我们通过使用 libffi 创建闭包来移除全局变量的目标。 Callback 现在已成为一个抽象类,由 C 和 Java 组件混合实现它。它的目标实际上是保存抽象方法Log 的实现,并管理需要保存以实现它的其余 C 数据的生命周期。大多数 libffi 工作是在 %extend SWIG 指令中完成的,该指令几乎反映了闭包的 libffi 文档。 java_callback 函数现在使用它传入的用户定义参数来存储它需要的所有信息,而不是全局查找,并且必须通过 ffi 调用传递/接收函数参数。我们的Callback_t 类型映射现在利用我们通过%extend 添加的额外函数来帮助设置指向我们真正需要的闭包的函数指针。

这里要注意的重要一点是,您在 Java 端负责管理回调实例的生命周期,无法从 C 端使该信息可见,因此过早的垃圾回收是一种风险。

要编译和运行它,工作 implements 需要在 run.java 中变为 extends 并且编译器需要添加 -lffi。除此之外,它像以前一样工作。


由于在您的实例中被包装的语言是 C++ 而不是 C,我们实际上可以通过 SWIG 的导向器功能来稍微简化一些 JNI 代码来帮助我们。然后变成:

%module(directors="1") test

%{
#include "test.h"
#include <assert.h>
#include <ffi.h>
%}

%feature("director") Callback;

// This rename makes getting the C++ generation right slightly simpler
%rename(Log) Callback::call;

// Make it abstract
%javamethodmodifiers Callback::call "public abstract"
%typemap(javaout) void Callback::call ";"
%typemap(javaclassmodifiers) Callback "public abstract class"
%typemap(jstype) Callback_t "Callback";
%typemap(jtype) Callback_t "long";
%typemap(jni) Callback_t "jlong";
%typemap(javain) Callback_t "$javainput.prepare_fp()";
%typemap(in) Callback_t {
  $1 = (Callback_t)$input;
}

%inline %{
struct Callback {
  virtual void call(Log l, const char *s) = 0;
  virtual ~Callback() {
    if (bound_fn) ffi_closure_free(closure);
  }

  jlong prepare_fp() {
    if (!bound_fn) {
      int ret;
      args[0] = &ffi_type_uint;
      args[1] = &ffi_type_pointer;
      closure = static_cast<decltype(closure)>(ffi_closure_alloc(sizeof(ffi_closure), &bound_fn));
      assert(closure);
      ret=ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_void, args);
      assert(ret == FFI_OK);
      ret=ffi_prep_closure_loc(closure, &cif, java_callback, this, bound_fn);
      assert(ret == FFI_OK);
    }
    return *((jlong*)&bound_fn);
  }
private:
  ffi_closure *closure;
  ffi_cif cif;
  ffi_type *args[2];
  void *bound_fn;

  static void java_callback(ffi_cif *cif, void *ret, void *args[], void *userdata) {
    (void)cif;
    (void)ret;
    Callback *cb = static_cast<Callback*>(userdata);
    printf("Starting arg parse\n");
    Log l = (Log)*(unsigned*)args[0];
    const char *s = *(const char**)args[1];
    printf("In java_callback: %s\n", s);
    cb->call(l, s);
  }
};
%}

%include "test.h"

这个 .i 文件极大地简化了 java_callback 内部所需的代码,现在可以直接替代之前的 libffi 和 C 实现。几乎所有的变化都与明智地启用导演和修复一些 C 主义有关。我们现在需要做的就是在我们的回调中调用纯虚拟 C++ 方法,而 SWIG 已经生成了处理其余部分的代码。

【讨论】:

  • 非常感谢,非常感谢您的努力。我将使用最后一个 .i 实现。
  • @arminsh 仅供参考,自从我最初编写它以来,我已经清理了最后一个 .i 实现
  • 使用你的最新实现,我有 99% 的几率得到Unhandled exception at 0x00007FFF404AC3C3 (****.dll) in java.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF. 我猜这是关于你警告过我的过早垃圾收集?我应该如何管理 run r = new run(); 的生命周期?我尝试将r 声明为finalr.swigReleaseOwnership(); 无济于事。有时,Log 函数会以随机间隔运行,我可以打印消息。
  • @ArminSh 在调用 TestIt 之后添加System.out.println(r); 是否可以使其可靠地工作?如果是这样,那肯定是垃圾收集问题 - 我会考虑一个更清洁的解决方案。
  • 我没有 TestIT 功能,但我将System.out.println(r); 放在SetCallback() 之后和main 的末尾,它不起作用。我想需要注意的一件事是,我通过 nuget 将 libffi 添加到了我的 VS2013 解决方案中,因为在 Windows 中构建 libffi 很痛苦。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-19
  • 2011-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多