CMake/config.h

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา

เราเคยกล่าวไปแล้วในบทความการใช้ CMake เบื้องต้น ว่าปัญหาหนึ่งที่โปรแกรมเมอร์ภาษา C++ ที่ต้องการเขียนโปรแกรมให้รันได้ในหลายแพลตฟอร์มเจอคือการที่แพลตฟอร์มต่างๆ มีไลบรารีและฟังก์ชันให้เรียกใช้ไม่เหมือนกัน

ตัวอย่างหนึ่งคือฟังก์ชันสำหรับคืนชื่อไดเรคทอรีที่โปรแกรมทำงานอยู่ปัจจุบัน (current working directory) ซึ่งในระบบปฏิบัติการที่สืบเชื้อสายมาจาก Unix จะมีชื่อว่า getcwd และต้อง include ไฟล์ unistd.h เพื่อใช้งาน แต่ใน Windows ฟังก์ชันนี้กลับมีชื่อว่า _getcwd และต้อง include ไฟล์ direct.h แทน

ดังนั้น หากเราจะเขียนโปรแกรม print_cwd ที่พิมพ์ current working directory ออกทางหน้าจอ เราจะต้องรู้ว่าโปรแกรมของถูกคอมไพล์อยู่ในแพลตฟอร์มใด ซึ่งในบทความ CMake เบื้องต้น ผู้เขียนได้เสนอให้ใช้มาโคร _WIN32 เป็นตัวเช็คว่าโปรแกรมถูกคอมไพล์ใน Windows หรือไม่ อย่างไรก็ดีผู้เขียนคิดว่าการใช้ _WIN32 ไม่น่าจะเป็นความคิดที่ดี เนื่องจากเราไม่ทราบว่าคอมไพเลอร์ตัวอื่นนอกจาก cl ของไมโครซอฟต์จะนิยาม _WIN32 เอาไว้หรือไม่

สิ่งที่เราต้องการคือมาโครซึ่งจะถูกนิยามตามแพลตฟอร์มที่โปรแกรมถูกคอมไพล์ เนื่องจากผู้เขียนเขียนโปรแกรมอยู่ในสามแพลตฟอร์มใหญ่ๆ คือ Windows, Mac, และ Linux (ซึ่งเป็น Unix แบบหนึ่ง)​ผู้เขียนจึงอยากให้มีมาโครต่อไปนี้

  • __WIN_PLATFORM__ ถูกนิยามก็ต่อเมื่อโปรแกรมถูกคอมไพล์ใน Windows
  • __MAC_PLATFORM__ ถูกนิยามก็ต่อเมื่อโปรแกรมถูกคอมไพล์ใน Mac OS
  • __UNIX_PLATFORM__ ถูกนิยามก็ต่อเมื่อโปรแกรมถูกคอมไพล์ด้วยระบบปฏิบัติการที่สืบเชื้อสายมาจาก Unix อื่นๆ เช่น Linux แต่ไม่ใช่ Mac OS

ตามมาตรฐานการเขียนโค้ดของ GNU แล้ว มาโครที่ใช้สำหรับบอกแพลตฟอร์มเหล่านี้จะถูกบรรจุอยู่ในไฟล์ชื่อ config.h ซึ่งอยู่ในชั้นบนสุดของ source directory ของโปรเจค ไฟล์ config.h นี้จะถูกสร้างขึ้นโดยอัตโนมัติด้วยเครื่องมือชื่อ autoconf ในบทความนี้เราจะเลียนแบบมาตรฐานของ GNU แต่เราจะใช้ CMake สร้าง config.h แทน

หากมี config.h แล้วซอร์สโค้ดของโปรแกรม print_cwd อาจมีเนื้อหาดังต่อไปนี้

<geshi lang="c"> /* print_cwd.cpp */

  1. include "../config.h"
  1. ifdef __WIN_PLATFORM__
  2. include <direct.h>
  3. else
  4. include <unistd.h>
  5. endif
  1. include <stdio.h>
  2. include <stdlib.h>

int main() {

   char *buffer;
   
   #ifdef __WIN_PLATFORM__
   buffer = _getcwd(NULL, 0);
   #else
   buffer = getcwd(NULL, 0);
   #endif
   
   printf("%s\n", buffer);
   free(buffer);
   
   return 0;

} </geshi>

หมายเหตุ: ที่เรา include ../config.h แทนที่จะ include config.h เฉยๆ เป็นเพราะว่าจากบทความที่แล้ว เราได้เรียนรู้ว่าควรจัดให้ print_cwd.cpp อยู่ในไดเรคทอรี src/print_cwd เพื่อไม่ให้ไปปนกับไฟล์ของ target อื่นๆ แต่ config.h ควรอยู่ในไดเรคทอรี src เนื่องจากเป็นไฟล์ที่ทุกๆ target จะต้องใช้ร่วมกัน

การสร้างไฟล์ config.h โดยใช้ CMake มีขั้นตอนอยู่สามขั้นตอน

1. เขียนไฟล์ config.h.in เพื่อใช้เป็นต้นแบบของ config.h

2. เขียน CMakeLists.txt เพื่อกำหนดตัวตัวแปรต่างๆ ในการนำไปสร้าง config.h

3. เรียกคำสั่ง CONFIGURE_FILE ใน CMakeLists.txt เพื่อสร้าง config.h

config.h.in

config.h.in ควรอยู่ในไดเรคทอรี src เหมือนกับ config.h ดังนั้นโปรเจคของเราจึงมีโครงสร้างไดเรคทอรีดังต่อไปนี้

   sample/
       build/
       src/
           CMakeLists.txt
           config.h.in
           print_cwd/
               CMakeLists.txt
               print_cwd.cpp

ที่ไม่มี config.h เนื่องจากมันยังไม่ถูกสร้างขึ้นจริง

เนื้อหาของไฟล์ config.h.in นั้นเหมือนกับเนื้อหาของ header file ของภาษา C/C++ ธรรมดา แต่มาโครใดที่เราต้องกให้ CMake นิยามตามเงื่อนไขต่างๆ ของแพลตฟอร์ม เราจะใช้คำสั่ง #cmakedefine แทน #define ในกรณีของการนิยามมาโครบอกแพลตฟอร์มสามมาโครข้างต้น เราสามารถเขียน config.h.in ได้ดังต่อไปนี้

<geshi lang="c"> /* config.h.in */

  1. ifndef __CONFIG_H__
  2. define __CONFIG_H__
  1. cmakedefine __WIN_PLATFORM__
  2. cmakedefine __MAC_PLATFORM__
  3. cmakedefine __UNIX_PLATFORM__
  1. endif

</geshi>

เวลา CMake สร้างไฟล์ config.h มันจะเอา config.h.in เป็นต้นฉบับและเปลี่ยนแปลงเฉพาะบรรทัดที่มี #cmakedefine เท่านั้น บรรทัดอื่นๆ จะมีเนื้อหาเหมือนเดิม

CMakeLists.txt

ในที่นี้เราจะสนใจเฉพาะไฟล์ CMakeLists.txt ที่อยู่ในไดเรคทอรี src เท่านั้น ไฟล์ CMakeLists.txt ที่อยู่ใน src/print_cwd นั้นมีหน้าที่สร้าง executable ชื่อ print_cwd ซึ่งเนื้อหาของมันจะคล้ายกับไฟล์ CMakeLists.txt ทำนองเดียวกันซึ่งปรากฏอยู่ในบทความที่แล้ว

ไฟล์ CMakeLists.txt ที่เราสนใจอาจมีเนื้อหาดังต่อไปนี้

   PROJECT(sample)
   CMAKE_MINIMUM_REQUIRED(VERSION 2.6)

   IF(WIN32)
       SET(__WIN_PLATFORM__ ON)
   ELSE(WIN32)
       SET(__WIN_PLATFORM__ OFF)
   ENDIF(WIN32)

   IF(UNIX)
       IF(APPLE)
           SET(__MAC_PLATFORM__ ON)
           SET(__UNIX_PLATFORM__ OFF)
       ELSE(APPLE)
           SET(__MAC_PLATFORM__ OFF)
           SET(__UNIX_PLATFORM__ ON)
       ENDIF(APPLE)
   ELSE(UNIX)
       SET(__MAC_PLATFORM__ OFF)
       SET(__UNIX_PLATFORM__ OFF)
   ENDIF(UNIX)

   ADD_SUBDIRECTORY(print_cwd)

   CONFIGURE_FILE( config.h.in ${CMAKE_SOURCE_DIR}/config.h )

สังเกตว่าเนื้อหามีส่วนเพิ่มเติมเข้ามาสองส่วนจากเนื้อหาไฟล์ CMakeListst.txt ที่เราพบในบทความก่อนหน้านี้ ได้แก่

  • ส่วนที่เริ่มจาก IF(WIN32) จงถึง ENDIF(UNIX)
  • คำสั่ง CONFIGURE_FILE ในบรรทัดสุดท้าย

กำหนดค่าให้ตัวแปร

ส่วนซึ่งเริ่มตั้งแต่ IF(WIN32) ถึง ENDIF(UNIX) ทำหน้าที่นิยามตัวแปร __MAC_PLATFORM__, __WIN_PLATFORM__, และ __UNIX_PLATFORM__ ให้มีค่าเป็น "จริง" หรือ "เท็จ" ที่ถูกต้องตรงตามแพลตฟอร์มที่โปรแกรมถูกคอมไพล์ ส่วนนี้มีหลักษณะคล้ายๆ กับโปรแกรมคอมพิวเตอร์เล็กๆ โปรแกรมหนึ่ง

CMake มีภาษาสคริปต์ที่ทรงพลังพอสมควร ภาษาของ CMake อนุญาตให้เราและกำหนดค่าให้ตัวแปร นอกจานี้ยังมีคำสั่ง if และลูป while ให้ใช้ คุณสามารถดูรายละเอียดของโครงสร้างของภาษาเหล่านี้จาก documentation ของ CMake หรือจากวิกิของ Kernigh

คำสั่ง SET

คำสั่งที่เราใช้มีอยู่สองคำสั่งได้แก่คำสั่ง SET ซึ่งใช้กำหนดค่าให้ตัวแปร ซึ่งคำสั่งนี้มีรูปแบบ

   SET(<ชื่อตัวแปร> <ค่าของตัวแปร>)

ตัวแปรใน CMake มีค่าเป็นสตริง (string) เท่านั้น ดังนั้นค่า ON และ OFF ที่เรากำหนดให้จริงแล้วจึงเป็นค่าสายอักษร "ON" และ "OFF"

ค่าความจริง

CMake ถือว่าสตริง "ON", "YES", และ "TRUE" มีค่าเป็น "จริง" และถือว่าสตริง "OFF", "NO", และ "FALSE" มีค่าเป็นเท็จ ดังั้นเราอาจจะเปลี่ยน ON เป็น TRUE หรือ ​OFF เป็น NO ก็ได้

(จริงๆ แล้วสตริงที่ไม่ใช่สตริงข้างบนที่อาจมีค่าเป็นจริงหรือเท็จได้อีก กรุณาอ่านวิกิของ Kernigh หากต้องการหาข้อมูลเพิ่มเติม)

คำสั่ง IF

คำสั่ง IF ใน CMAKE มีรูปแบบดังต่อไปนี้

   IF(<เงื่อนไข>)
       <กลุ่มคำสั่งที่จะทำเมื่อเงื่อนไขเป็นจริง>
   ENDIF(<เงื่อนไข>)

หรือ

   IF(<เงื่อนไข>)
       <กลุ่มคำสั่งที่จะทำเมื่อเงื่อนไขเป็นจริง>
   ELSE(<เงื่อนไข>)
       <กลุ่มคำสั่งที่จะทำเมื่อเงื่อนไขเป็นเท็จ>
   ENDIF(<เงื่อนไข>)

ซึ่งคำสั่งทั้งสองรูปแบบมีความหมายเหมือนกับคำสั่ง if ในภาษาโปรแกรมอื่นๆ

สังเกตว่าเวลาเขียนคำสั่งเหล่านี้ เงื่อนไขที่อยู่ในวงเล็บหลัง IF, ELSE, และ ENDIF จะต้องเป็นเงื่อนไขเดียวกันเสมอ ไม่งั้น CMake จะจับคู่ IF กับ ELSE หรือ ENDIF ไม่ถูก พึงระวังว่า CMake ไม่มีคำสั่ง ELSEIF ให้ใช้ภาษาโปรแกรมทั่วๆ ไป

WIN32, APPLE, และ UNIX

WIN32, APLLE, และ UNIX เป็นตัวแปรสามตัวที่ CMake กำหนดค่าให้อัตโนมัติก่อนการประมวลผล CMakeLists.txt โดยที่

  • WIN32 จะมีค่าเป็นจริงก็ต่อเมื่อระบบปฏิบัติการที่ใช้อยู่ในตระกูล Windows
  • APPLE จะมีค่าเป็นจริงก็ต่อเมื่อระบบปฏิบัติการที่ใช้อยู่ในตระกูล Mac OS
  • UNIX จะมีค่าเป็นจริงก็ต่อเมื่อระบบปฏิบัติการที่ใช้สืบเชื้อสายมาจาก Unix

หมายเหตุ: ผู้เขียนสร้างตัวแปรใหม่สามตัวแทนที่จะใช้ WIN32, APPLE, และ UNIX ไปเลยเนื่องจาก (1) ความหมายของ __UNIX_PLATFORM__ ไม่ตรงกับความหมายของ UNIX (UNIX จะเป็นจริงในระบบปฏิบัติการตระกูล MAC OS ด้วย) และ (2) ผู้เขียนเห็นว่าชื่อมาโครที่ไม่มี underscore นำหน้าควรจะถูกเปิดไว้ให้ผู้เขียนโปรแกรมนำไปใช้

คำสั่ง CONFIGURE_FILE

คำสั่ง CONFIGURE_FILE มีรูปแบบดังต่อไปนี้

   CONFIGURE_FILE(<ชื่อไฟล์ต้นฉบับ> <ชื่อไฟล์ที่ต้องการสร้าง>)

สังเกตว่าเราเขียนคำสั่ง CONFIG_FILE ไว้เป็นคำสั่งสุดท้ายหลังจากการตั้งแต่ตัวแปรและการกำหนด target ต่างๆ (เรากำหนด target ผ่านการสั่ง ADD_DIRECTORY) เนื่องจากคำสั่ง CONFIGURE_FILE จะนำค่าของตัวแปรต่างๆ ณ ตำแหน่งที่ CONFIGURE_FILE ถูกเรียกมาใช้ในการสร้าง config.h ดังนั้นเราจึงเอาคำสั่งนี้ไว้เป็นคำสั่งสุดท้าย เพื่อให้แน่ใจได้ว่าตัวแปรทุกตัวถูกกำหนดไว้เรียบร้อยแล้ว

แต่สังเกตว่าเราไม่ได้ใส่ชื่อ config.h ไว้เฉยๆ แต่เราเติม ${CMAKE_SOURCE_DIR}/ ไปข้างหน้ามัน ที่เป็นเช่นนี้ก็เพราะว่าไฟล์ที่ CMake สร้างจะอยู่ใน binary directory เสมอ เว้นแต่จะกำหนดเป็นอย่างอื่น แต่เราต้องการให้ config.h อยู่ในไดเรคทอรี src ดังนั้นเราจึงใช้ตัวแปร CMAKE_SOURCE_DIR ซึ่งเป็นไดเรคทอรีที่มีไฟล์ CMakeLists.txt ของโปรเจคอยู่ (ในที่นี้แค่ src) สังเกตอีกอย่างว่าเวลาเราใช้ตัวแปรของ CMake เราจะล้อมชื่อตัวแปรด้วย ${ และ } เหมือนกับการใช้ตัวแปรใน unix shell ต่างๆ

หากเราอยู่ในระบบปฏิบัติการ Mac OS เราจะได้ src/config.h ที่มีเนื้อหาดังต่อไปนี้

<geshi lang="c">

  1. ifndef __CONFIG_H__
  2. define __CONFIG_H__

/* #undef __WIN_PLATFORM__ */

  1. define __MAC_PLATFORM__

/* #undef __UNIX_PLATFORM__ */

  1. endif

</geshi>

ในทางกลับกันหากเราอยู่ใน Windows แล้ว src/config.h จะมีเนื้อหาดังต่อไปนี้

<geshi lang="c">

  1. ifndef __CONFIG_H__
  2. define __CONFIG_H__
  1. define __WIN_PLATFORM__

/* #undef __MAC_PLATFORM__ */ /* #undef __UNIX_PLATFORM__ */

  1. endif

</geshi>

ซึ่งนิยามมาโครตรงตามความต้องการของเราเป๊ะ