使用CEF(六)— 解讀CEF的cmake工程設定

2023-10-11 12:02:49

距離筆者的《使用CEF》系列的第一篇文章居然已經過去兩年了,在這麼長一段時間裡,筆者也寫了很多其它的文章,再回看《使用CEF(一)— 起步》編寫的內容,文筆稚嫩,內容單薄是顯而易見的(主要是教大家按部就班的編譯libcef_dll_wrapper庫檔案)。筆者一直以來的個性就是希望自己學習到的知識,研究出的內容,踩過的坑能夠及時的寫出來,介紹給更多的小夥伴。

這篇文章產生的背景是最近筆者再一次仔細的閱讀了CEF binary distribution(CEF二進位制分發包)的工程程式碼以及根目錄下的CMakeLists.txt檔案的所瞭解到的東西,希望在本文能夠讓讀者小夥伴對於CEF binary distribution的工程結構有一個較為清晰的瞭解。

CMake基礎匯入

CMake是什麼,它和Unix下的make+gcc、macOS下的xcode+clang以及Windows下的VS+msvc工具鏈的關係不在本文解釋,但閱讀本文還是需要對CMake所扮演的角色有基本認識,所以如果你還不是特別清楚,建議先從筆者這一篇文章瞭解下《C與CPP常見編譯工具鏈與構建系統簡介 - 知乎 (zhihu.com)》。CMake本身無法構建任何的應用,它生成不同構建工具所需要的設定或某種輸入,再讓構建工具基於設定呼叫工具鏈,對程式碼進行構建。

target

一般來說,我們使用CMake來構建某種產物(這裡的「構建」不嚴謹,只是方便描述),這個產物可以是可執行二進位制程式直接執行,可以是一個庫檔案。構建產物在CMake中被抽象成了名為target的東西,CMake的核心運轉就是圍繞target進行的。

在CMake中定義某個target,最最最基礎的方式有兩種:add_executableadd_library

add_executable()add_executable — CMake 3.27.6 Documentation

該命令用於定義一個可以構建成可執行程式的target,簡單用法形式如下:

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               [source1] [source2 ...])
  • 第一個必填引數name,就是我們要編譯的可執行程式target的名稱;
  • 可選引數WIN32MACOS_BUNDLE。分別用於Windows平臺和macOS平臺可執行應用程式的構建。如果沒有設定這個引數,你會發現最終編譯的可執行程式本質上就是從控制檯程式啟動的程式。兩個最直觀的例子:在Windows上的QT GUI專案,沒有設定WIN32引數,那麼編譯後執行起來時除了我們的GUI表單展示,還會有一個黑色控制檯視窗展示;在macOS上,你經常看到的某某應用XXX.app實際上是一個bundle,裡面有這個應用的各種設定、實際執行的可執行檔案等,如果你想要最終構建出來的屬於這種應用程式,那麼就需要MACOS_BUNDLE引數;
  • 可選引數EXCLUDE_FROM_ALL,表明整個專案構建的時候,排除當前這個target;
  • 至於source1source2等等就是標頭檔案、原始碼檔案了。

add_library()add_library — CMake 3.27.6 Documentation

該命令用於定義一個生成的庫檔案的target,普通用法形式如下:

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [<source>...])
  • 第一個必填引數name,就是我們要編譯的庫檔案target的名稱;
  • 引數STATICSHAREDMODULE互斥三選一。STATIC表明希望將這個庫檔案編譯為靜態庫;SHARED表明希望將這個庫檔案編譯為動態連線庫;MODULE表明編譯為一個動態庫,但是通過執行時以程式的方式載入(比如dlopen在Unix-like系統中,或LoadLibrary在Windows系統中)。
  • 可選引數EXCLUDE_FROM_ALL,表明整個專案構建的時候,排除當前這個target;
  • 至於source等等就是標頭檔案、原始碼檔案了。

target_include_directories與target_link_libraries

想要構建C/C++工程,我們經常需要在編譯階段使用外部庫的標頭檔案分析依賴與記憶體佈局,以及在連結階段連結這些外部庫檔案。在CMake中,我們一般使用target_include_directories指令來指定對應target編譯過程中外部庫標頭檔案的搜尋路徑,以及使用target_link_libraries指令來指定連結階段要連結哪些庫檔案,具體用法讀者自行了解。

值得注意的是,除了上述兩個指令外,你還會搜尋到兩個類似的指令include_directorieslink_libraries。這兩個指令命名上沒有"target_"字首,其作用主要是提供全域性的標頭檔案和連結庫搜尋路徑。這個兩個全域性作用的指令的背景在於CMake是支援多target模組構建的,可以通過專案頂層的CMakeLists.txt中設定這兩個指令,讓子模組target共用這些標頭檔案和庫檔案路徑設定。但是如沒有必要,儘可能使用target_xxx來給指定的target設定。舉一反三,CMake中還有很多的target_開頭的指令,其目的都是針對某個指定的target的設定。

由於篇幅原因,本文關於CMake基本匯入的部分就介紹到這裡,接下來,讓我們逐步分析CEF binary distribution中的CMakeLists.txt。

頂層CMakeLists.txt

OVERVIEW

overview部分簡單介紹了CMake,然後介紹CEF binary distribution不同平臺下支援的專案構建系統和工具鏈:

# Linux:      Ninja, GCC 7.5.0+, Unix Makefiles
# MacOS:      Ninja, Xcode 12.2 to 13.0
# Windows:    Ninja, Visual Studio 2022

CMAKE STRUCTURE

該部分介紹了CEF binary distribution的CMAKE工程結構,說明了CEF二進位制分發包主要由以下幾個部分組成:

# CMakeLists.txt              Bootstrap that sets up the CMake environment.
# cmake/*.cmake               CEF configuration files shared by all targets.
# libcef_dll/CMakeLists.txt   Defines the libcef_dll_wrapper target.
# tests/*/CMakeLists.txt      Defines the test application target.
  • CMakeLists.txt:組織構建CEF二進位制分發的CMake環境。
  • cmake/*.cmake:CMake組態檔,可被所有的target使用。
  • libcef_dll/CMakeLists.txt:定義了libcef_dll_wrapper這個target的CMake設定。
  • tests/*/CMakeLists.txt:定義了所有的測試Demo應用target。

BUILD REQUIREMENTS

該部分主要介紹了編譯libcef_dll_wrapper以及相關樣例demo在不同作業系統平臺上的環境要求。

BUILD EXAMPLES

這一部分主要介紹瞭如何構建libcef_dll_wrapper以及demo。具體的做法就是在cef_binary_xxx目錄(後續都用該指代CEF binary distribution資料夾根目錄)中建立一個名為build的目錄,進入該目錄後,針對不同的平臺,使用CMake生成不同的構建系統的工程設定,並進行構建。其中,由於Ninja是一個跨平臺的構建系統,所以你會看每個平臺都有Ninja構建系統的生成指令。例如,下圖展示了在macOS x86 64位元架構上使用CMake生成對應的構建方案的兩種方式:1、xcode構建方案(xcodebuild構建方案體系);2、Ninja構建方案。

無論是xcode還是ninja,都是構建系統,在macOS上最終呼叫編譯工具鏈是底層的clang/LLVM。

再比如,在Windows64位元系統上也有兩種方式:1、VisualStudio解決方案(MSBuild構建方案體系);2、Ninja構建方案。

同樣的,無論是vs MSBuild還是ninja,都是構建系統,在Windows上最終呼叫的是底層的msvc編譯工具鏈。

對於使用Ninja,讀者會看到都會呼叫ninja cefclient cefsimple,這個命令執行後,會編譯demo中的cefclient和cefsimple兩個專案,這裡只是官方例子,在實際使用過程中,並不是一定要按照它的操作來。另外,有讀者可能有疑問,這個過程並沒有看到關於libcef_dll_wrapper專案的構建,這裡先提前說明一下,在cefsimple和cefclient等demo中依賴了libcef_dll_wrapper並通過設定進行了指定,所以構建的過程中,會優先自動編譯libcef_dll_wrapper。關於這塊,等我們後面詳解的時候會介紹的。

在看完了關於不同平臺的構建方式以後,我們往下會看到關於"Global setup."的部分。這一部分開始,就是CMake真正有關的部分了。讓我們首先刪除掉所有的註釋,逐步分析這個頂層CMakeLists.txt的設定:

剔除了註釋以後,會發現其實內容並不多。這裡我們首先從上圖第8行開始關於設定CEF_ROOTCMAKE_MODULE_PATH的分析:設定首先定義了CEF_ROOT,它使用了CMake提供的變數CMAKE_CURRENT_SOURCE_DIR,也就是當前CMakeLists.txt所在目錄:cef_binary_xxx目錄;然後對CMAKE_MODULE_PATH追加${CEF_ROOT}/cmake這個目錄。

之所以這樣做,是因為接下來find_package(CEF REQUIRED)會根據CMAKE_MODULE_PATH所提供的路徑進行搜尋。關於find_package,網上解析的文章很多,這裡只簡單說明下,CMake官方檔案中提到find_package有兩種搜尋模式,其中一種就是模組搜尋模式(Module mode),該搜尋模式說明如下:

Module mode
In this mode, CMake searches for a file called Find<PackageName>.cmake, looking first in the locations listed in the CMAKE_MODULE_PATH, then among the Find Modules provided by the CMake installation. If the file is found, it is read and processed by CMake.

翻譯過來就是:當執行find_package(PackageName)的時候,CMake會在CMAKE_MODULE_PATH路徑列表中,查詢名為Find<PakcageName>.cmake檔案,找到後就會對該組態檔載入並處理。對照本例,find_package(CEF REQUIRED),在模組搜尋模式下,則是需要查詢一個名為FindCEF.cmake的檔案。由於我們在CMAKE_MODULE_PATH中追加了${CEF_ROOT}/cmake這個目錄,即cef_binary_xxx/cmake目錄,所以CMake會搜尋這個目錄,該目錄確實存在FindCEF.cmake檔案,於是被CMake命中並載入了。那麼,接下來讓我們開啟該FindCEF.cmake檔案,一探究竟。

FindCEF.cmake

FindCEF.cmake很好理解,大致處理過程是:

首先從CMake全域性上下文或系統環境變數等地方讀取名為CEF_ROOT的值,這個值是一個目錄,指代了cef_binary_xxx目錄,然後校驗該目錄路徑是否合法(路徑下的cmake目錄是否存在),並賦值給_CEF_ROOT這個值很關鍵,接下來都是使用這個_CEF_ROOT值);

然後,給CMAKE_MODULE_PATH追加${_CEF_ROOT}/cmake路徑,與之前cef_binary_xxx/CMakeList.txt中追加該PATH目的不一樣,這一次追加CMAKE_MODULE_PATH值的核心目的是為下面呼叫include("cef_variables")include("cef_macros")的時候,能夠找到${_CEF_ROOT}/cmake路徑下名為cef_variables.cmakecef_macros.cmake檔案。

CMake的官方檔案告訴我們,CMake在處理include("abc")的時候,會搜尋CMAKE_MODULE_PATH路徑下名為abc.cmake的檔案進行載入處理。CMake - include

看到這裡,有的讀者可能已經繞暈了,我們做一個簡單的流程圖來描述這個過程:

cef-binary-xxx/CMakeList.txt -> find_package(CEF REQUIRED) -> 在第一次 CMAKE_MODULE_PATH路徑設定前提下,找到了 FindCEF.cmake設定讀取;FindCEF.cmake -> include("cef_variables")、include("cef_macros"),按順序載入 cef_variables.cmake 和 cef_macros.cmake。

我們暫時不深入研究cef_variables.cmakecef_macros.cmake裡的內容,後面遇到一些特殊的變數、宏的時候,我們再來解釋,這裡我們可以先跳出細節,可以認為cef_variablescef_macros裡面分別定義了一些變數設定和宏定義,供後續CMake處理流程讀取或呼叫。

libcef_dll/CMakeLists.txt

現在,讓我們回到cef_binary_xxx/CMakeLists.txt,在find_package(CEF)之後,緊接著的就是add_subdirectory()指令:

# Include the libcef_dll_wrapper target.
# Comes from the libcef_dll/CMakeLists.txt file in the binary distribution
# directory.
add_subdirectory(${CEF_LIBCEF_DLL_WRAPPER_PATH} libcef_dll_wrapper)

這裡出現了一個變數:CEF_LIBCEF_DLL_WRAPPER_PATH,它來源於cef_variables.cmake中定義的:

也就是說,在本例中,add_subdirectory(${CEF_LIBCEF_DLL_WRAPPER_PATH} libcef_dll_wrapper)就是新增了子目錄cef_binary_xxx/libcef_dll。一旦新增了該子模組目錄,CMake就會在該目錄下搜尋對應的CMakeLists.txt檔案並進行載入(這裡就是cef_binary_xxx/libcef_dll/CMakeLists.txt)。

這份libcef_dll/CMakeLists.txt主要就是將libcef_dll_wrapper的各種原始碼、以及libcef的標頭檔案、各種平臺特定的原始碼檔案放到一些CMake變數中,最後的通過add_library指令,定義了一個名為libcef_dll_wrapper的target,並將前面的原始碼、標頭檔案等新增到這個target中:

寫到這裡,我們可以對cef_binary_xxx/CMakeLists.txt檔案做一個簡單的概念總結。首先,該CMakeLists.txt扮演的是專案頂層統領全域性的角色,它並沒有定義過任何的target,而是通過兩個步驟組織了`CEF binary distribution目錄中的libcef_dll_wrapper、demo等target的構建:

步驟一:負責預構造CMake處理環境上下文,包括準備各種設定變數、宏方法等,供後續過程使用。這個過程具體是是通過載入FindCEF.cmake,並在該檔案內部再載入cef_variables.cmakecef_macros.cmake兩個設定。

步驟二:通過add_subdirectory新增並管理起子模組target,包括libcef_dll_wrapper以及各種demo的target。這個過程CMake會讀取對應路徑下的CMakeLists.txt並載入。同時,這些檔案中使用到的一些CEF提供的變數、宏都來自於步驟一所載入的cef_variables.cmakecef_macros.cmake

cefsimple/CMakeLists.txt

因為libcef_dll_wrapper這個target最終產物是一個庫檔案,所以這個target所在CMakeLists.txt內容雖然很多,但是比較直白,就是各種原始碼、標頭檔案的新增。但是,如果target產物是一個可執行程式,CMakeLists.txt還會這麼簡單嗎?這裡我們分析下cefsimple這個樣例的CMakeLists.txt。

首先,cefsimple存放於cef_binary_xxx/tests/cefsimple目錄中,在cef_binary_xxx/CMakeLists.txt中,同樣通過add_subdirectory新增:

f(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests")
  add_subdirectory(tests/cefsimple) # cefsimple
  add_subdirectory(tests/gtest)
  add_subdirectory(tests/ceftests)
endif()

這裡之所以使用一個目錄判斷,目測是在CEF binary distribution的Minimal最小版本中,是剔除了樣例工程的,所以做了一個IF判斷。

所以,接下來我們開始分析cef_binary_xxx/tests/cefsimple/CMakeLists.txt檔案。

原始檔定義

該檔案實際上也分為兩個部分。第一部分就是通過變數來儲存cefsimple的相關原始碼、標頭檔案:

這一塊我們挑一個比較典型的處理:

首先使用CEFSIMPLE_SRCS來儲存平臺無關的原始碼和標頭檔案。其次,由於不同作業系統平臺下有一些平臺特定的原始碼,例如macOS下,設定表單標題,我們可以使用objective-c程式碼(.m/.mm檔案)來使用原生API操作表單標題,所以使用CEFSIMPLE_SRCS_平臺標識變數儲存這些平臺特定程式碼的列表;最後,使用一個名為APPEND_PLATFORM_SOURCES的宏來處理CEFSIMPLE_SRCS變數,這裡有兩個疑問點:1、這個宏的來源和作用;2、CEFSIMPLE_SRCS_平臺標識變數似乎沒有用到。這兩個疑問點一起解釋。實際上,這個宏就是來源於cef_macros.cmake中,找到對應宏的原始碼:

# Append platform specific sources to a list of sources.
macro(APPEND_PLATFORM_SOURCES name_of_list)
  if(OS_LINUX AND ${name_of_list}_LINUX)
    list(APPEND ${name_of_list} ${${name_of_list}_LINUX})
  endif()
  if(OS_POSIX AND ${name_of_list}_POSIX)
    list(APPEND ${name_of_list} ${${name_of_list}_POSIX})
  endif()
  if(OS_WINDOWS AND ${name_of_list}_WINDOWS)
    list(APPEND ${name_of_list} ${${name_of_list}_WINDOWS})
  endif()
  if(OS_MAC AND ${name_of_list}_MAC)
    list(APPEND ${name_of_list} ${${name_of_list}_MAC})
  endif()
endmacro()

這段宏的邏輯實際上就是通過判斷作業系統平臺,使用CMake提供的list APPEND機制,將入參name_of_listname_of_list_平臺標識合成為一個list列表。比較trick的就是,在呼叫APPEND_PLATFORM_SOURCES(CEFSIMPLE_SRCS),內部比如${${name_of_list}_MAC} 就是${CEFSIMPLE_SRCS_MAC},即獲取這個變數的資料。後面剩下關於設定原始檔的方式類似,這裡就請讀者自行分析了。

現在,讓我們回到對cefsimple/CMakeLists.txt本身的分析,接下來我們分析比較重要的第二部分:可執行程式的生成:

這裡我們對macOS平臺的可執行程式生成進行講解,因為它相對於在Windows和Linux更加複雜。首先,定義了在macOS平臺下會新增一些編譯指令(譬如支援objective-c語言編譯):

option(OPTION_USE_ARC "Build with ARC (automatic Reference Counting) on macOS." ON)
if(OPTION_USE_ARC)
  list(APPEND CEF_COMPILER_FLAGS
    -fobjc-arc
    )
  set_target_properties(${target} PROPERTIES
    CLANG_ENABLE_OBJC_ARC "YES"
    )
endif()

然後,設定了輸出的可執行程式一些名稱變數,這裡就是"cefsimple.app"

# Output path for the main app bundle.
set(CEF_APP "${CEF_TARGET_OUT_DIR}/${CEF_TARGET}.app")

# Variables referenced from the main Info.plist file.
set(EXECUTABLE_NAME "${CEF_TARGET}")
set(PRODUCT_NAME "${CEF_TARGET}")

再次需要提到的是,在macOS,一般可執行程式都會生成為一個App Bundle(About Bundles (apple.com))。

如果啟用了USE_SANDBOX標識,則會使用自定義宏(也是在之前的cef_macro.cmake中定義的)ADD_LOGICAL_TARGET進行特殊的處理:

if(USE_SANDBOX)
  # Logical target used to link the cef_sandbox library.
  ADD_LOGICAL_TARGET("cef_sandbox_lib" "${CEF_SANDBOX_LIB_DEBUG}" "${CEF_SANDBOX_LIB_RELEASE}")
endif()

接下來就是定義核心應用:

# Main app bundle target.
add_executable(${CEF_TARGET} MACOSX_BUNDLE ${CEFSIMPLE_RESOURCES_SRCS} ${CEFSIMPLE_SRCS})
SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_TARGET})
add_dependencies(${CEF_TARGET} libcef_dll_wrapper)
target_link_libraries(${CEF_TARGET} libcef_dll_wrapper ${CEF_STANDARD_LIBS})
set_target_properties(${CEF_TARGET} PROPERTIES
  MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/mac/Info.plist
  )

這段程式碼執行邏輯解釋如下:

  1. 使用add_executable定義了主程式target,注意新增了引數"MACOSX_BUNDLE"表明最終生成的target是一個macOS下的App Bundle,和在Windows下的"WIN32"引數異曲同工;
  2. 使用自定義宏SET_EXECUTALBE_TARGET_PROPERTIES為target新增一些屬性;
  3. 使用指令add_dependencies定義了我們當前cefsimple依賴了一個libcef_dll_wrappertarget,該指令的核心作用就是能夠確定一個target在生成的過程中需要什麼依賴。
  4. 設定了target一些特殊的properties,這裡主要就是定義當生成macOS的App Bundle的時候,會在Bundle中生成Info.plist,這個檔案是macOS下App Bundle中一個比較重要檔案,用來定義應用的一些與macOS作業系統相關的屬性,例如是否支援高分屏檢測等。開發過Windows應用的小夥伴都知道,在Windows下,會有一個app.manifest檔案,它倆也是異曲同工。

接下來就是使用CMake提供的add_custom_command指令,定義了編譯生成以後("POST_BUILD"標識),將相關的檔案拷貝至目標目錄的流程:

# Copy the CEF framework into the Frameworks directory.
add_custom_command(
  TARGET ${CEF_TARGET}
  POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_directory
          "${CEF_BINARY_DIR}/Chromium Embedded Framework.framework"
          "${CEF_APP}/Contents/Frameworks/Chromium Embedded Framework.framework"
  VERBATIM
  )

在使用CMake定義專案結構的時候,我們可以通過add_custom_command來實現編譯、構建過程中一些生命週期節點的處理邏輯,譬如拷貝依賴庫等。

接下來的foreach指令,這裡定義了n個helper的AppBundle target。譬如渲染程序、GPU加速程序、工具程序等具有特定功能的程序help程式:

值得注意的是,在macOS下,這裡helper的add_executable()新增的是CEFSIMPLE_HELPER_SRCS,這個變數裡面儲存的是:

翻看該process_helper_mac.cc原始碼,其實並不複雜:

// Entry point function for sub-processes.
int main(int argc, char* argv[]) {
#if defined(CEF_USE_SANDBOX)
  // Initialize the macOS sandbox for this helper process.
  CefScopedSandboxContext sandbox_context;
  if (!sandbox_context.Initialize(argc, argv)) {
    return 1;
  }
#endif

  // Load the CEF framework library at runtime instead of linking directly
  // as required by the macOS sandbox implementation.
  CefScopedLibraryLoader library_loader;
  if (!library_loader.LoadInHelper()) {
    return 1;
  }

  // Provide CEF with command-line arguments.
  CefMainArgs main_args(argc, argv);

  // Execute the sub-process.
  return CefExecuteProcess(main_args, nullptr, nullptr);
}

這裡只要熟悉CEF的多程序架構就能理解。不熟悉的夥伴可以閱讀這篇文章:使用CEF(三)— 從CEF官方Demo原始碼入手解析CEF架構與CefApp、CefClient物件 - 知乎 (zhihu.com)

關於cefsimple/CMakeLists.txt剩下的內容其實也不復雜了,讀者可以順著本文的思路進一步閱讀。

寫在最後

通過頂層CMakeLists.txt的說明,不難發現,cef_binary_xxx本身既是包含了了libcef_dll_wrapper原始碼構建的工程,同時也是一個比較標準的,想要使用libcef+libcef_dll_wrapper的CMake工程,所以,你才會在頂層CMakeLists.txt看到官方介紹了幾種基於cef_binary_xxx的CMake工程結構的專案整合案例: