最近想在C/C++中解析xml文件,本来开始的时候xml的模式比较简单,通过一些C语言中的字符串处理API就可以了,但是后续因为一些原因需要解析整个xml文档,考虑到代码库中已有的xml解析模块,有:
- libtinyxml:c++编写;
- libxml:C语言编写;
考虑到后续的发展,决定使用C++的xml解析库——libtinyxml。在开发代码的过程中,发现自己对C++的理解有漏洞,趁此机会查缺补漏一下。
如何编译libtinyxml库?
因为之前从未使用C++进行开发,因此对于C++的编译和构建过程极为模糊,而libtinyxml下载源码后,需要自己来编译,我简单看了一下,发现其代码不多,且makefile很短,可以帮助理解makefile。
认识makefile
开发人员使用的多数都是高级语言,这些高级语言可以分为两种:
- 编译型语言:典型如C和C++;
- 解释型语言:如Python等;
如果让这些高级语言可运行的话,需要将它们转换为机器语言,这个过程称为编译(compile)。不同的文件之间存在着各种依赖关系,在进行编译时,如何安排不同文件之间的编译依赖顺序,称为一个构建(build)过程。
不同编程语言都有自己的构建工具,比如Java中的Maven和Gradle等,这些工具可以根据特定的场景、文件类型、依赖关系等等,设置不同的profile,来满足开发者的需求。而在C/C++语言中,常用的是用make工具。
make简单来说是一个命令,在linux/MacOS中,make如果不加以说明的话,指的是GNU make utility。
make - GNU make utility to maintain groups of programs.
该工具不仅可以用来进行构建工程的,其规定,当指定文件发生变化时,可以运行特定的指令。
按照theicfire的说法,Make can also be used beyond compilation too, when you need a series of instructions to run depending on what files have changed.
如果要执行make命令,需要一个makefile,这里会有点概念上的混乱,make指什么?
- 狭义上,指的是GNU make utility;
- 广义上,针对C/C++语言的make构建工具有很多,如QT的qmake ,微软的MS nmake等,它们都可以叫make;
但是,它们都需要一个makefile,make本身不会执行编译,而是执行编译规则,通过写在makefile中编译命令,所以make更多的可以理解为一个makefile解析器,解析该文件并执行对应的命令。
与make类似的工具有Google的Ninja,在Android和Chrome的开发中有使用,相比make而言,ninja功能没有那么强大,但是正因为功能有限,所以启动编译的速度很快!
本文不会对make进行详述,因为这样的文章太多了,之后在后续的libtinyxml例子中说明,可以参考如下资料:
不同的make工具支持的makefile格式可能略有差别。因此,我们自己编写的makefile可能移植性不高,这也是一些跨平台的构建工具的目的,比如CMake和SCons,它们产生可移植的makefile,并简化动手写makefile时的巨大工作量,它们都是make的上层工具,可以用来产生makefile,与make等不冲突。
了解libtinyxml的makefile
根据libtinyxml中的makefile可以了解一下行常用的规则。
# 用':='定义变量
DEBUG := NO
PROFILE := NO
TINYXML_USE_STL := NO
CC := gcc
CXX := g++
LD := g++
AR := ar rc
RANLIB := ranlib
DEBUG_CFLAGS := -Wall -Wno-format -g -DDEBUG
RELEASE_CFLAGS := -Wall -Wno-unknown-pragmas -Wno-format -O3
LIBS :=
# 用${}调用变量
DEBUG_CXXFLAGS := ${DEBUG_CFLAGS}
RELEASE_CXXFLAGS := ${RELEASE_CFLAGS}
DEBUG_LDFLAGS := -g
RELEASE_LDFLAGS :=
# 使用判断语句
ifeq (YES, ${DEBUG})
CFLAGS := ${DEBUG_CFLAGS}
CXXFLAGS := ${DEBUG_CXXFLAGS}
LDFLAGS := ${DEBUG_LDFLAGS}
else
CFLAGS := ${RELEASE_CFLAGS}
CXXFLAGS := ${RELEASE_CXXFLAGS}
LDFLAGS := ${RELEASE_LDFLAGS}
endif
ifeq (YES, ${PROFILE})
CFLAGS := ${CFLAGS} -pg -O3
CXXFLAGS := ${CXXFLAGS} -pg -O3
LDFLAGS := ${LDFLAGS} -pg
endif
ifeq (YES, ${TINYXML_USE_STL})
DEFS := -DTIXML_USE_STL
else
DEFS :=
endif
INCS :=
CFLAGS := ${CFLAGS} ${DEFS}
CXXFLAGS := ${CXXFLAGS} ${DEFS}
OUTPUT := xmltest
# 声明第一个target为all,其含义在于:如果要构建all,必须执行${OUTPUT}
# 默认情况下,make会找第一个target执行
all: ${OUTPUT}
SRCS := tinyxml.cpp tinyxmlparser.cpp xmltest.cpp tinyxmlerror.cpp tinystr.cpp
SRCS := ${SRCS}
# 使用函数addsuffix将所有源文件的后缀名变为.o
OBJS := $(addsuffix .o,$(basename ${SRCS}))
# 声明第二个目标${OUTPUT},这个是一个对变量的调用;
# 当${OBJS}发生变化后(包括开始没有这些文件),会触发${OUTPUT}下的commands的执行,即重新进行编译。
# 如果此时${OBJS}中的文件不存在,则会找到对应的target执行,如下下边%.o所示。
# 这里用到了$@,其代表target ${OUTPUT}自身。
${OUTPUT}: ${OBJS}
${LD} -o $@ ${LDFLAGS} ${OBJS} ${LIBS} ${EXTRA_LIBS}
# 使用模式匹配,确定了所有的.c/.cpp生成相应的.o文件的规则
# $< 指定target的前置条件中的第一个
%.o : %.cpp
${CXX} -c ${CXXFLAGS} ${INCS} $< -o $@
%.o : %.c
${CC} -c ${CFLAGS} ${INCS} $< -o $@
dist:
bash makedistlinux
# 这是一个伪目标,即目标本身不是文件
clean:
-rm -f core ${OBJS} ${OUTPUT}
depend:
#makedepend ${INCS} ${SRCS}
# 说明下面4个target分别需要两个前提条件才能运行,没有这些也是可以编译的
tinyxml.o: tinyxml.h tinystr.h
tinyxmlparser.o: tinyxml.h tinystr.h
xmltest.o: tinyxml.h tinystr.h
tinyxmlerror.o: tinyxml.h tinystr.h
简单总结一下:
makefile的核心是一个构建规则,包括3个组件:
target:构建的目标
- 可以是文件;也可以是伪目标,即一个label;
- 默认,make执行第一个target all;
What does “all” stand for in a makefile? 专门针对all进行了解释。
prerequisites
- 设置一组条件,用来判断是否重新构建target;
- 只要有一个前置条件不存在或者时间比target新,就会触发重新构建;
- 如果一个前置条件(文件)不存在,需要写一个新的构建规则,生成该文件;
- 一个target没有任何前置条件,说明其独立于任何条件;
commands:表明如何更新目标文件,是构建目标的指令;
- 与prerequisites两者之间,必须有一个;
编译libtinyxml
如果理解了makefile运行的逻辑,执行libtinyxml编译的过程也能大概了解了:
- 执行make或者make all;
- all依赖OUTPUT,发现OUTPUT不存在,执行该target;
- OUTPUT依赖OBJS,发现OBJS不存在,执行对应的target,得到.o文件;
- 然后再回头执行OUTPUT
运行过程也如预期一样:
g++ -c -Wall -Wno-unknown-pragmas -Wno-format -O3 tinyxml.cpp -o tinyxml.o
g++ -c -Wall -Wno-unknown-pragmas -Wno-format -O3 tinyxmlparser.cpp -o tinyxmlparser.o
g++ -c -Wall -Wno-unknown-pragmas -Wno-format -O3 tinyxmlerror.cpp -o tinyxmlerror.o
g++ -c -Wall -Wno-unknown-pragmas -Wno-format -O3 tinystr.cpp -o tinystr.o
ar rc -o libtinyxml.a tinyxml.o tinyxmlparser.o tinyxmlerror.o tinystr.o
tinyxml的使用可以分为静态链接库和动态链接库,对这两个部分的说明超出了本文的范围,下面简单说一下如何更改makefile进行编译。
如果不需要xmltest.cpp,可以在SRCS中删除。
编译静态链接库
在这种模式下,只需修改两处即可:
OUTPUT := libtinyxml.a
${OUTPUT}: ${OBJS}
${AR} -o $@ ${LDFLAGS} ${OBJS} ${LIBS} ${EXTRA_LIBS}
这里需要注意:
库的命名:libXXX.a
静态链接库的链接:使用ar命令,该命令在Ubuntu和MacOS中是不同的
- 这里是将之前编译得到的.o文件链接为静态链接库libtinyxml.a。
编译动态链接库
编译动态链接库相比于静态的方式要复杂一些。
# 加-fPIC
DEBUG_CFLAGS := -Wall -Wno-format -g -DDEBUG -fPIC
RELEASE_CFLAGS := -Wall -Wno-unknown-pragmas -Wno-format -O3 -fPIC
OUTPUT := libtinyxml.so
# 加 -shared和-fPIC
${OUTPUT}: ${OBJS}
${LD} -shared -o $@ ${LDFLAGS} ${OBJS} ${LIBS} ${EXTRA_LIBS} -fPIC
这里还有一个趣事,我在添加-shared的时候出现了问题,误用了短横线,导致命令一直执行错误,没想到这个短横线还有不同,如文章所示,这些Unicode符号看起来真的很像。
如何使用静态链接库和动态链接库?
完成静态连接和动态连接库的构建后,如何使用它们呢?两者的区别是链接的方式不同。
这准备一个小的demo演示一下如何使用tinyxml。
// demo.cpp
#define TIXML_USE_STL
#include "tinyxml.h"
#include <iostream>
int main(){
TiXmlDocument doc;
bool loadOkay = doc.LoadFile("utf8test.xml");
if(!loadOkay) {
std::cout << "load error" << std::endl;
}
doc.Print(stdout);
return 0;
}
另外一些需要主要的地方是,保证#define TIXML_USE_STL
,如果没有这个宏会出现如下错误:
.text._ZN11TiXmlString4quitEv[ZN11TiXmlString4quitEv]+0x16): undefined reference to `TiXmlString::nullrep‘
源文件的目录结构如下:
|——tinyxml
| |——tinyxml.h
| |——libtinyxml.so
| |——libtinyxml.a
| |——demo.cpp
使用静态链接库
为了通过静态链接来使用tinyxml,可以有两种方式:
第一种:直接指定静态链接库libtinyxml.a
g++ -static demo.cpp libtinyxml.a -o demo
第二种
g++ -static demo.cpp -L . -ltinyxml -o demo
这种是通过指定库的加载位置和库的名称实现的:
-L .
:表明会在当前目录搜索静态链接库;-ltinyxml
:表示加载名称为libtinyxml.a的静态链接库,这里加载的是静态链接库还是动态链接库取决于是否有-static
参数;
对于生成的可执行文件,如何判断是静态链接还是动态链接,这里可以使用两个命令:
file demo
:判断是否为静态链接;demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=8319f8bcb8d2217e10d165b27c402fa5470a61ba, not stripped
ldd demo
:通过分析动态链接库的依赖关系,判断是否是动态链接;not a dynamic executable.
使用动态链接库
使用动态链接库进行编译时,使用如下命令:
g++ demo.cpp -L . -ltinyxml -o demo
但是实际运行时会报出如下错误:
./demo: error while loading shared libraries: libtinyxml.so: cannot open shared object file: No such file or directory.
根据错误信息发现,缺少对应的so库,可以通过ldd
查看少了哪些so:
linux-vdso.so.1 (0x00007ffc2c7c7000)
libtinyxml.so => not found
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f365757f000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f3657367000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3656f76000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f3656bd8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3657b0b000)
明显是libtinyxml.so找不到,说明在编译demo可执行文件的时候,找到了对应的so,但是在demo可执行文件中并没有保存该so的路径信息,因此在运行时会找不到,这里可以通过设置一个环境变量来解决:
# 将当前目录添加到动态链接库的搜索路径中
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
此时重新执行./demo就发现正常运行了。这个环境变量说明的是在运行中dynamic link loader如何链接该动态链接库,如文章所述,当然解决这个问题的不止一种方法,甚至该方法只能算是临时方法,不是最推荐的方法,但是本文不打算详述所有方法,那是另一个话题了。
总结
本文的内容主要包括以下内容:
- 初步认识&了解make和makefile;
- 完成了libtinyxml的编译;
- 学习了如何编译和使用静态链接库和动态链接库;