coldnew's blog

使用 C++ 實現一個簡單的 Coding Agent: CMake 專案的建立

使用 C++ 實現一個簡單的 Coding Agent: 從 curl 呼叫開始 一文中,我們說明了如何透過 curl 去做出簡單的 chat 功能,現在我們想要將這個功能改成用 C++ 實做,因此就來建立我們的專案。

從 CMake 開始

CMake 是一個跨平台的建置系統工具,現在大部分的 C++ 專案(除了 GNOME 體系偏好使用 meson、Google 體系使用 GNBazel 之外),幾乎都會採用 CMake 作為主要的專案管理與編譯工具。 (Qt6 已經從 qmake 遷移到 CMake 了)

因此我們需要知道一些 CMake 的基本知識,好方便後續文章的進行。

專案結構

在後續的文章中,我們基本上會使用這樣的結構

simple-agent/
├── CMakeLists.txt                 # 專案的 CMake 設定(入口檔)
├── options.cmake                  # 專案依賴以及編譯器設置
├── cmake/                         # 自訂 CMake 模組(輔助 .cmake 檔案)
   ├── clang-format.cmake
   ├── gtest.cmake
   └── fmt.cmake
├── src/                           # 真正的原始碼
   ├── CMakeLists.txt             # src 子目錄的 CMake
   ├── main.cpp                   # 程式進入點
   ├── agent.cpp
   ├── ...
   └── tools/                     # 子功能(讀檔、改檔等工具)
       ├── CMakeLists.txt
       ├── read_file.cpp
       ├── write_file.cpp
       └── ...
├── tests/                         # 單元測試 (optional)
├── .gitignore
├── README.md
└── ...

根目錄的 CMakeLists.txt : 專案的入口

最上層的 CMakeLists.txt 是整個專案的起點,因此我們需要指定使用的 CMake 版本以及一些專案基本的設定,更多設定比如依賴之類的,會切分到 options.cmake

cmake_minimum_required(VERSION 3.16)     # 指定最低 CMake 版本
project(simple_agent LANGUAGES C CXX)    # 專案名稱與使用語言

include (xxx.cmake)                      # 有些要用 cmake 實作的複雜設定抽成 cmake 模組

include (options.cmake)                  # 載入共用設定
add_subdirectory(src)                    # 加入 src/ 為子專案

options.cmake : 編譯選項與第三方套件

options.cmake 是這個專案最核心的設定檔。我們分段來看。

載入需要的 CMake 模組

INCLUDE (CheckLibraryExists)
INCLUDE (CheckIPOSupported)
INCLUDE (FindThreads)

CMake 內建了許多模組,這些 Check* 模組可以用來檢查某個功能是否被編譯器支援(例如 LTO)。 FindThreads 則負責尋找系統的 pthread 函式庫。

設定語言標準

SET(CMAKE_CXX_STANDARD 17)
SET(CMAKE_CXX_STANDARD_REQUIRED ON)

整個專案指定使用 C++17 ,並且強制要求編譯器必須支援( REQUIRED ON ),因此如果是不支援的編譯器編譯就會報錯

啟用 LTO 最佳化

CHECK_IPO_SUPPORTED(RESULT BUILD_WITH_LTO OUTPUT ERROR_MSG)
IF(BUILD_WITH_LTO)
  SET(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
ELSE()
  MESSAGE(WARNING "LTO unavailable: ${ERROR_MSG}")
ENDIF()

這邊會先檢查編譯器是否支援 LTO(Link-Time Optimization),如果有就啟用它。LTO 可以讓編譯器在連結階段做跨編譯單元的最佳化,可以縮減 binary 大小以及提升效率。

尋找第三方套件

對我們即將撰寫的 agent 來說,我們會需要用到 curlnlohmann_json 這幾個函式庫,其中 curl 負責發送 HTTP請求,大部分 Linux 系統皆內建,而 nlohmann_json 則是 C++ 上非常通用的 json 解析函式庫。

在 CMake 中,最簡單的引入方式是透過 find_package ,這會從你電腦系統已經安裝的套件裡面去尋找這些函式庫是否存在。

FIND_PACKAGE(curl REQUIRED)

REQUIRED 表示如果找不到這些套件,CMake 會直接報錯並停止,這樣可以及早發現問題而不是等到連結階段才失敗。

如果你的環境還沒有安裝這些套件,可以用 FetchContent 來自動下載、編譯,以 nlohmann_json 為例,我會這樣寫:

#-------------------------------------------------------------------------------
# Third-party dependencies
#-------------------------------------------------------------------------------
FetchContent_Declare(
    nlohmann_json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.11.3
)
FetchContent_MakeAvailable(nlohmann_json)

src/CMakeLists.txt : src 子專案

進入 src/ 目錄後,我們需要告訴 CMake 這裡面有哪些東西要編譯:

建立主程式

ADD_EXECUTABLE(simple_agent
  main.cpp
  agent.cpp
  message.cpp
)

ADD_EXECUTABLE 會把列出的 .cpp 檔編譯、連結成一個執行檔 simple_agent

連結所需的函式庫

TARGET_LINK_LIBRARIES(simple_agent PRIVATE
  ${CURL_LIBRARIES}
  nlohmann_json::nlohmann_json
)

PRIVATE 表示這些依賴只屬於 simple_agent 自己,不會傳染給任何依賴 simple_agent 的目標。

(如果在撰寫 lib 時候,讓自己寫的 lib 依賴全部透露給外部知道,則最終的執行檔依賴路徑會變得很複雜,因此好習慣就是只洩漏最低需要依賴的資訊即可)

簡單的 CMake 專案

上面介紹的是我們即將實作的 simple-agent 裡面部分的 cmake 結構,稍微看懂後我們就可以弄一個最最簡單的 CMake 專案跑看看,就像這樣

建立一個名為 hello 的資料夾並切入進去:

mkdir hello && cd $_

建立 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello)

set(CMAKE_CXX_STANDARD 17)

add_executable(hello hello.cpp)

建立 hello.cpp

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

設定 cmake 並開始編譯

cmake -B build -S .    # 告訴 cmake 我們要建立 build 資料夾用來做編譯設定,編譯專案是當前資料夾
cmake --build build    # 進行編譯

測試執行

$ ./build/hello
Hello, Wolrd!

看,我們程式順利建立並執行囉 :)

範例程式碼

本文的程式碼已經上線,你可以用以下命令取得,對應的 tag 為 v0.1.0, happy coding :)

git clone https://github.com/coldnew/simple-agent.git
cd simple-agent
git checkout v0.1.0

On this page