log4cplus全面介绍:最新版本、详细编译方法与使用指南
log4cplus是C++编写的开源的日志系统很有名也很强大,但网上大多的资料都是讲如何配置使用。关于编译过程一笔带过,尤其是windows下的编译过程少之又少。得先有编译这个提才有后续的使用,里面的坑也少有提及。以下对详细的编译过程和使用做个完整总结。
一、log4cplus介绍
log4cplus是C++编写的开源的日志系统,前身是java编写的log4j系统,受Apache Software License保护,作者是Tad E. Smith。
log4cplus具有线程安全、灵活、以及多粒度控制的特点,通过将日志划分优先级使其可以面向程序调试、运行、测试、和维护等全生命周期。你可以选择将日志输出到屏幕、文件、NT event log、甚至是远程服务器;通过指定策略对日志进行定期备份等等。
二、log4cplus下载
下载链接:https://sourceforge.net/projects/log4cplus/files/log4cplus-stable/2.0.7/
最新稳定版2.0.8(2022.8.15)
官方文档:https://sourceforge.net/p/log4cplus/wiki/Home/
github地址:https://github.com/log4cplus/log4cplus
国内镜像地址:mirrors / log4cplus / log4cplus · GitCode
最新版可能需要C++2017(不一定哦,实测vs2015编译2.0.8通过。官方介绍是3.0以后的才需要c++2017),这里也给个低版本的,支持用C++11的一个tag版本: GitHub - log4cplus/log4cplus at REL_2_0_4
三、编译生成库
Linux下编译
linux下的编译稍微简单些.
1- 解压: gzip -cd log4cplus-x.x.x.tar.gz | tar -xf -
2- 进入log4cplus根目录: cd log4cplus-x.x.x
3- 产生Makefile: ./configure --prefix=/where/to/install -enable-threads=no
如果需要指定安装路径可使用--prefix参数, 否则将缺省安装到/usr/local目录下。另外,如果需要单线程版本可通过参数-enable-threads=no指定, 否则默认将安装多线程版本。
另外需要注意的是,--enable-static这个参数是编译静态库的选项,默认是没打开的。需要静态库的话需要开启此选项。
windows下编译
windows下的编译稍麻烦些。
可以选择的编译方式有:Visual Studio,cmake,vcpkg等. 其中使用vcpkg是最简单省事的。
方式一、Visual Studio环境编译
On Windows, the primary build system is Visual Studio 2015 solution and projects (msvc14/log4cplus.sln
).
有windows下的Visual Studio环境的,可以打开Visual Studio,进入 log4cplus-2.x\msvc14目录下,运行log4cplus.sln解决方案。
1.选择log4cplus项目,属性里面的字符集和目标程序一致。右键——>属性——>配置属性——>常规——>字符集,选择Unicode字符集。
2.解决方案的平台与目标程序一致,这里选择的是x64。
3.版本也要与目标程序一致,这里选择的是release版本。
编译完成后,会在log4cplus-2.x\msvc14\x64\bin.Release文件夹下生成我们需要的log4cplus.lib和log4cplus.dll两个文件。
将log4cplus-2.0.x目录下的include文件夹拷贝到我们的目标程序文件夹中,这里面是我们需要的头文件。
简单的测试:
异步模式需要开启,不然多线程同时写日志会有问题。
以下是旧版本的才需要的。
旧版本的需要到 https://github.com/log4cplus/ThreadPool
把.h和.cpp文件下载下来,放到\log4cplus\log4cplus-REL_2_0_4\threadpool 目录里。
Catch-master
下载:Catch-master.zip 解压后,把文件夹中的内容复制到 log4cplus-REL_2_0_4\catch中。
新版本的不用这些操作,新版源码里已经包含了。
方式二、vcpkg环境
依赖包安装
1.使用神器vcpkg
2.vcpkg install log4cplus[core, unicode]:x86-windows
, 安装匹配项目的包(64位的是:x64-windows)
方式三、cmake环境编译
在下载的源码根目录下建个build_msvc文件夹,进入该文件夹,执行:
vc的工程
以上如果不指定生成的makefile类型,则默认生成的是vc的工程。
gcc工具链
若不需要vs的工程,则需额外指定生成的makefile文件类型如:
但以上生成的默认是使用的gcc工具链。
MinGW工具链
若使用MinGW
编译器 ,则需指定
生成的是MinGW Makefiles
,而不是生成 MSVC
的工程文件。
若想使用msvc的工具链和vc的nmake,怎么办呢?
msvc的工具链(nmake)
这个操作的前提是需要工具链和nmake在环境变量里能找到。
手工编译
手工编译的话,最简单的办法是直接进入vs自带的dos环境窗口:
以上是手工编译的详细过程。
自动化ps脚本编译
若要写成windows下的powershell脚本,则复用性更强些,可以写为:
经过以上操作,就输出了我们想要的log4cplusU.dll了,不过静态库竟然没生成。(其实是有生成的,在src文件夹里,没和动态库所在的bin文件夹一块儿)。使用时别忘把头文件也拷贝进自己的项目工程。
四、log4cplus库使用
加载库到工程
把需要的头文件放在工程目录下。把需要的log4cplusU.dll和log4cplusU.lib库也添加到工程项目里。
注意,虽然使用的是静态库,但是那个动态库也得放进去。否则虽然编译通过,但是跑不起来,会报找不到og4cplusU.dll的错误。
这里以cmake的工程为例,介绍下静态库的使用。CMakeLists.txt文件片段如下:
基本使用步骤
使用log4cplus有六个基本步骤:
1、实例化一个封装了输出介质的appender对象。
2、实例化一个封装了输出格式的layout对象。
3、将layout对象绑定(attach)到appender对象,如省略此步骤,简单布局器SimpleLayout(参见5.1小节)对象会绑定到logger。
4、实例化一个封装了日志输出logger对象,并调用其静态函数getInstance()获得实例,log4cplus::Logger::getInstance(“logger_name”)。
5、将appender对象绑定(attach)到logger对象。
6、设置logger的优先级,如省略此步骤,各种有限级的日志都将被输出。
代码示例
以上示例,使用了简单的Simplelayout布局器。输出格式可能不是我们想要的输出格式。一般常使用PatternLayout格式,示例如下:
遇到的坑
最后,本以为顺顺利利,结果竟出现了恼人的链接错误,大致内容为:
猜测原因难道是调用约定不一致?还是字符集问题?
关于调用约定
microsoft的vc默认的是__cdecl方式,而windows API则是__stdcall,如果用vc开发dll给其他语言用,则应该指定__stdcall方式。
1.__cdecl
所谓的C调用规则。按从右至左的顺序压参数入栈,由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的。返回值在EAX中因此,对于象printf这样变参数的函数必须用这种规则。编译器在编译的时候对这种调用规则的函数生成修饰名的饿时候,仅在输出函数名前加上一个下划线前缀,格式为_functionname。
2.__stdcall
按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,切记:函数自己在退出时清空堆栈,返回值在EAX中。__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。如函数int func(int a, double b)的修饰名是_func@12。
如何解决?
在编译库时首选要明确和统一调用方式。若是stdcall则在cmake中可尝试使用以下参数:
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--add-stdcall-alias")
经验证非此问题,那么最终原因最可能是字符集问题了。难怪编译的库都带个U,说明库本身是Unicode字符集的,那么代码里必须也得使用Unicode字符集。仅代码文件格式为utf-8行吗?
答案是不行的,需要代码文件里指定:#define UNICODE这个宏定义。猜测可能是log4plus的头文件里判断了这个宏来提供不同的接口。
字符集问题
由于我的导入库lib,以及动态库dll都是正确加载的,所以不是库没正确加载的问题,也不是release加载了debug版本的问题。
最可能是当前工程项目的“字符集”设置与log4plus所用的“字符集”不一致所导致的,log4cplus是使用的Unicode 符集,当前工程需要检查下是否是使用的Unicode字符集。这里面有个概念需要搞清楚啊,文件的编码格式为utf-8不代表使用的Unicode字符集!
- Unicode 是「字符集」
- UTF-8 是「编码规则」
最后果然成功啦,这时候激动的泪儿都要出来了,这坑有点儿深啊。
在main函数所在的代码文件的最上方定义:#define UNICODE 搞定。
注意:
#define UNICODE 必须是在文件的最上方,放在log4cplus下方不行!
还有其他的一些注意事项,网上的介绍少有提及,比如:
Linking on Windows
If you are linking your application with DLL variant of log4cplus, define LOG4CPLUS_BUILD_DLL
preprocessor symbol. This changes definition of LOG4CPLUS_EXPORT
symbol to __declspec(dllimport)
.
--with-qt5
This option is disabled by default. It enables compilation of a separate shared library (liblog4cplusqt5debugappender) that implements Qt5DebugAppender
. It requires Qt5 and pkg-config to be available.
Threads and signals
log4cplus is not safe to be used from asynchronous signals' handlers. This is a property of most threaded programmes in general. If you are going to use log4cplus in threaded application and if you want to use log4cplus from signal handlers then your only option is to block signals in all threads but one that will handle all signals. On POSIX platforms, this is possible using the sigwait()
call. log4cplus enables this approach by blocking all signals in any threads created through its threads helpers.
还是要好好多读以下官方的readme.md文档。
地址在这里:README.md · master · mirrors / log4cplus / log4cplus · GitCode
主进程无法退出问题
构造函数调用log4cplus::initialize(),析构函数调用log4cplus::Logger::shutdown()
log4cplus::Initializer m_initializer;//构造函数调用log4cplus::initialize(),析构函数调用log4cplus::Logger::shutdown()。
还没完:当你使用了ThreadPool之后,log4cplus总是会在main函数执行之前使用static机制初始化,并创建线程池。如果你在main函数退出的时候没有调用log4cplus::deinitialize();
则线程池中的线程不会退出。导致main函数无法退出。所以要在main函数退出之前调用这句话,如果不调用,那个log4cplus::Initializer m_initializer;只负责调用shutdown,而不负责调用clearThreadPool。只有log4cplus::deinitialize();才会既clearThreadPool又shutdown。
为何必须先调用log4cplus::initialize(),文档上有个解释如下:
还需要注意的是log4cplus::Initializer m_initializer;如果放到单独封装的单例类里的构造函数中是不行的,It will initialize the library and deinitialize it at the end of the constructor. The initializer
object must live throughout the use of the library.
可以单独封装一个接口,类似如下:
然后在主程序main函数return之前调用该函数。
封装使用
最后介绍下封装使用。新建两个文件,logger.h和logger.cpp.
日志的配置文件log4cplus.conf如下:
测试
单例封装
用于QT 项目
在qt项目中使用,把log4cplus的头文件拷入项目中,比如我的是根目录的includes文件夹。
在项目的.pro文件中,增加头文件的包含和链接库的脚本。
关于字符集,qt默认就是unicode字符集。为了保险起见,我在.pro文件中又设置了字符集如下:DEFINES += UNICODE _UNICODE
最后的最后,顺利的在qt项目中跑了起来。
上一篇: 示例:使用C语言递归函数的案例
推荐阅读
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面
-
log4cplus全面介绍:最新版本、详细编译方法与使用指南
-
在Windows下编译log4cplus:最新介绍、详细编译过程和使用指南(全面版)