原文地址:thatonegamedev.com/cpp/how-to-…
原文作者:thatonegamedev.com/
发布时间:2021-01-14
CMake是C/C++中使用最多的代码项目工具。它被广泛使用,但对于初学者来说,它的学习曲线也很陡峭。在任何代码项目中,最核心的事情之一就是管理依赖关系,因为对于小团队来说,编写和了解每一个主题将是非常困难的。通常有现成的库在那里,你只需要找到它们。
比如说,你想建立一个自定义的游戏引擎。你可以解决所有的问题--发明图形、数学、物理等。但如果从已经解决这些问题的公共库开始,会更容易。
在你开始之前
请记住,这在某种程度上是比较难管理的方法。请查看我的其他教程,了解如何使用包管理器管理它们。
当你想确保你的依赖关系总是正确的版本,并且你想把它们和你的项目一起编译的时候,这篇文章中描述的方法是很有用的。当你也使用CMake管理依赖关系时,它也是非常有用的,但这不是必须的。
我希望你已经熟悉了CMake的工作原理以及如何用C/C++编写代码。
子目录
在没有vcpkg这样的工具的情况下,我的方法是将它们添加为子目录。请记住,还有其他的方法来使用包和管理依赖关系,但我喜欢这种方法,因为它将所有的依赖关系放在项目中的同一相对层次,而且它更清晰,更容易进行初始编译。
在CMake项目中,你可以将你的代码分割成块(库),然后添加可执行文件(应用程序或测试)。在你继续之前,请在我的CMake目标文章中阅读更多关于这些的内容。你最好把它们分开到自己的目录中。CMake语言中的add_subdirectory命令只做一件事--获取相对于当前CMakeLists.txt目录的路径,并在该目录下执行CMakeLists.txt。因此,如果你把你的依赖关系下载到你的项目中作为一个子目录,你就可以添加它,然后把库链接到你的可执行文件中。
你也可以用同样的方法将你的代码分割成可消耗的模块,甚至为每个模块建立不同的资源库,这样你就可以在将来重复使用它们。
获取依赖关系
有几种方法可以让你的依赖关系适用于这种方法。如果您是在 Git SVN 下,可以使用 "git 子模块 "或 "git 子树 "命令集。
git 子模块
git子模块的方法会在你的git仓库中做一个目录来引用互联网上的另一个仓库。然后,Git提供了一整套命令来更新这个引用,并将该仓库的内容下载到该目录中。
git子模块的方法最有用的地方在于,它并不跟踪仓库的内容,它只是保留了一个引用。你也可以很容易地提交回初始版本库。
在你自己的管理仓库中使用这种方法,因为你可能会发现对它们进行imrpovements并直接提交回主仓库是很有用的。
阅读更多。Git 子模块官方文档
Git子树或Zip下载
我更喜欢git子树,因为它更容易更新,并且允许你修改依赖关系,而不需要将任何东西推送回原始仓库。如果你使用的是其他类型的源码控制,你可以直接下载你的依赖关系作为一个档案(zip),然后解压它们相对于你的项目。
git 子树的本质是将另一个仓库的内容复制到自己的仓库中。当你这样做时,它会自动提交。另外,这和下载一个zip文件并解压到自己的项目中,然后提交所有文件是一样的。
这在与外部项目合作时更有用,因为你永远不知道原始仓库会发生什么,无论发生什么,你都会有一份他们的代码副本。如果你使用的项目被关闭,你可以继续支持你自己的代码副本/分叉,你的项目将没有任何问题。
工程强度
因为这是一个广泛的话题,我想让文章简短一些,我会假设你已经知道如何下载一个repo,并在正确的地方解压。
我的一般方法是首先创建一个名为package的库。它将作为你所有依赖关系的唯一结合点。这个库会在相对于你的项目根目录的一个叫做Packages的子目录中。所以我的做法是创建一个名为Packages的文件夹,并在其中添加一个CMakeLists.txt。我还添加了一个Packages.h和Packges.cpp,这样以后我就可以在Packages.h文件中添加所有的依赖关系。然后所有的依赖关系都会以子目录的形式进入该文件。
root
├── Packages/
│ ├── ...dependency subdirectories...
│ ├── include/
│ │ └── Packages.h
│ ├── src/
│ │ └── Packages.cpp
│ └── CMakeLists.txt
├── Main/
│ ├── src/
│ │ └── ...game files...
│ └── CMakeLists.txt
└── CMakeLists.txt
在Packages目录下的CMakeLists.txt文件将包含这样的内容。
project(Packages)
# example raylib library
# add_subdirectory(raylib)
add_library(Packages STATIC include/Packages.h src/Packages.cpp)
# example linking raylib
# target_link_libraries(Packages raylib)
target_include_directories(Packages
PUBLIC
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
在注释的代码中,你可以看到一个如何将raylib库链接到packages库的例子。这通常是一个静态链接,但有些库有其他可选配置。
你的主目标应该是怎样的?
另外,为了说明以后如何使用packages项目,这里有一个使用packages库的可执行文件的例子。
project(Main LANGUAGES CXX)
add_executable(Main src/main.cpp)
target_link_libraries(Main PUBLIC Packages)
target_include_directories(Main
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
正如你在例子中所看到的,我们可以保持相当简单。我们在Packages项目中包含多少个依赖关系并不重要,因为main将只包含一个目标,这将使它更简单。
好吧......但是如何包含依赖关系呢?
这就是主要的问题。没有一个通用的方法! C和C++项目分散在不同的构建系统上:有些使用CMake,有些使用Make,有些使用SCons,等等。你懂的。所有这些项目的真实情况是,它们都有头文件,它们有构建成库的源文件。对于他们中的一些人来说,这将是很容易的,就像为其他人添加一个子目录一样难,因为要为他们实现整个构建系统。
我正在构建我自己的游戏引擎,我将向你展示一些最常见的库包括,如果你决定自己制作一个自定义引擎,你将不得不使用这些库。
Raylib
例如Raylib就很简单。你必须遵循Ray仓库中的文档,但或多或少你只需要添加。
# ...
add_subdirectory(raylib)
# ...
target_link_libraries(Packages raylib)
OpenGL / Glad
很高兴这个项目没有自带资源库。你可以自己从OpenGL规范中为你想使用的东西生成API头文件。要做到这一点,你可以去这个网站glad.dav1d.de/ ,然后给自己生成一个API。你最终会得到一个压缩包,里面有两个或多个树状文件,我把它们放到了一个叫 glad 的目录中。
glad
├── src/
│ └── glad.c
├── include/
│ ├── glad/
│ │ └── glad.h
│ └── KHR/
│ └── khrplatform.h
└── CMakeLists.txt
然后,我还在里面扔了一个CMakeLists.txt文件,并配置了一个自己的新项目。
project(glad LANGUAGES CXX)
add_library(glad STATIC
src/glad.c
include/glad/glad.h
include/KHR/khrplatform.h
)
target_include_directories(glad
PUBLIC
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
那么要添加它就很容易了。
# ...
# Some magic required on linux btw if you're working with OpenGL
find_package(OpenGL REQUIRED)
add_subdirectory(glad)
# ...
target_link_libraries(Packages glad ${OPENGL_LIBRARIES}
)
所以,我们涵盖了两种极端的情况--你必须包含一个目录,你必须创建另一个cmake项目,只是在头文件中。这不是很难吗?
ImGui
所以这里是一个比较敏感的情况。如果你使用的是外部厂商的代码,你一般不希望把CMakeLists.txt文件写在你复制外部代码和系统的目录里面。这使得如果他们不使用CMake的话,将你的构建系统纳入其中就有点棘手了。这里有两种方法--克隆并支持你自己的版本库的fork,并定期更新。
或者直接从外面的文件中创建自己的目标。例如,如果我们把imgui下载到一个目录imgui中,它在Packages/CmakeLists.txt中会是这样的。
add_library(imgui STATIC
imgui/imgui.h
imgui/imconfig.h
imgui/imgui_internal.h
imgui/imstb_rectpack.h
imgui/imstb_textedit.h
imgui/imstb_truetype.h
imgui/imgui.cpp
imgui/imgui_demo.cpp
imgui/imgui_draw.cpp
imgui/imgui_widgets.cpp
imgui/imgui_tables.cpp
imgui/backends/imgui_impl_glfw.h
imgui/backends/imgui_impl_glfw.cpp
imgui/backends/imgui_impl_opengl3.h
imgui/backends/imgui_impl_opengl3.cpp
)
target_link_libraries(imgui glad glfw)
target_include_directories(imgui
PUBLIC
$<INSTALL_INTERFACE:imgui>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/imgui>
)
# ...
target_link_libraries(Packages imgui)
Lua
例如,Lua是一个makefile项目。你也可以做同样的事情,但不要污染你的主包/CMakeLists.txt,而是像我们在 glad 中做的那样创建以下结构。
glad
├── src/
│ └── ...clone your lua here...
└── CMakeLists.txt
我不会骗你。对于其中的一些,你必须深入到C/C++代码中,动手去了解如何正确编译它们。我拿了lua的官方镜像仓库,并尝试先用下面的行来编译,将所有的源文件收集到一个cmake变量中。
file(GLOB Lua_Sources src/*.c)
add_library(lua STATIC Lua_Sources)
target_include_directories(lua
PUBLIC
$<INSTALL_INTERFACE:src>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
)
......它可以很好地生成项目,但随后向我抛出一个错误。结果发现这个错误是关于重复定义的符号。查看我的C/C++编译的文章,在那里我提到了最常见的编译错误。我发现这个库只能用一个叫onelua.c的源文件来编译,所以我把我的脚本改成了这个样子。
project(lua LANGUAGES C)
add_library(lua STATIC src/onelua.c)
target_include_directories(lua
PUBLIC
$<INSTALL_INTERFACE:src>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
)
...... 我又一次失败了。这次我的主文件不想编译,因为主函数是重复的。然后我不得不深入到Lua C代码中去搜索,发现有一个定义是需要把lua作为一个库而不是一个perter来构建的。要做到这一点的行是
target_compile_definitions(lua PRIVATE MAKE_LIB)
这就是将lua加入CMake项目的过程。其余的就和前面的一样了。
# ...
add_subdirectory(lua)
# ...
target_link_libraries(Packages lua)
Assimp
有些项目是在CMake中制作的,但是太复杂了,以至于它们暴露了CMake选项,你需要在构建库的时候进行切换。如果你是单独构建项目,这很容易,但如果你是在将引用的项目添加为子目录时进行,则需要一些魔法。以Assimp(这是一个3D模型加载库)为例,你可以定义你想包含哪些文件格式和操作来加载模型。例如,您可能只需要构建一个FBX加载器。
set(BUILD_SHARED_LIBS FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_NO_EXPORT TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_TESTS FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_INSTALL_PDB FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_ZLIB TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_ASSIMP_TOOLS FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_COLLADA_IMPORTER TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_OBJ_IMPORTER TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_FBX_IMPORTER TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_BLEND_IMPORTER TRUE CACHE BOOL "x" FORCE)
add_subdirectory(assimp)
# ...
target_link_libraries(Packages assimp)
选项是很难设置的,正因为如此,我们需要在添加相关的子目录之前,强制将其设置为某个值。如果名称太通用,比如BUILD_SHARED_LIBS,你可能还想取消设置,这样就不会影响你包含的其他包。
一些简单的例子
我还将给出一些代表更简单的例子的厉害的库和它们的仓库。GLFW , glm , EnTT , sol2 , spdlog , fmt , easy_profiler , OpenAL-soft 。
# you may also care to set the options
# LIBTYPE to STATIC
# ALSOFT_UTILS to FALSE
# ALSOFT_EXAMPLES to FALSE
# ALSOFT_BACKEND_DSOUND to FALSE if you are building with Github Actions on Windows
add_subdirectory(openal-soft)
add_subdirectory(entt)
add_subdirectory(glfw)
add_subdirectory(glm)
add_subdirectory(easy_profiler)
add_subdirectory(fmt)
add_subdirectory(spdlog)
add_subdirectory(sol2)
target_link_libraries(Packages
glfw
glm
OpenAL
assimp
easy_profiler
EnTT
fmt
spdlog
lua
sol2
)
你怎么知道...
每个项目都是不同的。困难的是,每个人都没有通用的方法,而且由于CMake本身实际上是一种有能力的编程语言,你可能需要深入了解如何包含你可能想要依赖的每个项目。通常有两个重要的问题需要你自己去回答。
你怎么知道在target_link_libraries里面要放什么?
这很棘手 - 你必须在依赖项目 CMakeLists.txt 文件中搜索,找到调用 add_library 命令的地方。然后无论目标名称是什么,都将是你需要在 target_link_libraries 中使用的内容。
如何知道有哪些选项
你需要仔细阅读你所依赖的 CMakeLists.txt 文件,以查看变量控制编译的每一个地方。有时你可能会在版本库中找到方便的文档来指导你怎么做,但那是很少见的。查找所有的 SET() 命令和所有的 OPTION() 命令。有时你甚至可能需要深入到C/C++代码中去看看是什么触发了某些预处理程序的定义。
结束语
这是一篇相当长的文章,我希望我没有把你吓跑。有时涉及到依赖关系时,它是相当复杂的,但这真的取决于你所包含的项目。从长远来看,这确实可以节省时间,因为包括已经解决的问题,你就不必自己去实现它们(这有时可能需要太深入的知识)。