CMake 中使用 protobuf/protobuf-c

本文主要提供一种在 C\C++ 项目中嵌入 Protobuf/Protobuf-C proto 源文件进行编译的应用方法。

Protobuf 的 proto 源文件在 C\C++ 项目构建需要首先通过 protoc 工具生成对应的 C++ 源码文件,然后参与编译过程。将这个工具生成过程也集成到 CMake 构建中会方便 proto 定义变更的及时跟进,也可以将 proto 文件同时纳入版本管理中。

在 CMake 中可以使用 execute_process 来手动调用 protoc 来进行源码生成,但是这样在每次 cmake 命令时都重新生成源码文件,导致依赖的其他构建目标也重新编译,在 proto 定义未修改的情况下无端增加了编译时间。本文提供的方法可以避免这个问题。

主要思路如下:

  1. proto 文件构建作为 cmake 工程一个的 subdirectory。允许多个 proto 文件参与构建,同时将生成的源码编译为库作为其他目标的链接对象,而不是以源码形式参与其他目标的编译。
  2. 利用 cmake 的 add_custom_command 和 add_custom_target 进行 proto 文件生成。将生成源码文件与 proto 源文件增加依赖关系,只有当 proto 文件更改时才需要重新生成。

在项目中,将项目使用的 proto 文件放到一个子目录比如 proto 中,在目录中同时增加一个 CMakeLists.txt 作为项目 subdirectory。项目主 CMakeLists.txt 去 add_subdirectory 这个 proto 目录,然后 include_directories 对应的头文件目录变量。

以下为 proto 中的 CMakeLists.txt示例,同时包括了 protobuf 和 protobuf-c 两个示例:

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 获取编译器
find_program(
    PROTOC_C
    protoc-c
    DOC "Protobuf-c Compiler (protoc-c)"
    REQUIRED
)
find_program(
    PROTOC_CXX
    protoc
    DOC "Protobuf Compiler (protoc)"
    REQUIRED
)

# 需要编译的 proto 文件
file (GLOB PROTO_SOURCE_FILES
    "${CMAKE_CURRENT_SOURCE_DIR}/*.proto"
)

set(PROTO_PATH    "${CMAKE_CURRENT_SOURCE_DIR}")
set(PROTO_C_OUT   "${CMAKE_CURRENT_BINARY_DIR}/gen_c")
set(PROTO_CXX_OUT "${CMAKE_CURRENT_BINARY_DIR}/gen_cxx")

file(MAKE_DIRECTORY ${PROTO_C_OUT})
file(MAKE_DIRECTORY ${PROTO_CXX_OUT})

# 使用 protoc 处理 proto 文件
foreach(input_proto ${PROTO_SOURCE_FILES})
    get_filename_component(DIR ${input_proto} DIRECTORY)
    get_filename_component(FILE_NAME ${input_proto} NAME_WE)

    set(OUTPUT_C_HEADER     "${PROTO_C_OUT}/${FILE_NAME}.pb-c.h")
    set(OUTPUT_C_SOURCE     "${PROTO_C_OUT}/${FILE_NAME}.pb-c.c")
    list(APPEND OUTPUT_SOURCES_C
        ${OUTPUT_C_HEADER} ${OUTPUT_C_SOURCE})

    set(OUTPUT_CXX_HEADER   "${PROTO_CXX_OUT}/${FILE_NAME}.pb.h")
    set(OUTPUT_CXX_SOURCE   "${PROTO_CXX_OUT}/${FILE_NAME}.pb.cc")
    list(APPEND OUTPUT_SOURCES_CXX
        ${OUTPUT_CXX_HEADER} ${OUTPUT_CXX_SOURCE})
endforeach()

add_custom_command(
    OUTPUT  ${OUTPUT_SOURCES_C}
    COMMAND ${PROTOC_C} --c_out=${PROTO_C_OUT} --proto_path=${PROTO_PATH} ${PROTO_SOURCE_FILES}
    DEPENDS ${PROTO_SOURCE_FILES}
    WORKING_DIRECTORY ${PROTO_PATH}
    COMMENT "Generate C Protobuf Source Files"
)1

add_custom_command(
    OUTPUT  ${OUTPUT_SOURCES_CXX}
    COMMAND ${PROTOC_CXX} --cpp_out=${PROTO_CXX_OUT} --proto_path=${PROTO_PATH} ${PROTO_SOURCE_FILES}
    DEPENDS ${PROTO_SOURCE_FILES}
    WORKING_DIRECTORY ${PROTO_PATH}
    COMMENT "Generate Cpp Protobuf Source Files"
)

add_custom_target(
    compile_c_protos
    DEPENDS ${OUTPUT_SOURCES_C}
)
add_custom_target(
    compile_cxx_protos
    DEPENDS ${OUTPUT_SOURCES_CXX}
)

# 设置生成源文件包含目录变量供上层引用
set(PROTO_GEN_C_INCLUDE_DIRS ${PROTO_C_OUT} PARENT_SCOPE)
set(PROTO_GEN_CXX_INCLUDE_DIRS ${PROTO_CXX_OUT} PARENT_SCOPE)

# 将生成的文件打包为库 proto_gen_c
# 程序可以链接到该库
add_library(proto_gen_c	${OUTPUT_SOURCES_C})
target_link_libraries(proto_gen_c protobuf-c)
add_dependencies(proto_gen_c compile_c_protos)

add_library(proto_gen_cxx ${OUTPUT_SOURCES_CXX})
target_link_libraries(proto_gen_cxx protobuf)
add_dependencies(proto_gen_cxx compile_cxx_protos)

在项目主 CMakeLists.txt 中,如下引用:

1
2
3
add_subdirectory(proto)
include_directories(${PROTO_GEN_C_INCLUDE_DIRS})
include_directories(${PROTO_GEN_CXX_INCLUDE_DIRS})

在需要使用 proto 的目标 CMakeLists.txt 脚本中,只需要目标链接 proto_gen_cxx 或 proto_gen_c 即可。

Nuklear 界面库使用

Nuklear 是一个 C 编写的、只有头文件的 GUI 界面库,也是 imgui 类的界面实现工具,支持多种渲染后端,包括 gdi/gdi+/D3D/X11/Xft/SDL/opengl 等,特别适合跨平台和工具软件的界面实现。

Nuklear 其实核心是实现了基于2D绘图的界面控件绘制以及交互事件处理,而底层的渲染和用户操作事件处理,则由使用的后端来实现。Nuklear 实现了大部分常用控件,包括 label、button、radio、checkbox、edit、slider、chart 等,但没有提供常用对话框比如 MessageBox 和 FileDialog(毕竟控件都只是绘制)。

下图是一个样例代码实现的界面,源码托管在 GitHub 上。其中使用了 tinyfiledialogs 来实 MessageBox 和 FileDialog 对话框功能。

Nuklear Demo

Nuklear 的文档都包含在头文件里,接口和样例齐全,WIKI 里也有部分控件的使用样例代码,这里说一些使用时的注意点:

  1. Nuklear 的界面在每一帧中都会重新绘制,所以没有其他界面库的事件循环和回调,通常实现一些功能需要在代码里保存一些状态。
  2. Nuklear 的控件布局是基于行的,建议多看一下头文件中关于 layout row 的说明和示例。
  3. Nuklear 支持多 window,这些 window 都在同一个系统窗口内。
  4. 可以自定义界面控件的外观,具体参见样例代码中的 style.c 文件。
  5. 推荐使用 gdp+(win) 和 xft(linux)后端,其他后端多数需要携带字体文件,如果需要显示中文,部署需要携带字体,如果只是英文,nuklear 自带了一块字体。
  6. 所有后端的实现代码可以参见 Nuklear 仓库的示例。
  7. tinyfiledialogs 库使用后部署无依赖,win 上基于 comdlg32 和 ole32,linux 是基于命令行调用的窗口。需要注意的是,在程序中调用对话框后会阻塞已有的 Nuklear 程序。
  8. Nuklear 支持在 window 中直接进行2D绘图,主要是调用 nk_window_get_canvas() 后使用自带的绘图函数进行绘制操作,需要注意的是坐标处理(建议使用 nk_window_get_content_region() 获取窗口范围)。

每个工具都有应用的长处和短处,Nuklear 适合快速原型、工具小软件或者编辑器,但并不适合实现复杂的、侧重用户交互的程序,在实际中有时也需要权衡。

PS: 在 Linux 上需要屏幕录制时,推荐软件 peek。

CORS 中继程序

随着 CORS 全国一张网的运行,类似千寻这样的网络差分在实际运用中越来越普遍。这里开源一个可以中继重分发已有 CORS 服务的程序 cors-relay.

cors-relay 是为了解决现有 CORS 服务的一些限制性,就像 README 文档中提到的,cors-relay 就是针对以下需求场景开发的:

  1. 源 CORS 的服务重分发以及高效利用。使用多个源 CORS 帐号建立帐号池,然后使用 cors-relay 创建多用户,在保证源 CORS 帐号安全的情况下,重分发 CORS 服务。这么做,可以将包月包年的源 CORS 服务以灵活的服务期限进行分发(包天,包周,甚至几个小时)。同时,因为用户一般不会同时进行连接,那么可以进行超量分发,类似于飞机票的“超卖”。

  2. 同地区的差分广播。利用 cors-relay 客户端差分数据复用的特性,可以利用 1 个 CORS 帐号,实现地区的无限制差分数据服务,覆盖范围可以达到1000平方公里(30km x 30km)。

  3. 源 CORS 的安全分享。在不失去已有 CORS 帐号的安全控制前提下,提供新的用户名密码分发已有 CORS 服务给第三方。

现有的 CORS 服务,比如千寻,官方渠道都只提供包年包月的帐号,但在测绘施工实际使用中,由于项目进度或者天气原因,RTK 测量并不是连续的密集的使用,多数测量人员都希望有包天的服务,这样投入更少,也更灵活。利用 cors-relay 进行池化,可以重分发灵活期限的 CORS 服务,成规模后怎会大幅减少帐号购买的费用。

多人同地区作业也是如此,以往的电台作业中,一个基准站可以覆盖50KM以内的移动站需求。但现在,每个移动站都需要一个 CORS 帐号来进行作业,本来可以共用差分数据源的,却要付出成倍的成本。cors-relay 可以复用相近的差分源,覆盖同一地区只需要一个源 CORS 帐号。

cors-relay 是基于之前开源的 ntripcaster 项目开发的,期间也经历过实际运用测试。

目前 cors-relay 也有一些需要改进的地方,在 README 里也有提及,不过暂时可能没有太多时间花在这上面。

(完)