CMake/external libraries
แม้ปัจจุบันมีไลบรารีภาษา C/C++ ที่สามารถใช้ได้บนแพลตฟอร์มพลายแพลตฟอร์มอยู่มากมาย แต่การใช้ไลบรารีเหล่านั้นพัฒนาโปรแกรมยังเป็นเรื่องที่ยุ่งยาก เนื่องจากแพลตฟอร์มต่างๆ เก็บไลบรารีเหล่านี้ไว้ที่ไดเรคต่างๆ กัน CMake มีสคริปต์สำหรับค้นหาแตำแหน่งของไลบรารีที่ได้รับความนิยมอยู่หลายๆ ไลบรารี ทำให้การใช้ไลบรารีเหล่านี้เขียนโปรแกรมที่ใช้ได้หลายๆ แพลตฟอร์มเป็นเรื่องง่าย (แต่ก็ยังมีรายละเอียดมากพอสมควร) ในบทความนี้เราจะมาศึกษาวิธีการเรียกใช้สคริปต์เหล่านี้โดยใช้ไลบรารี OpenGL และ GLUT เป็นตัวอย่าง
เนื้อหา
ติดตั้ง GLUT บน Windows
สร้างไดเรคทอรีสำหรับเก็บไลบรารี
หากผู้อ่านใช้ Linux หรือ Mac OS X ก็ไม่มีความจำเป็นจะต้องติดตั้งไลบรารีสองไลบรารีข้างต้นแต่อย่างใด เนื่องจากมันติดมากับระบบปฏิบัติการแล้ว อย่างไรก็ดีผู้อ่านที่ใช้ Windows จะต้องมีการติดตั้ง GLUT ให้ CMake สามารถค้นหาเจอได้ วิธีการติดตั้งนี้ความจริงแล้วมีหลายวิธี แต่ผู้เขียนจะนำเสนอวิธีการติดตั้งของตัวเองไว้ ณ ที่นี้
ผู้เขียนได้สร้างไดเรคทอรีหนึ่งในฮาร์ดดิสก์ไว้สำหรับเก็บไลบรารีภาษา C/C++ โดยเฉพาะ ในบทความนี้ขอสมมติให้เป็นไดเรคทอรี C:\usr\local เพื่อให้คล้ายๆ กับไดเรคทอรีใน Unix ในไดเรคทอรีนี้จะได้เรคทอรีย่อดังสองไดเรคทอรีที่สำคัญคือ
- C:\usr\local\include สำหรับเก็บ header file
- C:\usr\local\lib สำหรับเก็บไลบรารีที่อยู่ในรูปไฟล์ .lib
- C:\usr\local\bin สำหรับเก็บไลบรารีที่อยู่ในรูปไฟล์ .dll
หลังจากนั้นผู้เขียนจะกำหนด environmental variable ของ Windows สองตัวดังต่อไปนี้ (คุณสามารถดูวิธีกำหนด environmental variable ได้ที่ เวบไซต์นี้)
- เพิ่ม C:\usr\local\bin เข้าในตัวแปร PATH ซึ่งมีอยู่แล้ว
- สร้างตัวแปร CMAKE_PREFIX_PATH ใหม่และให้มันมีค่าเท่ากับ C:\usr\local
CMake จะดูค่าของตัวแปร CMAKE_PREFIX_PATH เวลามันค้นหาไดเรคทอรีของไลบรารีต่างๆ การกำหนด CMAKE_PREFIX_PATH ไม่มีความจำเป็นใน Linux หรือ Mac OS เนื่องจากแพลตฟอร์มเหล่านั้นมีมาตรฐานโครงสร้างไดเรคทอรีกำหนดอยู่ตายตัว ไม่เหมือนกับ Windows
ลง GLUT
คุณสามารถดาวน์โหลด GLUT สำหรับ Windows ได้ที่เวบไซต์ของ Nate Robins เราแนะนำให้คุณดาวน์โหลด glut-3.7.6-bin.zip มาแล้วขยายมันออก แล้วให้คัดลอกไฟล์เหล่านี้ใส่ไดเรคทอรีดังต่อไปนี้
- glut32.dll ลงใน C:\usr\local\bin
- glut32.lib ลงใน C:\usr\local\lib
- glut.h ลงใน C:\usr\local\include\GL โดยที่คุณต้องสร้างไดเรคทอรี C:\usr\local\include\GL ขึ้นมาก่อน
- โปรแกรมวาดรูปสี่เหลี่ยมจัตุรัส
เราจะเขียนโปรแกรม square สำหรับวาดรูปสี่เหลี่ยมจัตุรัสโดยใช้ OpenGL และ GLUT อันดับแรกเราจะสร้างไดเรคทอรีของโปรเจคซึ่งมีไฟล์อยู่ดังต่อไปนี้
sample/ build/ src/ CMakeLists.txt config.h.in square/ CMakeLists.txt square.cpp
ซึ่งเหมือนกับโครงสร้างไดเรคทอรีในบทความเกี่ยวกับการตรวจสอบแพลตฟอร์มก่อนหน้านี้ ไฟล์ config.h.in นั้นมีเนื้อหาเหมือนเดิม ที่เปลี่ยนคือไฟล์ CMakeLists.txt ทั้งสองไฟล์ แต่เราจะพูดถึงมันทีหลัง
ไฟล์ square.cpp มีเนื้อหาดังนี้
<geshi lang="c">
- include "../config.h"
- ifdef __WIN_PLATFORM__
- include <windows.h>
- endif
- ifdef __MAC_PLATFORM__
- include <OpenGL/gl.h>
- include <OpenGL/glu.h>
- include <GLUT/glut.h>
- else
- include <GL/gl.h>
- include <GL/glu.h>
- include <GL/glut.h>
- endif
void display() {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClearDepth(1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0f, 1.0f, 1.0f); glBegin(GL_QUADS); glVertex2f(-0.5f, -0.5f); glVertex2f( 0.5f, -0.5f); glVertex2f( 0.5f, 0.5f); glVertex2f(-0.5f, 0.5f); glEnd(); glutSwapBuffers();
}
int main(int argc, char **argv) {
glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutCreateWindow("CMake with OpenGL and GLUT"); glutInitWindowSize(512, 512); glutDisplayFunc(display); glutMainLoop(); return 0;
} </geshi>
สังเกตว่าในโค้ดข้างบน ก่อนที่ถึงโค้ดที่ทำงานจริง มีโค้ดส่วนหนึ่งที่ใช้ include ไฟล็ที่เหมาะสมซึ่งค่อนข้างยาว (13 บรรทัด) แสดงให้เห็นถึงความยุ่งยากในการเขียนโปรแกรมภาษา C++ ให้ทำงานได้หลายแพลตฟอร์ม ถึงแม้ว่าเราจะมีตัวช่วยอย่าง CMake อยู่ก็ตาม (ดังนั้นจึงไม่แปลกดอกที่คนหันไปใช้ Java หรือ C# มากกว่า)
การใช้ OpenGL และ GLUT มีรายละเอียดพอสรุปได้ดังนี้
- แพลตฟอร์มต่างๆ เก็บ OpenGL อยู่คนละที่กัน
ดังนั้นเราจึงต้องมีการใช้ preprocessor macro ใน config.h สำหรับตรวจสอบแพลตฟอร์ม และนี่เป็นเหตุผลที่เราต้อง include ../config.h
- ใน Windows ก่อนที่เราจะ include GL/gl.h เราจะต้องทำการ
include windows.h ก่อนเสมอ แต่ในแพลตฟอร์มอื่นเราไม่ต้องทำเช่นนั้น
- หากผู้อ่านติดตั้ง GLUT ตามที่ผู้เขียนบอกไว้แล้ว
วิธีการ include ไฟล์ของ OpenGL และ GLUT ใน Windows และใน Linux จะมีลักษณะเหมือนกัน กล่าวคือทั้งสามไฟล์อยู่ในไดเรคทอรี GL ซึ่งอยู่ในไดเรคทอรีที่ CMake ไปหา OpenGL และ GLUT เจออีกที
- ในทางกลับกัน Mac OS เก็บ OpenGL และ GLUT แยกกัน
แถมยังเก็บไม่เหมือนของระบบอื่นๆ เลย ทำให้ชื่อไฟล์ header ของ OpenGL ต้องมี OpenGL/ นำหน้า ส่วน header ของ GLUT ต้องมี GLUT/ นำหน้า
คำสั่ง FIND_PACKAGE
CMake เรียกไลบรารีภายนอกว่า "package" และมีคำสั่ง FIND_PACKAGE ไว้สำหรับค้นหาที่อยู่และข้อมูลจำเป็นอื่นๆ ของ package ที่ผู้ใช้กำหนด คำสั่ง FIND_PACKAGE มีรูปแบบการใช้ดังต่อไปนี้
FIND_PACKAGE(<ชื่อ package>)
และถ้าหากการคอมไพล์โปรเจคต้องใช้ package ที่เราต้องการหา เราอาจสั่ง
FIND_PACKAGE(<ชื่อ package>) REQUIRED)
เพื่อบอกให้ CMake พิมพ์ข้อความแสดงความผิดพลาดหากหา package ไม่เจอ
ในกรณีของโปรเจคในบทความนี้ เราจะใช้ package สองตัวคือ OpenGL และ GLUT (คุณสามารถดู package อื่นๆ ที่ CMake ค้นหาเป็นได้จาก documentation ของ CMake) ฉะนั้นเราจะต้องมีการเพิ่มสคริปต์สองบรรทัดข้างล่างนี้
FIND_PACKAGE(OpenGL REQUIRED) FIND_PACKAGE(GLUT REQUIRED)
ลงในไฟล์ src/CMakeLists.txt ดังต่อไปนี้
CMAKE_MINIMUM_REQUIRED(VERSION 2.6) PROJECT(sample)
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)
FIND_PACKAGE(OpenGL REQUIRED) FIND_PACKAGE(GLUT REQUIRED)
ADD_SUBDIRECTORY(square)
CONFIGURE_FILE( config.h.in ${CMAKE_SOURCE_DIR}/config.h )
สังเกตเราใส่คำสั่ง FIND_PACKAGE ไว้ก่อนการประกาศ target ต่างๆ ทุก target เนื่องจาก target ที่เราประกาศอาจต้องการใช้ package ที่เราหาในภายหลัง
- ให้ target เรียกใช้ package
ก่อนที่เราจะคอมไพล์ target หนึ่งที่เรียกใช้ package หนึ่งได้นั้น เราจะต้องให้ข้อมูลสองประการต่อไปนี้กับคอมไพเลอร์
1. ไดเรคทอรีที่ให้คอมไพเลอร์ไปหาไฟล์ header ของ package
2. ไฟล์ไลบรารีที่คอมไพเลอร์จะต้องลิงก์เข้ากับผลลัพธ์ที่ได้จากการคอมไพล์ไฟล์ของ target
หลังจากใช้ FIND_PACKAGE หาที่อยู่ของ package แล้ว โดยมากข้อมูลไดเรคทอรีที่อยู่ของไฟล์ header นั้นจะอยู่ในตัวแปรชื่อ <ชือ่ package ตัวพิมพ์ใหญ่>_INCLUDE_DIR และข้อมูลไลบรารีที่จะต้องลิงก์จะอยู่ตัวแปรชื่อ <ชื่อ package ตัวพิมพ์ใหญ่>_LIBRARIES ฉะนั้นชื่อตัวแปรที่เก็บข้อมูลเหล่านี้ของ OpenGL และ GLUT ได้แก่ OPENGL_INCLUDE_DIR, OPENGL_LIBRARIES, GLUT_INCLUDE_DIR, และ GLUT_LIBRARIES
เราควรจะบอกข้อมูลข้างต้น**เป็นราย target ไป** เนื่องจาก target แต่ละตัวอาจใช้ package ไม่เหมือนกัน ดังนั้นคำสั่งที่ใช้กำหนดข้อมูลเหล่านี้จึงควรอยู่ใน CMakeLists.txt ของ target แต่ละตัว ในกรณีของโปรแกรม square ไฟล์ CMakeLists.txt ของมันมีเนื้อหาดังต่อไปนี้
INCLUDE_DIRECTORIES(${OPENGL_INCLUDE_DIR} ${GLUT_INCLUDE_DIR})
ADD_EXECUTABLE(square square.cpp)
TARGET_LINK_LIBRARIES(square ${OPENGL_LIBRARIES} ${GLUT_LIBRARIES})
คำสั่ง INCLUDE_DIRECTORIES
มีรูปแบบ
INCLUDE_DIRECTORIES(<ไดเรคทอรี 1> <ไดเรคทอรี 2> ...)
โดยที่เราสามารถใส่ไดเรคทอรีกี่ไดเรคทอรีก็ได้ในวงเล็บ หลังจากสั่งคำสั่งนี้แล้ว คอมไพเลอร์จะคอมไพล์ target ทุก target โดยใช้ไดเรคทอรีที่กำหนดในการค้นหาไฟล์ header (_หมายเหตุ:_ จริงๆ แล้วไม่ใช่ target ทุก target เสียทีเดียว แต่เป็นทุก target ในไฟล์ CMakeLists.txt เดียวกัน หรือไฟล์ CMakeLists.txt อื่นๆ ที่ปรากฏอยู่ในไดเรคทอรีย่อยที่ถูกเพิ่มผ่าน ADD_SUBDIRECTORY กล่าวคือ คำสั่ง INCLUDE_DIRECTORIES ที่ปรากฏใน src/square/CMakeLists.txt จะไม่มีผลต่อ target ที่ประกาศใน src/cube/CMakeLists.txt หรือ src/cube/star/CMakeLists.txt เลย)
คำสั่ง TARGET_LINK_LIBRARIES
มีรูปแบบ
TARGET_LINK_LIBRARIES(<ชื่อ target> <ไลบรารี 1> <ไลบรารี 2> ...)
โดยที่เราสามารถใส่ไลบรารีลงไปกี่ไลบรารีก็ได้หลังชื่อ target คำสั่ง TARGET_LINK_LIBRARIES นี้เนื่องจากมันต้องใช้ชื่อ target มันจึงถูกเรียกอยู่หลังคำสั่ง ADD_EXECUTABLE หรือ ADD_LIBRARY (ที่เรายังไม่ได้พูดถึง) ซึ่งเป็นตัวสร้าง target เสมอ เมื่อสั่ง TARGET_LINK_LIBRARIES แล้ว target ที่กำหนดจะถูกลิงก์เข้ากับไลบรารีที่เรากำหนดให้
สรุป
หากต้องการใช้ไลบรารี (package) ภายนอกให้
1. สั่ง FIND_PACKAGE(<package>) ใน CMakeLists.txt ของโปรเจค
2. สั่ง INCLUDE_DIRECTORIES(<PACKAGE>_INCLUDE_DIR) ใน CMakeLists.txt ของ target ที่จะต้องใช้ package ก่อนการประกาศ target นั้นด้วย ADD_EXECUTABLE หรือ ADD_LIBRARY
3. สั่ง TARGET_LINK_LIBRARIES(<target> <PACKAGE>_LIBRARIES) ใน CMakeLists.txt ของ target หลังประกาศ target แล้ว