【问题标题】:Integrating log4j with SWIG/JNI?将 log4j 与 SWIG/JNI 集成?
【发布时间】:2016-06-02 16:39:36
【问题描述】:

我正在开发一个具有 C++ 模块的 Java 程序。我希望将 C++ 的 stdout/stderr 发送到 slf4j/log4j 记录器。

一些可能性:

  1. 让我的 C++ 模块在字符串中捕获 stdout/stderr,该字符串返回并发送到记录器。一个缺点是 C++ 模块可能很昂贵,而且这将是异步的。
  2. 让我的 Java 软件创建/管理 C++ 模块可以写入的临时文件。完成后,我的软件会读取并删除文件并将数据发送到 slf4j。
  3. 有什么方法可以通过我的 swig 接口从 log4j 获取 ostream?
  4. 看来我可以 create a Director in swig 将字符串从 C++ 传递给 Java 以转发到记录器。

其他人是如何解决这个问题的?

【问题讨论】:

  • “最佳”方法可能相当复杂。也就是说,您将实现自己的 ostream 子类,将其结果发送到 log4j,并用该流替换 stdout/stderr。说起来容易做起来难。第一部分在这里解决:stackoverflow.com/questions/533038/… 第二部分是通过 JNI 调用 log4j。这是一个巨大的,很大的 PITA。原生 JNI 繁琐且容易出错。你为此打电话给 SWIG 主管,这可能是票,但我没有用过。
  • 如果您愿意接受一些妥协,解决方案将变得相当简单但简洁。 (仅限std::cout/std::cerr`,任意缓冲意味着时间戳仅在函数调用期间准确)

标签: java c++ java-native-interface log4j swig


【解决方案1】:

要让 C++(通过 SWIG 生成的包装器调用)日志记录到 log4j,您需要解决三个问题:

  1. 我们如何捕获 C++ 输出(即挂钩 iostream 对象)。
  2. 我们如何将此挂钩插入到生成的包装器中。
  3. 我们实际上如何使用我们捕获的内容调用 log4j。

在回答这个问题时,我编写了以下头文件来演示我的解决方案是如何工作的:

#include <iostream>

void test1() {
  std::cout << "OUT: " << "test1\n";
  std::cerr << "ERR: " << "test1\n";
}

struct HelloWorld {
  static void test2() {
    std::cout << "OUT: " << "test2\n";
    std::cerr << "ERR: " << "test2\n";
  }

  void test3() const {
    std::cout << "OUT: " << "test3\n";
    std::cerr << "ERR: " << "test3\n";
  }
};

最后,我希望看到std::coutstd::cerr 都以正确的顺序进入log4j。我有answered that question before,在这种情况下,为了保持简单和便携,我开始使用rdbuf()std::coutstd::cerr 使用的内部缓冲区交换为我在std::stringstream 中实际创建的缓冲区,例如:

std::stringstream out; // Capture into this

// Save state so we can restore it
auto old_buf = std::cout.rdbuf();
// Swap buffer on cout
std::cout.rdbuf(out.rdbuf());

// Do the real call to C++ here
// ...

// Reset things
std::cout.rdbuf(old_buf);

// Walk through what we captured in out

当然,这不会捕获来自 libc 函数(printf() 等)或系统调用(write() 等)的输出,但它会获得所有标准 C++ 输出。

所以这就是问题#1 从我们的列表中划掉了。对于问题 #2,SWIG 的 %exception directive 非常符合我们想要做的事情,它让我们有机会在对包装函数的调用被分派之前和之后执行 C++ 代码。在上面的示例中,我们需要做的就是使用特殊变量 $action 来让替换发生在注释“在这里对 C++ 进行真正的调用”。

对于问题 #3,我们需要进行一些 Java 调用。我开始认为 JNI 不会太糟糕,也许有点冗长。基本上我们要做的就是复制以下 Java 代码 (from the log4j docs):

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class HelloWorld {
    private static final Logger logger = LogManager.getLogger("HelloWorld");
    public static void main(String[] args) {
        logger.info("Hello, World!");
    }
}

但在 JNI 而不是 Java 内部,并将正确的字符串传递给 getLogger 调用。

因此,将所有这些整合到一个 SWIG 界面中,我们得到:

%module test

%{
#include "test.hh"
#include <sstream>
#include <cassert>
static const char *defaultLogname="$module"; // Use if we're not in a class
%}

// Exception handling for all wrapped calls
%exception {
  // Hook output into this:
  std::stringstream out;

  // Save old states
  auto old_outbuf = std::cout.rdbuf();
  auto old_errbuf = std::cerr.rdbuf();
  // Do actual buffer switch
  std::cout.rdbuf(out.rdbuf());
  std::cerr.rdbuf(out.rdbuf());
  try {
    $action
  }
  catch (...) {
    // TODO: use RAII instead of poor finally substitute?
    std::cout.rdbuf(old_outbuf);
    std::cerr.rdbuf(old_errbuf);
    throw;
  }
  // Restore state
  std::cout.rdbuf(old_outbuf);
  std::cerr.rdbuf(old_errbuf);

  // JNI calls to find mid and instance for Logger.error(String) for the right name
  static const std::string class_name = "$parentclassname";
  // prepare static call to org.apache.logging.log4j.LogManager.getLogger(String)
  // start with class lookup:
  jclass logmanagercls = JCALL1(FindClass, jenv, "org/apache/logging/log4j/LogManager");
  assert(logmanagercls);
  // find method ID for right overload of getLogger
  jmethodID getloggermid = JCALL3(GetStaticMethodID, jenv, logmanagercls, "getLogger", "(Ljava/lang/String;)Lorg/apache/logging/log4j/Logger;");
  assert(getloggermid);

  // Prep name strign to pass into getLogger
  jstring logname = JCALL1(NewStringUTF, jenv, (class_name.size() ? class_name.c_str(): defaultLogname));

  // Actually get the Logger instance for us to use
  jobject logger = JCALL3(CallStaticObjectMethod, jenv, logmanagercls, getloggermid, logname);
  assert(logger);
  // Lookup .error() method ID on logger, we need the jclass to start
  jclass loggercls = JCALL1(GetObjectClass, jenv, logger);
  assert(loggercls);
  // and the method ID of the right overload
  jmethodID errormid = JCALL3(GetMethodID, jenv, loggercls, "error", "(Ljava/lang/String;)V");
  assert(errormid);

  // Loop over all the lines we got from C++:
  std::string msg;
  while(std::getline(out, msg)) {
    // Pass string into Java logger
    jstring jmsg = JCALL1(NewStringUTF, jenv, msg.c_str());
    JCALL3(CallVoidMethod, jenv, logger, errormid, jmsg);
  }
}

// And of course actually wrap our test header    
%include "test.hh"

我添加了一些 Java 来证明这是可行的:

public class run {
  public static void main(String[] argv) {
    System.loadLibrary("test");
    test.test1();
    HelloWorld.test2();
    HelloWorld h1 = new HelloWorld();
    h1.test3();    
  }
}

在当前目录中使用 log4j 2.6 jar 编译并运行:

swig3.0 -c++ -java -Wall test.i
javac *.java
g++ -std=c++1y -Wall -Wextra -shared -o libtest.so test_wrap.cxx -I/usr/lib/jvm/default-java/include/ -I/usr/lib/jvm/default-java/include/linux -fPIC 
LD_LIBRARY_PATH=. java -classpath log4j-api-2.6.jar:log4j-core-2.6.jar:. run

运行时给出:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
22:31:08.383 [main] ERROR test - OUT: test1
22:31:08.384 [main] ERROR test - ERR: test1
22:31:08.386 [main] ERROR HelloWorld - OUT: test2
22:31:08.386 [main] ERROR HelloWorld - ERR: test2
22:31:08.386 [main] ERROR HelloWorld - OUT: test3
22:31:08.386 [main] ERROR HelloWorld - ERR: test3

讨论点:

  • JNI 是否冗长?肯定是的,但这是否足够糟糕到走另一条路?对我来说不是(尽管导演的想法是可行的,但由于complexities with producing interfaces,它并不像听起来那么简单)
  • 您的日志事件的时间戳将在此处“错误”,因为捕获的所有日志信息都将在底层 C++ 函数调用返回后推出,而不是在其执行期间推出
  • cout/cerr 消息混合并记录在同一级别。我这样做是因为前一点,如果它们没有像这样混合在一起,那么如果没有更多的工作,就不可能获得相对排序
  • 如果您的 C++ 方法产生大量输出,缓冲区将变得相当大
  • 如果您有一个大型 C++ 项目,我希望您使用的日志记录框架比 iostreams 更好。如果是这种情况,您最好使用一个适配器来实现其输出接口,该适配器直接传递给 Java,并保留时间戳、日志级别等信息。 (反之亦然)
  • 即使坚持使用 iostream,Boost iostreams 库也可以更轻松1编写一些实际挂钩输出写入的内容,而不是当前的挂钩技术(但引入更大的依赖关系,因此权衡)。你也可以在没有提升的情况下做到这一点,但是basic_streambuf 类相当笨拙。

1 如果您有兴趣,我可能可以在此答案中说明增强版本。

【讨论】:

  • 感谢您的回答!我对其进行了一些调整以使用 slf4j 并拦截我正在使用的第 3 方库(封闭源代码)的输出,它似乎可以工作。我也遇到了一些奇怪的缓冲问题——日志很长时间没有出现,当它们出现时,它就在 Finalizer 线程中。也许我使用 3rd 方工具的 ostream 处理程序的具体方式不是线程安全的。不管怎样,我把代码放在一个选择性异常处理程序中,这个问题似乎也消失了。
  • @amos 您对基于 boost streambuf 的解决方案感兴趣吗?
  • 我的代码正在使用第 3 方的日志记录工具,而现在对我来说,重构所有这些以提高性能比付出更多的努力。不过,感谢您的提议!
  • @amos 你不必重构它,它只是 std::stringstream 的一个插件,它提供时间戳、“实时”输出,而不是围绕函数调用进行缓冲以及 cerr/cout 的不同日志级别。除了 SWIG 模块(它将是内部模块)之外的所有模块都不需要更改。
猜你喜欢
  • 2019-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-19
  • 1970-01-01
相关资源
最近更新 更多