将log4j与SWIG / JNI集成?

时间:2016-06-02 16:39:37

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

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

一些可能性:

  1. 让我的C ++模块将字符串中的stdout / stderr捕获并返回并发送给记录器。缺点是C ++模块可能很昂贵,而且这是异步的。
  2. 让我的Java软件创建/管理C ++模块可以写入的临时文件。完成后,我的软件将读取然后删除该文件并将数据发送到slf4j。
  3. 有没有办法从log4j获取ostream我可以通过我的swig界面?
  4. 看起来我可以在swig中create a Director将字符串从C ++传递给Java以转发给记录器。
  5. 其他人如何解决这个问题?

1 个答案:

答案 0 :(得分:2)

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

  1. 我们如何捕获C ++输出(即挂钩iostream对象)。
  2. 我们如何将此挂钩插入生成的包装器中。
  3. 我们如何使用我们捕获的内容实际调用log4j。
  4. 在回答这个问题时,我编写了以下头文件来演示我的解决方案是如何工作的:

    #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使用的内部缓冲区交换为I' d实际上是在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 ++项目,我希望你有一个比iostream更好的日志框架。如果是这种情况,您可能最好使用适配器实现其输出接口,该适配器直接传递给Java,并保留时间戳,日志级别等信息。 (反之亦然)
    • 即使坚持使用iostream,Boost iostreams库也可以让 1 更容易编写一些内容,这些东西实际上会在它们发生的时候钩住输出写入而不是当前的挂钩技术(但是会引入更大的依赖关系,所以它是权衡)。您也可以在没有提升的情况下执行此操作,但basic_streambuf类相当笨拙。

    1 如果您有兴趣,我可以在这个答案中说明提升版本。