将 log4j 与 SWIG/JNI 集成?
Integrating log4j with SWIG/JNI?
我正在开发一个具有 C++ 模块的 Java 程序。我希望将我的 C++ stdout/stderr 发送到 slf4j/log4j 记录器。
一些可能性:
- 让我的 C++ 模块在字符串中捕获 stdout/stderr,该字符串被返回并发送到记录器。缺点是 C++ 模块可能很昂贵,而且这将是异步的。
- 让我的 Java 软件 create/manage 有一个 C++ 模块可以写入的临时文件。完成后,我的软件将读取并删除文件并将数据发送到 slf4j。
- 有什么方法可以通过我的 swig 接口从 log4j 获取 ostream 吗?
- 看起来我可以 create a Director swig 将字符串从 C++ 传递给 Java 以转发给记录器。
其他人是怎么解决这个问题的?
要将 C++(通过 SWIG 生成的包装器调用)记录到 log4j,您需要解决三个问题:
- 我们如何捕获 C++ 输出(即挂钩 iostream 对象)。
- 我们如何将这个挂钩插入到生成的包装器中。
- 我们如何使用捕获的内容实际调用 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::cout
和 std::cerr
都以正确的顺序进入 log4j。我已经 answered that question before, in this instance to keep it simple and portable I started out using rdbuf()
将 std::cout
和 std::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
来让替换发生在注释 "do the real call to C++ here".
的位置
对于问题 #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)
,它并不像听起来那么简单
- 此处日志事件的时间戳将是 "wrong",因为捕获的所有日志信息都将在基础 C++ 函数调用 returns 之后推出,而不是在其执行期间推出
- cout/cerr 消息混合在一起并记录在同一级别。我这样做是因为前一点,如果他们没有像这样混合,那么如果没有更多的工作就不可能获得相对顺序
- 如果您的 C++ 方法产生大量输出,缓冲区将变得相当大
- 如果您有一个大型 C++ 项目,我希望您使用的日志记录框架比 iostreams 更好。如果是这种情况,您最好使用适配器实现其输出接口,该适配器直接传递给 Java 并保留时间戳、日志级别等信息。 (反之亦然)
- 即使坚持使用 iostream,Boost iostreams 库也可以更轻松地1 编写一些实际上会在输出写入发生时挂钩的东西,而不是当前的挂钩技术(但是引入更大的依赖关系,所以这是一种权衡)。您也可以在没有提升的情况下进行,但是
basic_streambuf
class 相当笨拙。
1 如果您有兴趣,我可能可以在这个答案中说明升压版本。
我正在开发一个具有 C++ 模块的 Java 程序。我希望将我的 C++ stdout/stderr 发送到 slf4j/log4j 记录器。
一些可能性:
- 让我的 C++ 模块在字符串中捕获 stdout/stderr,该字符串被返回并发送到记录器。缺点是 C++ 模块可能很昂贵,而且这将是异步的。
- 让我的 Java 软件 create/manage 有一个 C++ 模块可以写入的临时文件。完成后,我的软件将读取并删除文件并将数据发送到 slf4j。
- 有什么方法可以通过我的 swig 接口从 log4j 获取 ostream 吗?
- 看起来我可以 create a Director swig 将字符串从 C++ 传递给 Java 以转发给记录器。
其他人是怎么解决这个问题的?
要将 C++(通过 SWIG 生成的包装器调用)记录到 log4j,您需要解决三个问题:
- 我们如何捕获 C++ 输出(即挂钩 iostream 对象)。
- 我们如何将这个挂钩插入到生成的包装器中。
- 我们如何使用捕获的内容实际调用 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::cout
和 std::cerr
都以正确的顺序进入 log4j。我已经 answered that question before, in this instance to keep it simple and portable I started out using rdbuf()
将 std::cout
和 std::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
来让替换发生在注释 "do the real call to C++ here".
对于问题 #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) ,它并不像听起来那么简单
- 此处日志事件的时间戳将是 "wrong",因为捕获的所有日志信息都将在基础 C++ 函数调用 returns 之后推出,而不是在其执行期间推出
- cout/cerr 消息混合在一起并记录在同一级别。我这样做是因为前一点,如果他们没有像这样混合,那么如果没有更多的工作就不可能获得相对顺序
- 如果您的 C++ 方法产生大量输出,缓冲区将变得相当大
- 如果您有一个大型 C++ 项目,我希望您使用的日志记录框架比 iostreams 更好。如果是这种情况,您最好使用适配器实现其输出接口,该适配器直接传递给 Java 并保留时间戳、日志级别等信息。 (反之亦然)
- 即使坚持使用 iostream,Boost iostreams 库也可以更轻松地1 编写一些实际上会在输出写入发生时挂钩的东西,而不是当前的挂钩技术(但是引入更大的依赖关系,所以这是一种权衡)。您也可以在没有提升的情况下进行,但是
basic_streambuf
class 相当笨拙。
1 如果您有兴趣,我可能可以在这个答案中说明升压版本。