以libtinyxml为例了解C++的make使用


最近想在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的编译;
  • 学习了如何编译和使用静态链接库和动态链接库;

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录