CMake’te Fonksiyonlar ve Makrolar

Bir önceki yazıda projelerin büyüdükçe tek bir dosya ve dizine sığdırılmaya çalışılmasının çeşitli zararlarından bahsetmiştim. Bunu önlemek için projeyi çeşitli farklı kaynak kodlara ve alt dizinlere bölmemiz gerekiyordu. Kod olarak düşünecek olursak, böyle bir şeyi yapmak için projede birden fazla sınıf veya fonksiyon oluşturmamız gerekir. Ancak bu şekilde mantıksal bir parçalama işlemi yapmamız mümkün olur. İşte bir kodu veya bir algoritmayı böldüğümüz bu yeniden kullanılabilir yapılara fonksiyon (function) ismini vermekteyiz.

Fonksiyon sözcüğü dilin paradigmasına ve kullanım şekline göre çeşitli isimler alabilir: Metot, prosedür, rutin vb. CMake’te de elbette belli parametreler alan ve belli işleri yerine getiren bu gibi yapılar bulunmaktadır. CMake’te kodları bölüp yeniden kullanılabilir parçalara dönüştürebileceğiniz iki dil özelliği bulunmaktadır: Fonksiyon (Function) ve Makro (Macro). Bu yapıların görevleri diğer programlama dillerindeki karşılıklarına benzemektedir. Ancak anlam olarak aslında C ve C++ dillerindeki karşılıklarına daha yakındırlar.

CMake’te fonksiyonlar yeni bir faaliyet alanı (scope) oluştururken, makrolar bunu yapmazlar. Makrolar tıpkı C’de #define önişlemci komutu ile tanımladığımız makrolar gibi, çağrıldığı yere önişleme aşamasında üretilen kodu yapıştırırlar. Ayrıca CMake’te fonksiyonlara verilen argümanlar, fonksiyonun içinde kullanılabilecek değişkenler olarak ele alınırken, makrolara verilen argümanlar basit bir String olarak ele alınmaktadırlar. Aslında CMake’te bu iki kavram arasındaki en bariz farklar bunlardır. Şimdi CMake’te nasıl fonksiyon ve makro tanımlayabileceğimize bakalım:

function(<name> [<arg1> …])
<commands>
endfunction()

macro(<name> [<arg1> …])
<commands>
endmacro()

Tıpkı if ve foreach yapılarında olduğu gibi fonksiyon ve makrolar da function ve macro anahtar kelimesi ile başlayıp endfunction ve endmacro anahtar kelimesi ile sonlandırılırlar. Aradaki “<commands>” kısmına fonksiyon veya makronun yapacağı işleri belirten çeşitli komutlar yazılabilir. Fonksiyon veya makronun “<name>” ile belirtilen ilk parametresine yazılan isim, her zaman fonksiyonun veya makronun ismini belirtir ve bu isimle çağrılırlar. Örneğin print_message isimli ve argüman almayan bir fonksiyon tanımlayıp onu çağıralım:

function(print_message)
    message("Hello from print_message");
    message("Mustafa Yemural")
endfunction()

print_message();
ÇIKTI
Hello from print_message
Mustafa Yemural

Fonksiyon veya makro ismi sadece harflerden, sayılardan ve alt çizgi (_) karakterinden meydana gelebilir. Ayrıca bu isimlerde büyük/küçük harf uyumluluğu aranmaz (case-insensitive). Yani mesela küçük harflerle tanımladığınız yukarıdaki print_message isimli fonksiyonu çok değişik yazımlarla çağırmanız mümkündür:

Print_message()
PRINT_message()
PRINT_MESSAGE()
Print_Message()

Elbette CMake dokümanında yer alan komutlar genellikle küçük harflerden ve alt çizgi karakterinden oluşan bir yazım biçimini baz alırlar. Fonksiyon ve makrolarla ilgili bahsedebileceğim bir ortak özellik de CMake’in çok eski versiyonlarında fonksiyon isminin aynı zamanda endfunction veya endmacro yanındaki parantezin içine de yazılma zorunluluğudur (tıpkı if gibi yapılarda olduğu gibi). Ancak güncel versiyonlarda buna ihtiyaç duyulmamaktadır.

Şimdi argümanlar konusuna biraz daha ayrıntılı olarak değinelim. Daha önce söykediğim gibi, fonksiyon ve makro argümanları arasındaki tek ve en önemli fark; fonksiyonlarda argümanların değişken olarak, makrolarda ise basit bir String olarak ele alınmasıdır. Peki bu tam olarak neyi değiştirir? Örneğin fonksiyonlarda kullanılan argümanlar daha önce gördüğümüz DEFINED operatörü ile if koşulunda kontrol edilebilirler. Ancak makrolardaki argümanlarda bir if kontrolü gerçekleştirmek istediğinizde bu sefer String üzerinde işlem yapıyormuşsunuz gibi algılanır. Örnek:

function(test_function arg)
    if(DEFINED arg)
        message("It is a defined variable (for function)")
    else()
        message("It is a not defined variable (for function)")
    endif()
endfunction()

macro(test_macro arg)
    if(DEFINED arg)
        message("It is a defined variable (for function)")
    else()
        message("It is a not defined variable (for function)")
    endif()
endmacro()

test_function(test)
test_macro(test)
ÇIKTI
It is a defined variable (for function)
It is a not defined variable (for function)

Yukarıdaki örnekten anlayabileceğiniz gibi, fonksiyon ve makro argümanlarının bu farklarına dikkat ederek onları kullanmanız gerekir. Şimdi de birden fazla argüman kullanımına küçük bir örnek verelim:

project(Test)

macro(build_engine targetName externalLib)
    add_executable(${targetName} Engine/Core/core.cpp)
    target_include_directories(${targetName} PUBLIC Engine/Core)
    target_link_libraries(${targetName} PRIVATE ${externalLib})
endmacro()

build_engine(Core library.dll)

Bu örnekte aslında biraz daha kullanışlı bir makro yazmış olduk. Fonksiyon ve makroları kullanarak tekrarlanan işleri nasıl kısaltabileceğimizi bir nevi öğrenmiş olduk. Şimdi bu örnekler üzerinden geliştirmeler yaparak ilerleyelim. Örneğin ben tek bir kaynak dosyasını değil de birden fazla kaynak dosyayı aynı hedef ile derlemek istiyorum. Bunu yapmak için fonksiyon veya makroyu değişken sayıda argüman alacak bir hale getirmemiz lazım. Peki CMake’te değişken sayıda argüman alan makro veya fonksiyon nasıl yazılır? Bunun için öncelikle şu 3 değişkenin anlamını açıklamamız gerekir:

  • ARGC: Bir fonksiyona veya makroya verilen toplan argüman sayısını belirtir. Bu sayı hem ismi açık olarak belirtilen argümanları hem de ismi verilmeyen argümanları kapsar.
  • ARGV: Fonksiyona geçilen her bir argümanı içeren bir liste değişkenidir. Yine isimli ve isimsiz argümanları içermektedir.
  • ARGN: ARGV ile aynıdır, ancak sadece isimsiz argümanları içerir.

Bu değişkenler fonksiyon veya makro tanımları içerisinde kullanıldıklarında özel anlamlar taşırlar. Bunlara ek olarak ARGV# olarak belirtilen bir değişken listesi de vardır. Buradaki “#” karakteri yerine bir doğal sayı gelebilir. Bu şekilde onu ARGV0, ARGV1, … ARGV9 şeklinde kullanabiliriz. ARGV0 ilk argümanı, ARGV1 ikinci argümanı belirtir ve bu şekilde gider. Bu argümanlara isimli argümanlar dahildir. Şimdi az önce yazdığımız örneği bu değişkenleri kullanarak tekrar düzenleyelim:

project(Test)

macro(build_engine targetName)
    add_executable(${targetName} ${ARGN})
    target_include_directories(${targetName} PUBLIC Engine/Core)

    message("All Arguments: ${ARGV}")
    message("Unnamed Arguments: ${ARGN}")
    message("Argument Count: ${ARGC}")
endmacro()

build_engine(Core Engine/Core/core.cpp Engine/Core/math.cpp Engine/Core/calc.cpp)
ÇIKTI
All Arguments: Core;Engine/Core/core.cpp;Engine/Core/math.cpp;Engine/Core/calc.cpp
Unnamed Arguments: Engine/Core/core.cpp;Engine/Core/math.cpp;Engine/Core/calc.cpp
Argument Count: 4

Burada targetName isimli argüman, bunun dışında verilenler ise isimsiz argümandır. Çıktıdan anlayabileceğiniz gibi ARGV tüm argümanları, ARGN ise sadece isimsiz argümanları içeren bir listedir. ARGC ise tüm argümanların sayısını vermektedir. Bu şekilde örnekteki gibi birden fazla kaynak kod dosyasını tek bir hedef ile birleştiren fonksiyonlar veya makrolar yazabiliriz. Şimdi de ARGV# kullanımıyla ilgili örnek verelim:

project(Test)

macro(build_engine targetName)
    add_executable(${ARGV0} ${ARGN})
    target_include_directories(${ARGV0} PUBLIC Engine/Core)

    message("Second Argument: ${ARGV1}")
    message("Third Argument: ${ARGV2}")
    message("Fourth Argument: ${ARGV3}")
endmacro()

build_engine(Core Engine/Core/core.cpp Engine/Core/math.cpp)
ÇIKTI
Second Argument: Engine/Core/core.cpp
Third Argument: Engine/Core/math.cpp
Fourth Argument:

Yine bu örnekte fonksiyona veya makroya verilmeyen bir argümanın ARGV# ile elde edilmeye çalışılmasında boş String veya değişken gibi davranılacağı anlaşılmaktadır. Bizim burada yaptığımız örnekte bir sorun olmasa da bu konuda dikkatli olmakta fayda vardır. ARGV veya ARGN değişkenleri aslında liste içerdiğinden, bu liste elemanları üzerinde foreach ile dolaşıp ayrı ayrı işlemler yapmamız da mümkündür. Şimdi bir fonksiyonda bunu yapalım:

function(print)
    foreach(message IN LISTS ARGN)
        message("Message: ${message}")
    endforeach()
    
endfunction()

print(Selam Mustafa Yemural)
ÇIKTI
Message: Selam
Message: Mustafa
Message: Yemural

Daha önce söylediğim gibi, bu tip durumlarda makro kullanırken dikkatli olmakta fayda var. Çünkü foreach döngüsünde kullanılan LISTS anahtar kelimesinden sonra, normalde bir değişken gelmesi gerekir. Ancak makrolarda kullanılan ARGN bir değişken değildir. Yukarıdaki örneği şu şekilde değiştirdiğimizde herhangi bir mesaj çıktısı alamayız:

macro(print)
    foreach(message IN LISTS ARGN)
        message("Message: ${message}")
    endforeach()
    
endmacro()

print(Selam Mustafa Yemural)

Bu nedenle bu tip durumlarda makro yerine fonksiyon kullanmanız daha iyi olur. Şimdi isimsiz argümanları bir kenara bırakıp daha farklı bir argüamn yapısına geçelim. Öncelikle daha önce gördüğümüz ve çeşitli hedeflerlere çeşitli kütüphaneleri bağlamak için kulanılan target_link_libraries komutunun genel formuna bakalım:

target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>…
[<PRIVATE|PUBLIC|INTERFACE> <item>…]…)

Aslında bu komut da değişken sayıda argüman alan bir komut olmasına rağmen bazı farkları bulunmaktadır. Örneğin hedef ismini belirttikten sonra bir veya birden fazla kütüphane ismini belirtmek için PRIVATE, PUBLIC veya INTERFACE anahtar kelimelerinden birini kullanmamız gerekir. Bu tip argümanlara CMake’te Anahtar Kelime Argümanı (Keyword Argument) adı verilmektedir. Bunu bizim fonksiyonlarımızda veya makrolarımızda da yapmamız mümkündür. Bunun için öncelikle cmake_parse_arguments komutunu tanıyalım:

include(CMakeParseArguments) # CMake 3.4 and earlier
cmake_parse_arguments(prefix
                      noValueKeywords
                      singleValueKeywords
                      multiValueKeywords
                      argsToParse)

Burada CMake 3.4 veya daha öncesi bir versiyonu kullanıyorsanız, “CMakeLists.txt” dosyasına cmake_parse_arguments komutunu kullanmadan önce CMakeParseArguments modülünü eklemeniz gerekir. CMake 3.5 ve sonrası için buna gerek yoktur. cmake_parse_arguments komutu, onun argsToParse parametresine verdiğiniz argümanları belli anahtar kelimelere göre işler ve bizim kullanabileceğimiz bir hale getirir. Genellikle argsToParse kısmına ARGN değişkeni verilmektedir. Bu komutta yer alan anahtar kelime alan parametrelerden hepsi, fonksiyon tarafından destekelenecek olan anahtar kelimelerin bir listesini alır.

noValueKeywords herhangi bir argüman almayacak olan anahtar kelimeleri belirtir. Bu tıpkı add_executable komutundaki EXCLUDE_FROM_ALL anahtar kelimesi gibi, fonksiyonda bir özelliğin etkinleştirilip etkinleştirilmeyeceğini belirten anahtar kelimlerdir. singleValueKeywords ise yazıldıktan sonra sadece bir argüman alabilen anahtar kelimelerin listesini belirtir. multiValueKeywords sıfır veya daha fazla argüman alabilen anahtar kelimelerin listesini belirtir. Zorunlu bir durum olmasa da geleneksel olarak anahtar kelime isimleri tıpkı diğer komutlarda olduğu gibi büyük yazılmaktadır.

Şimdi bu komutla ilgili son ve en önemli detayı açıklayalım. Bu komutun prefix kısmına bir ön ek yazılabilir. Bu ön ek ile cmake_parse_arguments komutundan çıkan sonuçların değerlerini kolayca alabiliriz. Örneğin TARGET isimli bir anahtar kelimemiz ve ARG isimli bir ön ekimiz olsun. Bu fonksiyon çalıştıktan sonra TARGET için verilen değerleri ARG_TARGET şeklinde alabiliriz. Yani buradaki prefix parametresi bize anahtar kelimelere verilen değerleri almak ve işlemek için kolaylık sağlar. Şimdi anahtar kelime argümanları ile ilgili bir örnek yapalım. Aslında burada sadece daha önce yazdığımız build_engine isimli fonksiyonumuzu/makromuzu daha da genişletmiş oluyoruz:

function(build_engine)
    # Define variables which will use in argument parsing
    set(prefix ARG)
    set(noValues ENABLE_COMPLETE_MESSAGE)
    set(singleValues TARGET)
    set(multiValues SOURCES INCLUDES LIBS)

    # Process the arguments passed in
    include(CMakeParseArguments)
    cmake_parse_arguments(${prefix}
                         "${noValues}"
                         "${singleValues}"
                         "${multiValues}"
                         ${ARGN})

    add_executable(${${prefix}_TARGET} ${${prefix}_SOURCES})
    target_include_directories(${${prefix}_TARGET} PUBLIC ${${prefix}_INCLUDES})
    target_link_libraries(${${prefix}_TARGET} PUBLIC ${${prefix}_LIBS})

    if(${prefix}_ENABLE_COMPLETE_MESSAGE)
        message("Single Value TARGET = ${${prefix}_TARGET}")
        message("Multi Value SOURCES = ${${prefix}_SOURCES}")
        message("Multi Value INCLUDES = ${${prefix}_INCLUDES}")
        message("Multi Value LIBS = ${${prefix}_LIBS}")
        message("No Value ENABLE_COMPLETE_MESSAGE = ${${prefix}_ENABLE_COMPLETE_MESSAGE}")
    endif()
endfunction()

build_engine(TARGET Core SOURCES Engine/Core/core.cpp Engine/Core/math.cpp Engine/Core/calc.cpp INCLUDES Engine/core LIBS testlib.dll testlib2.dll ENABLE_COMPLETE_MESSAGE)
ÇIKTI
Single Value TARGET = Core
Multi Value SOURCES = Engine/Core/core.cpp;Engine/Core/math.cpp;Engine/Core/calc.cpp
Multi Value INCLUDES = Engine/core
Multi Value LIBS = testlib.dll;testlib2.dll
No Value ENABLE_COMPLETE_MESSAGE = TRUE

Sanırım işlevsel olarak bu fonksiyonun ne yaptığını anlatmama pek gerek yok; verilen isimde ve kaynak dosyalarda bir hedef oluşturup yine verilen başlık dosyaları ve kütüphaneleri bu hedefe bağlıyor. Ayrıca ENABLE_COMPLETE_MESSAGE seçeneği girilirse yukarıdaki mesajları ekrana basıyor, girilmezse basmıyor. Burada verdiğimiz prefix bilgisinin kullanımını çok rahat bir şekilde görebilirsiniz. Ayrıca yukarıda yazdığımız build_engine isimli fonksiyonu kendiniz de çok çeşitli şekillerde çağırarak, anahtar kelime argümanlarını daha iyi anlayabilirsiniz.

Fonksiyonların makrolardan farkının yeni bir değişken faaliyet alanı oluşturmak olduğunu söylemiştik. Bunun anlamı bir fonksiyon çağrıldığında, onun içinde yer alan değişkenlerin aynı isimli dış faaliyet alanındaki değişkenlere hiçbir etkisinin olmamasıdır. Ancak makrolar yeni bir değişken faaliyet alanı oluşturmadığından çağrılan faaliyet alanında değişikliğe neden olabilirler. Peki bu durumda fonksiyonlar çağrılan faaliyet alanında nasıl değişiklik yapabilirler? Sonuçta C veya C++ dilindeki gibi CMake’te bir fonksiyondan geri dönüş mekanizması yer almaz.

Aslında soruyu şöyle değiştirsek daha kolay olur: Bir CMake fonksiyonundan nasıl değer geri döndürebiliriz? Dediğim gibi bunun söz dizimi açısından bir yolu yok. Ancak bu durum onun imkansız olduğu anlamına gelmez. CMake’te fonksiyonlara belli bir faaliyet alanında geçilen argümanların değerlerini değiştirmeniz elbette mümkündür. Bunu yapmak için set komutunun PARENT_SCOPE anahtar kelime argümanı kullanılmaktadır. Bu şekilde fonksiyonun çağrıldığı faaliyet alanındaki bir değişkenin değerini değiştirebiliriz. Şimdi bir örnek üzerinden bunu anlatalım:

function(var_init variable)
    set(${variable} "Initial Value" PARENT_SCOPE)
endfunction()

set(test2 "Other Value")

var_init(test)
var_init(test2)

message("Test: ${test}")
message("Test2: ${test2}")
ÇIKTI
Test: Initial Value
Test2: Initial Value

Örnekte fonksiyona bir test argümanı geçtiğinizde zaten o faaliyet alanında yeni bir değişken oluşturmuş oluyorsunuz. test2 değişkenini ise daha önceden oluşturulan bir değişkene olan etkisini göstermek için kullandım. Gördüğünüz gibi fonksiyona argüman olarak geçilen değişkenlere, fonksiyonun içerisinde PARENT_SCOPE ile bir değer atandı ve çağrı yapılan faaliyet alanında bu değişkenlerin değerleri değişti. İşte bu, bir fonksiyondan değer döndürme anlamını tam karşılamasa da bir fonksiyonun çağrıldığı faaliyet alanındaki değişkenleri nasıl değiştirebildiğinin yegane göstergesidir.

Burada makrolar ile ilgili çok küçük bir bilgi vermekte fayda var; makrolar zaten yeni bir faaliyet alanı oluşturmadığından onların içerisinde PARENT_SCOPE anahtar kelimesini kullanmaktan kaçınmanız gerekir. Makrolar zaten o anki faaliyet alanını etkilerler. Hatta o anki faaliyet alanında bu tür değişiklikler yapmak istiyorsanız PARENT_SCOPE kullanmadan makroları kullanmak bu iş için iyi bir pratiktir. Bu nedenle bu tip durumlarda makroları kullanmayı tercih etmeniz daha az karmaşaya sebep olacaktır.

Peki CMake’teki return komutu tam olarak ne işe yarar? Bu komutun bir geri dönüş değeri gönderme işine yaramadığı açıktır. Ancak bu komut ile akış, çağrılan faaliyet alanına geri döner. Yani bir nevi fonksiyonu sonlandırmış olursunuz. Ancak return komutunu makrolarda da kullanabildiğinizi unutmayın. Elbette bu beklemediğiniz birçok şeye sebebiyet verebilir. Örneğin bir fonksiyon içerisinde bir makroyu çağırıp onun içerisinde de return komutunu kullanırsanız, şu şekilde ilginç sonuçlar ile karşılaşabilirsiniz:

macro(inner)
    message("Beginning of inner macro")
    return() # Dangerous!
    message("End of inner macro (never called)")
endmacro()

function(test)
    message("Beginning of test function")
    inner()
    message("End of test function (never called)")
endfunction()

test()
ÇIKTI
Beginning of test function
Beginning of inner macro

Bu nedenle makrolarda return komutunun kullanmaktan kaçınmalısınız. Şimdi de yazımızın son konusuna geçelim. CMake’te zaten tanımlı olan bir komut/fonksiyon/makro ile aynı isimli başka bir fonksiyon veya makro tanımlayıp bunu çağırdığınızda ne olur? Bu durumda CMake eskisini alt çizgili (_) hale getirip bu şekilde kullanıma sunar. Bu davranış aslında CMake’te dokümante edilmemiştir. Ayrıca bu davranış yeniden tanımlanan şey zaten varolan bir CMake komutu olsa da kullanıcının daha önceden tanımladığı bir fonksiyon veya makro olsa da değişmez. Şimdi örneğe bakalım:

function(func)
    message("Old Function")
endfunction()

function(func)
    message("New Function")
endfunction()

func()
_func()
ÇIKTI
New Function
Old Function

Yukarıda func fonksiyonu iki kere ard arda tanımlanmıştır. Bu fonksiyonu çağırdığınızda en son yapılan tanımlama dikkate alınacaktır. Ancak eski tanımlamayı da kullanmak isterseniz bu sefer alt tireli (_) hali ile çağırmanız gerekecektir. Bu durum tıpkı C++’ta fonksiyonları ezmeye (override) benzemektedir. Ancak tabi C++’ta ezilmiş olan tanım bir daha aynı nesne için kullanılamaz. CMake’te bir fonksiyonu ikiden fazla kere ezdiğinizde artık eski tanımlara ulaşılamaz. Yani iki önceki tanıma ulaşmak için başına iki alt tire (_) koymak gibi teknikler işe yaramayacaktır. Örnek:

function(func)
    message("Not Printed")
endfunction()

function(func)
    message("Old Function")
endfunction()

function(func)
    message("New Function")
endfunction()

func()
_func()
# __func()  # ERROR!
ÇIKTI
New Function
Old Function

Böylelikle fonksiyonlar ve makrolar yazımızın sonuna gelmiş olduk. Bu yazıda CMake’te fonksiyon ve makroları nasıl oluşturabileceğimizi ve kullanabileceğimizi gördük. Ayrıca değişken sayıda argüman alabilen ve anahtar kelime argümanları alabilen fonksiyonların parametrik yapılarını ve argümanları nasıl elde edebileceğimizi gördük. Fonksiyon ve makrolar arasındaki temel faaliyet alanı farklılıklarından bahsedip son olarak da aynı isimli birden fazla fonksiyon tanımı yaptığımızda CMake’in nasıl bir davranış sergilediğini gördük. Bir sonraki yazıda görüşmek üzere.

5 2 votes
Article Rating
Subscribe
Bildir
guest

0 Yorum
Inline Feedbacks
View all comments