CMake使用进阶

为什么使用CMake

CMake是跨平台的构建工具,公司主要使用这个工具生成,掌握了CMake用法觉得自动化构建还是很容易的

cmake使用方法

1.基本结构

1
2
3
4
5
6
7
8
- project
- includes
- CMakeLists.txt
- *.h
- src
- CMakeLists.txt
- *.cc
- CMakeLists.txt

  1. 依赖CMakeLists.txt文件,项目主目标一个,主目录可指定包含的子目录
  2. 在项目CMakeLists.txt中使用project指定项目名称,add_subdirectory添加子目录
  3. 子目录CMakeLists.txt从父目录CMakeLists.txt继承设置

2.语法

  • 注释: 行注释,以#开头

  • 变量

    • 使用set命令显式定义及赋值
    • 在非if语句中,使用${}引用
    • if中直接使用变量名引用
    • 后续的set命令会清理变量原来的值
  • command (args …)

    • 命令不分大小写
    • 参数使用空格分隔
    • 使用双引号引起参数中空格
  • set(var a;b;c) <=> set(var a b c)

    • 定义变量var并赋值为a;b;c这样一个string list
  • add_executable(${var}) <=> add_executable(a b c)

    • 变量使用${xxx}引用
  • 条件语句

1
2
3
4
5
6
7
if(var) #var非empty,0,FALSE
...
elseif()
...
else()
...
endif()
  • 循环语句
1
2
3
4
5
6
7
8
9
10
第一种:
set(var a b c)
foreach(f ${var})
...
endforeach(f)

第二种:
while()
...
endwhile()

3.内部变量

1
2
3
4
5
6
7

1. CMAKE_C_COMPILER/CMAKE_CXX_COMPILER 指定C/C++编译器
2. CMAKE_C_FLAGS/CMAKE_CXX_FLAGS 设置C/C++编译参数
3. EXECUTABLE_OUTPUT_PATH 可执行文件的存放路径
4. LIBRARY_OUTPUT_PATH 库文件存放路径
5. CMAKE_BUILD_TYPE 编译类型(Debug,Release)
6. BUILD_SHARED_LIBS 编译动态链接库(ON,OFF)

4.命令(不分大小写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

project( test ) #指定项目名称,使用${test_SOURCE_DIR}表示项目根目录

include_directories #指定头文件的搜索路径

link_directories #指定动态链接库的搜索路径

add_subdirectory #包含子目录

add_executable #编译可执行程序,指定编译`add_executable(hello hello.cc header.cc)`

add_definitions #添加编译参数 `add_definitions("-Wall -g")`

target_link_libraries #添加链接库 `target_link_libraries( demo ${BOOST_LIBRARIES} )`

add_library #编译成静态库 `add_library(demo demo.cc)`

add_custom_target #添加自定义编译目标

set_target_properties #设置编译目标属性,OUTPUT_NAME,VERSION等

link_libraries(lib1 lib2 ) #所有编译目标链接相同的库

5.其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. MESSAGE({SEND_ERROR|STATUS|FATAL_ERROR} "this is message")        #打印编译信息,FATAL_ERROR会停止编译

2. AUX_SOURCE_DIRECTORIES #获得目录下所有源文件,添加新文件需要重新运行cmake .

3. FILE(GLOB SRC_FILES RELATIVE ${CMAKE_CURRENT_SOUCE_DIR} *.cpp) #显式添加编译文件列表,添加新文件无需重新运行cmake .

4. FIND_LIBRARY(Boost COMPONENTS thread system log REQUIRED) #查找并添加动态库(添加一部分)

5. OPTION 条件编译
OPTION(DEBUG_mode "this is debug." ON)
IF(DEBUG_mode)
add_definitions(-DDEBUG)
ENDIF()

6. ADD_DEPENDENCIES 添加编译依赖项

7. SET_TARGET_PROPERTIES 设置目标文件目录

8. CMAKE_MINIMUM_REQUIRED(VERSION 2.6) 设置cmake最小版本

9. LESS,GREATER,EQUAL 数字比较

10.STRLESSSTRGREATERSTREQUAL 字符串比较

6.C++ Server组使用范例

  • workspace

    • CMakeLists.txt #根目录cmake file
    • common_inc #头文件
    • common_lib #库文件
  • 编译common库

    • add_subdirectory(stt_common/branch/1.0)
    • add_subdirectory(stt_msgbus/branch/1.0_cluster)
    • add_subdirectory(stt_dy_common/trunk)
  • 编译具体项目

    • add_subdirectory(stt_dy_chatroom/trunk)

Win平台使用CMake

  • 像大多数开源软件一样,CMake和Unix/Linux的结合比Win好多了
  • 在Win下使用比较麻烦,当然这只是在链接各种第三方库的情况下
  • 而我们一般都在使用各种第三方库的情况下,CMake才比VS有优势
  • 下面将以CMake链接Boost的一个项目作为示例

语法规则

  • CMake的内建命令大部分都是通用的,但是对于Win下使用VS的编译器来说,需要注意以下几点:
    • 必须定义CMAKE_CXX_COMPILER为cl
    • 最好使用/EHsc的定义来避免链接的过程失败,因为VS有自己的安全检查方法
    • 可以使用WinDbg来确定可执行文件的依赖,然后针对性去除多余的LINK_LIBRARIES
    • 生成MakeFile或者VS的工程需要定义好平台架构,默认是x86,还可以定义Win64和arm,可以参阅cmake帮助

示例

  • 源文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

#include <boost/thread.hpp>
#include <boost/asio.hpp>
#include <boost/timer.hpp>
#include <boost/ref.hpp>
#include <boost/bind.hpp>


void test_timer(const boost::system::error_code& ec,boost::asio::deadline_timer& t,int& count)
{

if(!ec){
if(count++==5) return;
t.expires_from_now(boost::posix_time::seconds(1));
t.async_wait(boost::bind(&test_timer,boost::asio::placeholders::error,
boost::ref(t),boost::ref(count)));
std::cout<<"working\n";
}
}

int main(int argc, char const *argv[])
{

boost::asio::io_service io;


int count = 0;
boost::asio::deadline_timer tm(io);

tm.expires_from_now(boost::posix_time::seconds(1));
tm.async_wait(boost::bind(test_timer,boost::asio::placeholders::error,
boost::ref(tm),boost::ref(count)));
io.run();

return 0;
}
  • CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmake_minimum_required(VERSION 2.8)
set(CMAKE_CXX_COMPILER "cl")
set(CMAKE_CXX_FLAGS "/O2")
add_definitions("/EHsc")
#find_package(Boost COMPONENTS thread system REQUIRED)

include_directories(
D:\\boost_1_60_0
)

link_directories(
D:\\boost_1_60_0\\lib64-msvc-14.0
)

link_libraries(
boost_thread-vc140-mt-1_60
boost_system-vc140-mt-1_60
)

add_executable(test_vc 1.cc)
  • 构建过程
1
2
3
4
5
6
7
8
//生成VS2015 64位的应用,默认是x86
cmake -G "Visual Studio 14 2015 Win64" .
//VS编译打开目录下的解决方案,编译即可

//nmake使用,在有外部链接库的时候容易失败
cmake -G "NMake Makefiles" .
//找到对应平台的nmake命令,一定要一致
nmake

Protobuf使用手册

Protobuf协议文件定义

选择版本

  • syntax 声明可以选择protobuf的编译器版本(v2和v3)
    • syntax="proto2";选择2版本,各个字段必须明确标注编号以确定序列化后二进制数据字段的位置
    • syntax="proto3";选择3版本,没有强制使用字段编号

字段修饰符

  • required
    • 对于required的字段而言,编号初值是必须要提供的,否则字段的便是未初始化的
    • 对于修饰符为required的字段,序列化的时候必须给予初始化,否则程序运行会异常
  • optional
    • 对于optional的字段而言,如果未进行初始化,那么一个默认值将赋予该字段编号
    • 也可以指定默认值,如示例所示.
  • repeated
    • 对于repeated的字段而言,该字段可以重复多个,即每个编码单元可能有多个该字段
    • 在高级语言里面,我们可以通过数组来实现,而在proto定义文件中可以使用repeated来修饰,从而达到相同目的。
    • 当然,出现0次也是包含在内的。

字段类型

proto Type C++ Type Notes
int32 int32 有符号32位整型数,非固定长度编码,编码效率比sint32低
int64 int64 有符号64位整型数,非固定长度编码
uint32 uint32 无符号32位整型数,非固定长度编码
uint64 uint64 无符号64位整型数,非固定长度编码
sint32 int32 有符号32位整型数,非固定长度编码,效率较高
sint64 int64 有符号64位整型数,非固定长度编码,效率较高
fixed32 uint32 无符号,固定4字节编码,数据大于2^28时,效率比uint32高
fixed64 uint64 无符号,固定8字节编码,数据大于2^56时,效率比uint32高
sfixed32 int32 有符号,固定4字节编码
sfixed64 int64 有符号,固定8字节编码
bool bool 布尔值
float float 浮点数
double double 浮点数
string string 必须为UTF-8编码或者7bit ASCII字符串
bytes string 字节数组

字段类型对应二进制类型

字段类型 二进制类型 二进制编码值
int32,int64,uint32,uint64,sint32,sint64,bool,enum Varint(可变长度int) 0
fixed64,sfixed64,double 64bit固定长度 1
string,bytes,inner messages(内部嵌套),packaed repeated fields(repeated字段) Length-delimited 2
groups(deprecated) Start group 3
groups(deprecated) Endd group 4
fixed32,sfixed32,float 32bit固定长度 5

数据编码原则

1.Varints编码规则

  • protobuf编码基础是Varints,Varints是将一个整数序列化为一个或多个Bytes的方法,越小的整数,使用的Bytes越少
    • 每个byte最高位(msb)是标志位,0表示是最后一个byte,1表示该字段值还有后续byte
    • 每个byte低7位存放数值
    • Varints使用Little Endian(小端)字节序
  • 转换示例
1
2
3
4
dec(300) 
=> bin(00000001 00101100)
=> Little(00101100 00000001)
=> Encode(10101100 00000010)

2.消息编码规则

  • message都是以一组或多组key-value对组成,key和value分别采用不同的编码方式
  • 序列化时,将message中所有key-value序列化成二进制字节流。反序列化时,解析出所有key-value对,
    如果遇到无法识别的类型,则直接跳过。这种机制保证了旧有的编/解码在协议添加新的字段时,依旧可以正常工作
  • key由两部分组成,一部分是在定义消息时对字段的编号(field_num),另一部分是字段类型(wire_type,编号最大不超过536870911.
  • key编码方式filed_num<<3|wire_type,编码后的二进制长度是变长
  • varint(wire_type=0)编码规则:
    • int32,int64直接按照varint方法来编码,因此-1,-2这种负数由于补码数表示有很多1,所占的byte也比较多
    • sint32,sint64采用Zigzag方法来避免上述问题
    • 首先采用Zigzag方法,将正数、0和负数映射到无符号数上
    • 再采用varint编码方法
    • Zigzag映射规则
1
2
Zigzag(n) = (n<<1)^(n>>31) ,n为sint32时
Zigzag(n) = (n<<1)^(n>>63) ,n为sint64时
  • 映射值表
Original(原始值) (编码后的值)EncodeAs
0 0
-1 1
1 2
-2 3
2 4
-3 5
2147483647 4294967294
-2147483648 4294967295
  • 64bit(wire_type=1)和32bit(wire_type=5)编码是在key后跟上Little Endian字节的数值
  • string,bytes都属于length-delimited编码,length-delimited(wire_type=2)的编码方式:key+length+content

    • key的编码方式是统一的
    • length采用varints编码方式
    • content就是由length指定的长度的Bytes
  • 完整示例

    • type.proto

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      message IntType {
      optional int32 a_i32 = 1;
      optional int64 b_i64 = 2;
      optional sint32 c_s32 = 3;
      optional sint64 d_s64 = 4;
      optional sfixed32 e_sf32 = 5;
      optional sfixed64 f_sf64 = 6;
      }

      message UIntType {
      optional uint32 a_u32 = 1;
      optional uint64 b_u64 = 2;
      optional fixed32 c_f32 = 3;
      optional fixed64 d_f64 = 4;
      }

      message FType{
      optional float a_f = 1;
      optional double b_d = 2;
      optional bool c_b = 3;
      }

      message BSType{
      optional string a_s = 1;
      repeated bytes b_bs = 2;
      }
  • 初始值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //int
    type::IntType it;
    it.set_a_i32(-1);
    it.set_b_i64(-1);
    it.set_c_s32(-1);
    it.set_d_s64(-1);
    it.set_e_sf32(-1);
    it.set_f_sf64(-1);

    //uint
    type::UIntType ut;
    ut.set_a_u32(1);
    ut.set_b_u64(2);
    ut.set_c_f32(1);
    ut.set_d_f64(2);

    //float
    type::FType ft;
    ft.set_a_f(0.1);
    ft.set_b_d(0.2);
    ft.set_c_b(false);

    //bytes
    type::BSType bt;
    bt.set_a_s("hello,world.");
    bt.add_b_bs("h");
    bt.add_b_bs("0x1");
    bt.add_b_bs("d");
    bt.add_b_bs("ILV");
    bt.add_b_bs("0xf");
  • 二进制表示

    • IntType

      | 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
      | :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
      | int32 | 1 | 1<<3 OR 0 = 00001000 | -1 | 11111111(9 times) 00000001 | 00001000 11111111(9times) 00000001 | varints编码 |
      | int64 | 2 | 2<<3 OR 0 = 00010000 | -1 | 11111111(9 times) 00000001 | 00010000 11111111(9times) 00000001 | varints编码 |
      | sint32 | 3 | 3<<3 OR 0 = 00011000 | -1 | 00000001 | 00011000 00000001| 无符号变换 |
      | sint32 | 4 | 4<<3 OR 0 = 00100000 | -1 | 00000001 | 00100000 00000001| 无符号变换 |
      | sfixed32 | 5 | 5<<3 OR 5 = 00101101 | -1 | 11111111(4 times) | 00011000 11111111 11111111 11111111 11111111 | 固定4byte长度 |
      | sfixed32 | 6 | 6<<3 OR 1 = 00110001 | -1 | 11111111(8 times) | 00011000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 | 固定8byte长度 |

    • UIntType

      | 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
      | :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
      | uint32 | 1 | 1<<3 OR 0 = 00001000 | 1 | 00000001 | 00001000 00000001| 无符号 |
      | uint64 | 2 | 2<<3 OR 0 = 00010000 | 2 | 00000010 | 00010000 00000010| 无符号 |
      | fixed32 | 3 | 3<<3 OR 5 = 00011101 | 1 | 00000001 00000000(3 times) | 00011101 00000001 00000000(3 times) | 固定4byte长度 |
      | fixed64 | 4 | 4<<3 OR 1 = 00100001 | 2 | 00000010 00000000(7 times) | 00011101 00000001 00000000(7 times) | 固定8byte长度 |

    • FType

      | 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
      | :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
      | float | 1 | 1<<3 OR 5 = 00001101 | 0.1 | 11001101 11001100 11001100 00111101 | 00001101 11001101 11001100 11001100 00111101 | IEEE浮点数,4byte |
      | double | 2 | 2<<3 OR 1 = 00010001 | 0.2 | 10011010 10011001 10011001 10011001 10011001 10011001 11001001 00111111 | 00010001 10011010 10011001 10011001 10011001 10011001 10011001 11001001 00111111| IEEE浮点数,8byte |
      | bool | 3 | 3<<3 OR 0 = 00011000 | false | 00000000 | 00011000 00000000 | 固定1byte长度 |

    • BSType

      | 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
      | :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
      | string | 1 | 1<<3 OR 2 = 00001010 | “hello,world.” | 00001100(length) 01101000 01100101 01101100 01101100 01101111 00101100 01110111 01101111 01110010 01101100 01100100 00101110(ASCII) | length+content编码 |
      | bytes | 2 | 2<<3 OR 2 = 00010010 | “h”,”0x1”,”d”,”ILV”,”0xf” | 00000001 01101000 00010010 00000011 00110000 01111000 00110001 00010010 00000001 01100100 00010010 00000011 01001001 01001100 01010110 00010010 00000011 00110000 01111000 01100110 | 符号分割 |

3.编解码字段顺序

  • 编解码与字段顺序无关,由key-value机制就能保证
  • 对于未知的字段,编码的时候会把它写在序列化完的已知字段后面

嵌套与引用

1.嵌套定义

  • message定义中可以嵌套定义message,enum
  • 嵌套定义的单元外部可见,引用路径由外到内逐层引用

2.引用

  • 定义package相当于C++的namespace,外部引用时需要package名
  • 引用外部message定义,只需要import外部proto文件即可使用
  • 引用使用相对路径,生成文件时protobuf可以自动处理

完整示例

  • user.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//user.proto

syntax="proto2“;
package user;

message User{
required sfixed64 uid = 1;

enum PhoneType{
NONE = 1;
HOME = 2;
MOBILE = 3;
}

message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = MOBILE];
}

repeated PhoneNumber phone = 2;

optional sfixed32 age = 3;
}

message Admin {
required sfixed64 uid = 1;
optional User.PhoneNumber phone = 2;
}
  • room.proto
1
2
3
4
5
6
7
8
9
10
11
12
//room.proto

syntax="proto2";
package room;

import "relative_dir/user.proto";

message Room {
repeated sfixed64 rid = 1;
repeated user.User user = 2;
optional double profit = 3;
}

Protobuf编译使用

  • 由协议生成C++文件:protoc --cpp_out=${DIR} proto-file...
  • 多个协议文件依赖关系:protoc --cpp_out=${DIR} base-proto-file deliver-proto-file...
  • 依赖问题按相对路径处理import即可解决

cmake自动化编译

1
2
3
4
5
6
7
FIND_PACKAGE(Protobuf REQUIRED)
LINK_LIBRARIES(
protobuf
)

FILE(GLOB ProtoFiles RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.proto)
PROTOBUF_GENERATE_CPP(PROTO_SRCS PROTO_HDRS ${ProtoFiles} )

Protobuf序列化与反序列化

1.初始化

  • set_xxx()设置required,optional字段值
  • add_xxx()添加repeated字段值
  • set_xxx(int,x)设置repeated中元素的值

2.序列化

  • required字段需要初始化,可以通过IsInitialized来检查是否完成message对象的初始化
  • SerializedAsString(),SerializedToString(std::string*)序列化为std::string
  • SerializedToArray(void*,int)序列化为byte数组
  • SerializedToOstream(ostream*)序列化到输出流
  • ByteSize()获取二进制字节序的大小,可用于初始化存放容器

3.反序列化

  • ParseFromString(std::string& data)从字符串中反序列化
  • ParseFromArray(const void *,int)从字节序中反序列化
  • ParseFromIstream(istream*)从输入流中反序列化
  • has_xxx()用于检查相应字段是否存在数据
  • xxx_size()用于确定repeated字段是否存在,0表示未序列化

4.获取对象

  • xxx()返回required/optional字段的const值,只读模式,返回repeated列表的指针,用于修改
  • mutable_xxx()返回字段指针,用于修改
  • xxx(int)返回repeated字段列表的元素,只读

protobuf实现原理

1.protobuf的cache机制

  • protobuf message的clear()操作是存在cache机制的,它并不会释放申请的空间,这导致占用的空间越来越大。
  • 如果程序中protobuf message占用的空间变化很大,那么最好每次或定期进行清理。这样可以避免内存不断的上涨。

注意事项

  1. 嵌套定义时,被嵌套的结构体被解析成A_B形式,需要获取mutable_b()指针来初始化该字段,使用栈上的对象会导致程序异常
  2. 应用程序中使用protobuf,需要在退出程序时调用google::protobuf::ShutdownProtobufLibrary()以清理内存,否则会造成内存泄漏
,