Оглядываясь на материал, рассмотренный в этой книге, можно сказать, что синтаксис CMake уже начинает напоминать язык программирования. Он поддерживает переменные, логику «if-then-else», циклы и включение других файлов для обработки. Не стоит удивляться тому, что CMake также поддерживает такие распространенные в программировании понятия, как функции и макросы. Как и в других языках программирования, функции и макросы являются основным механизмом для проектов и разработчиков, позволяющим расширить функциональность CMake и инкапсулировать повторяющиеся задачи естественным образом. Они позволяют разработчику определять многократно используемые блоки кода CMake, которые можно вызывать так же, как обычные встроенные команды CMake. Они также являются краеугольным камнем собственной системы модулей CMake (рассматривается отдельно в главе 12, «Модули»).
9.1. Основы
Функции и макросы в CMake очень похожи на свои одноименные аналоги в C/C++. Функции вводят новую область видимости, а аргументы функции становятся переменными, доступными внутри тела функции. Макросы, с другой стороны, эффективно вставляют свое тело в точку вызова, а аргументы макроса подставляются в виде простых строковых замен. Такое поведение отражает работу функций и макросов #define в C/C++. Функция или макрос CMake определяется следующим образом:
После определения функция или макрос вызывается точно так же, как и любая другая команда CMake. Тело функции или макроса выполняется в точке вызова. Например:
Как показано выше, аргумент name определяет имя, используемое для вызова функции или макроса, и оно должно содержать только буквы, цифры и символы подчеркивания. Имя будет обрабатываться без учета регистра, поэтому соглашения о верхнем/нижнем регистре - это скорее вопрос стиля (в документации CMake принято, что имена команд все строчные, а слова разделяются подчеркиванием). Очень ранние версии CMake требовали повторения имени в качестве аргумента endfunction() или endmacro(), но новые проекты должны избегать этого, поскольку это только добавляет ненужный беспорядок.
9.2. Обработка аргументов
Работа с аргументами в функциях и макросах одинакова, за исключением одного очень важного различия. Для функций каждый аргумент является переменной CMake и обладает всеми обычными свойствами переменной CMake. Например, их можно проверять в операторах if() как переменные. В отличие от этого, аргументы макросов являются заменителями строк, поэтому все, что было использовано в качестве аргумента в макровызове, по сути, вставляется туда, где этот аргумент появляется в теле макроса. Если макроаргумент используется в операторе if(), он будет рассматриваться как строка, а не как переменная. Следующий пример и его вывод демонстрируют разницу:
Вывод:
Помимо этого различия, функции и макросы поддерживают одни и те же возможности обработки аргументов. Каждый аргумент в определении функции служит чувствительной к регистру меткой-идентификатором. Для функций эта метка действует как переменная, а для макросов - как строковая подстановка. К значению этого аргумента можно обратиться в теле функции или макроса, используя обычную нотацию переменных, хотя макроаргументы технически не являются переменными.
И вызов func(), и вызов macr() выводят одно и то же:
В дополнение к именованным аргументам функции и макросы поставляются с набором автоматически определяемых переменных (или имен, подобных переменным, в случае макросов), которые позволяют обрабатывать аргументы дополнительным способом:
- ARGC Это значение будет равно общему количеству аргументов, переданных функции. Учитываются именованные аргументы плюс все дополнительные неименованные аргументы, которые были переданы.
- ARGV Это список, содержащий все аргументы, переданные функции, включая именованные аргументы и любые дополнительные неименованные аргументы, которые были переданы.
- ARGN Как и ARGV, только здесь содержатся аргументы, выходящие за рамки именованных (т.е. необязательные, неименованные аргументы).
В дополнение к вышесказанному, на каждый отдельный аргумент можно сослаться как на имя вида ARGVx, где x - номер аргумента (например, ARGV0, ARGV1 и т. д.). Это относится и к именованным аргументам, поэтому на первый именованный аргумент также можно ссылаться через ARGV0 и т. д. Обратите внимание, что использование ARGVx при x >= ARGC следует считать неопределенным поведением.
Типичные ситуации, в которых используются имена ARG..., включают поддержку необязательных аргументов и реализацию команды, которая может принимать произвольное их количество для обработки. Рассмотрим функцию, которая определяет цель исполняемого файла, связывает эту цель с некоторой библиотекой и определяет для нее тестовый пример. Такая функция часто встречается при написании тестовых примеров (эта тема рассматривается в главе 27, «Тестирование»). Вместо того чтобы повторять шаги для каждого тестового случая, функция позволяет определить шаги один раз, и тогда каждый тестовый случай становится простым определением в одну строку.
Приведенный выше пример показывает, насколько полезен ARGN. Он позволяет функции или макросу принимать различное количество аргументов, но при этом указывать набор именованных аргументов, которые должны быть предоставлены.
Однако есть особый случай, о котором следует знать и который может привести к неожиданному поведению. Поскольку макросы рассматривают свои аргументы как подстановки строк, а не как переменные, если они используют ARGN в том месте, где ожидается имя переменной, переменная, на которую она ссылается, будет находиться в области видимости, из которой вызывается макрос, а не ARGN из собственных аргументов макроса. В следующем примере показана такая ситуация:
Выходные данные будут такими:
При использовании ключевого слова LISTS с функцией foreach() необходимо указать имя переменной, но ARGN, заданный для макроса, не является именем переменной. Когда макрос вызывается из другой функции, в макросе используется переменная ARGN из этой вложенной функции, а не ARGN из самого макроса. Ситуация проясняется, если вставить содержимое тела макроса непосредственно в функцию, в которой он вызывается (что, собственно, и будет делать CMake):
В таких случаях следует подумать о том, чтобы сделать макрос функцией, или, если он должен оставаться макросом, избегать обращения с аргументами как с переменными. В приведенном выше примере реализация dangerous() может быть изменена на использование foreach(arg IN ITEMS ${ARGN}), но см. раздел 9.8, «Проблемы с обработкой аргументов», где описаны некоторые возможные предостережения.
9.3. Ключевые слова в качестве аргументов
В предыдущем разделе было показано, как переменные вида ARG... можно использовать для работы с изменяющимся набором аргументов. Этой функциональности достаточно для простого случая, когда требуется только один набор изменяемых или необязательных аргументов, но если необходимо поддерживать несколько наборов необязательных аргументов, обработка становится довольно утомительной. Кроме того, описанная выше базовая обработка аргументов является довольно жесткой по сравнению со многими встроенными командами CMake, которые поддерживают аргументы в виде ключевых слов и гибкий порядок их следования.
Рассмотрим команду target_link_libraries():
В качестве первого аргумента требуется указать имя цели (targetName), но после этого вызывающая сторона может предоставить любое количество секций PRIVATE, PUBLIC или INTERFACE в любом порядке, причем каждая секция может содержать любое количество элементов. Определяемые пользователем функции и макросы могут поддерживать аналогичный уровень гибкости с помощью команды cmake_parse_arguments(), которая имеет две формы. Первая форма поддерживается всеми версиями CMake и работает как для функций, так и для макросов:
Раньше команда cmake_parse_arguments() предоставлялась модулем CMakeParseArguments, но в CMake 3.5 она стала встроенной командой. Строка include(CMakeParseArguments) ничего не даст в CMake 3.5 и более поздних версиях, в то время как в более ранних версиях CMake она определит команду cmake_parse_arguments() (подробнее о таком использовании include() см. в главе 12, «Модули»).
Вторая форма была введена в CMake 3.7 и может использоваться только в функциях, но не в макросах:
Обе формы команды похожи, различаясь только тем, как они принимают набор аргументов для разбора. В первой форме argsToParse обычно задается как ${ARGN} без кавычек. Это позволяет получить все аргументы, переданные вложенной функции или макросу, кроме именованных аргументов, за исключением нескольких особых угловых случаев, которые не применяются в большинстве ситуаций (см. раздел 9.8, «Проблемы с обработкой аргументов»).
Во второй форме опция PARSE_ARGV указывает cmake_parse_arguments() считывать аргументы непосредственно из набора переменных ${ARGVx}, причем x варьируется от startIndex до (ARGC - 1). Поскольку эта функция читает переменные напрямую, она не поддерживает использование внутри макросов. Как уже объяснялось в разделе 9.2, «Обработка аргументов», макросы используют для своих аргументов замену строк, а не переменные. Основное преимущество второй формы в том, что для функций она надежно справляется с угловыми случаями, с которыми не справляется первая форма. Если для вложенной функции нет именованных аргументов, то передача ${ARGV} или ${ARGN} в первую форму эквивалентна передаче PARSE_ARGV 0 во вторую форму, когда ни один из угловых случаев не применим.
В остальном поведение двух форм команды одинаково. Каждый из ...Keywords - это список имен ключевых слов, которые нужно искать при разборе. Поскольку они представляют собой список, их необходимо заключить в кавычки, чтобы обеспечить правильную обработку. ValuelessKeywords определяют отдельные аргументы ключевых слов, которые действуют как булевы переключатели. Наличие ключевого слова означает одно состояние, а его отсутствие - другое. singleValueKeywords требуют ровно одного дополнительного аргумента после ключевого слова при их использовании, в то время как multiValueKeywords требуют ноль или более дополнительных аргументов после ключевого слова. Хотя это и не обязательно, преобладающим является правило, согласно которому ключевые слова должны быть написаны в верхнем регистре, при этом в качестве разделителя используется подчеркивание. Обратите внимание, что ключевые слова не должны быть слишком длинными, иначе они могут быть неудобными в использовании.
Когда cmake_parse_arguments() возвращает управление, могут быть определены переменные, имена которых состоят из указанного префикса prefix, подчеркивания и имени ключевого слова, с которым они связаны. Например, с префиксом ARG переменная, соответствующая ключевому слову FOO, будет ARG_FOO. Для каждого ключевого слова ValuelessKeywords будет определена соответствующая переменная со значением TRUE, если ключевое слово присутствует, или FALSE, если его нет. Для каждого из ключевых слов singleValueKeywords или multiValueKeywords соответствующая переменная будет определена только в том случае, если это ключевое слово присутствует и после него указано значение.
В следующем примере показано, как определяются и обрабатываются три различных типа ключевых слов:
Соответствующий вывод будет выглядеть следующим образом:
Вызов cmake_parse_arguments() в приведенном выше примере можно было бы записать и во второй форме, например, так:
Аргументы могут быть переданы команде таким образом, что останутся аргументы, не связанные ни с одним ключевым словом. Команда cmake_parse_arguments() предоставляет все оставшиеся аргументы в виде списка в переменной <prefix>_UNPARSED_ARGUMENTS. Преимущество формы PARSE_ARGV заключается в том, что если какие-либо нераспарсенные аргументы сами являются списком, то их встроенные точки с запятой будут экранированы. Это сохраняет исходную структуру аргументов, в отличие от другой формы команды, которая этого не делает. Следующий уменьшенный пример демонстрирует это более наглядно:
Вывод:
Внутри функции demoArgs() вызов cmake_parse_arguments() определит переменную ARG_SPECIAL со значением secretSauce. Аргументы burger, fries и cheese;tomato не соответствуют никаким распознанным ключевым словам, поэтому они рассматриваются как оставшиеся аргументы. Как видно из приведенного выше вывода, исходный список cheese;tomato сохраняется, поскольку была использована форма PARSE_ARGV. Этот важный момент рассматривается в разделе 9.8.2, «Пересылка аргументов».
В приведенном выше примере ключевое слово SPECIAL ожидает, что за ним последует единственный аргумент. Если бы в вызове было опущено это значение, cmake_parse_arguments() не выдала бы ошибку. В CMake 3.14 и более ранних версиях проект не смог бы обнаружить такую ситуацию, но в более поздних версиях это исправлено. В CMake 3.15 и более поздних версиях переменная <prefix>_KEYWORDS_MISSING_VALUES будет заполнена списком, содержащим все одно-значные или много-значные ключевые слова, которые присутствовали, но не имели следующего за ними значения. Это можно продемонстрировать, изменив предыдущий пример:
В приведенном выше примере за SPECIAL и ORDINARY сразу следует другое ключевое слово, поэтому у них нет значений, связанных с ними. Оба могут или должны иметь значения, поэтому они будут присутствовать в переменной ARG_KEYWORDS_MISSING_VALUES, заполняемой cmake_parse_arguments(). В случае SPECIAL это, скорее всего, ошибка, но для ORDINARY это может быть допустимо, так как многозначные ключевые слова могут законно не иметь значений. Поэтому разработчики должны быть осторожны в использовании <prefix>_KEYWORDS_MISSING_VALUES.
Команда cmake_parse_arguments() обеспечивает значительную гибкость. Хотя первая форма команды обычно принимает ${ARGN} в качестве набора аргументов для разбора, можно указать и другие аргументы. Этим можно воспользоваться, например, для многоуровневого разбора аргументов:
В приведенном выше примере первый уровень аргументов, разобранных cmake_parse_arguments(), - это обычные ${ARGN}. Единственными ключевыми словами на этом первом уровне являются два многозначных ключевых слова LIB и TEST. Они определяют, к какой цели должны применяться следующие за ними подварианты. На втором уровне разбора в качестве набора аргументов для разбора передается либо ${GRP_LIB}, либо ${GRP_TEST}, а не ${ARGN}. Конфликта, связанного с тем, что в исходном наборе аргументов ARGN вложенные опции встречаются более одного раза, не возникает, поскольку вложенные опции каждой цели разбираются отдельно.
По сравнению с базовой обработкой аргументов с помощью именованных аргументов или переменных ARG..., преимущества cmake_parse_arguments() многочисленны:
- Будучи основанной на ключевых словах, точка вызова приобретает читаемость, поскольку аргументы, по сути, становятся самодокументирующимися. Другим разработчикам, читающим код в точке вызова, обычно не нужно заглядывать в реализацию функции или ее документацию, чтобы понять, что означает каждый из аргументов.
- Вызывающая сторона сама выбирает порядок передачи аргументов.
- Вызывающая сторона может просто опустить те аргументы, которые не нужно предоставлять.
- Поскольку каждое из поддерживаемых ключевых слов должно быть передано в cmake_parse_arguments(), а она обычно вызывается в верхней части функции, обычно становится ясно, какие аргументы поддерживает функция.
- Поскольку разбор аргументов, основанных на ключевых словах, выполняется командой cmake_parse_arguments(), а не специальным парсером, написанным вручную, ошибки разбора аргументов практически исключены.
Хотя эти возможности довольно мощные, команда все же имеет некоторые ограничения. Встроенные команды поддерживают повторение ключевых слов. Например, такие команды, как target_link_libraries(), позволяют использовать ключевые слова PRIVATE, PUBLIC и INTERFACE более одного раза в одной команде. Команда cmake_parse_arguments() не поддерживает это в той же степени. Она возвращает только значения, связанные с последним вхождением ключевого слова, и отбрасывает предыдущие. Парсинг повторяющихся ключевых слов возможен только в том случае, если используется техника многоуровневого разбора ключевых слов и ключевое слово встречается только один раз на любом уровне обрабатываемых аргументов.
9.4. Возвращение значений
Фундаментальное различие между функциями и макросами заключается в том, что функции вводят новую область видимости переменных, а макросы - нет. Функции получают копию всех переменных из вызывающей области видимости. Переменные, определенные или измененные внутри функции, не влияют на одноименные переменные вне функции (если только они не распространяются явно, о чем речь пойдет ниже). Что касается переменных, то функция, по сути, является своей собственной автономной «песочницей», подобно области видимости, создаваемой командой block() (см. раздел 6.4, «Блоки области видимости»). Макросы, с другой стороны, разделяют ту же область видимости переменных, что и их вызывающая функция, и поэтому могут напрямую изменять переменные вызывающей функции. Обратите внимание, что функции не вводят новую область видимости политики (см. раздел 13.3, «Рекомендуемые практики», для дальнейшего обсуждения этого вопроса).
9.4.1. Возвращение значений из функций
В CMake 3.25 и более поздних версиях функция может эффективно возвращать значения, указывая переменные для передачи вызывающей стороне. Это достигается с помощью ключевого слова PROPAGATE в команде return(), аналогично поведению, описанному ранее в разделе 8.4, «Досрочное завершение». Для каждого имени переменной, указанного после PROPAGATE, эта переменная будет обновлена в вызывающей области видимости и будет иметь то же значение, что и в функции на момент вызова return(). Если возвращаемая переменная была снята командой unset() в области видимости функции, она также будет снята в вызывающей области видимости. Политика CMP0140 должна быть установлена на NEW при определении функции, если используется ключевое слово PROPAGATE (глава 13 «Политики» подробно рассматривает политики).
Обычно функция не должна указывать имена переменных, которые должны быть установлены в вызывающей области видимости. Вместо этого следует использовать аргументы, чтобы сообщить ей имена переменных, которые должны быть установлены в вызывающей области видимости. Это гарантирует, что вызывающая сторона полностью контролирует действия функции и что функция не будет перезаписывать переменные, которых вызывающая сторона не ожидает. Собственные встроенные команды CMake обычно следуют этому шаблону. Приведенный выше пример следует этой рекомендации, позволяя вызывающей стороне указать имя переменной result в качестве первого аргумента функции.
Оператор return() распространяет переменные в вызывающую область видимости. Это означает, что любые операторы block() внутри функции не препятствуют распространению переменных в вызывающую функцию, но они будут влиять на значение распространяемой переменной(ых). Предыдущий пример можно немного изменить, чтобы продемонстрировать это:
В CMake 3.24 и более ранних версиях функции не поддерживают возврат значения напрямую. Поскольку функции вводят собственную область видимости переменных, может показаться, что нет простого способа передать информацию обратно вызывающей стороне, но это не так. Как уже говорилось в разделе 6.4, «Блоки области видимости», и разделе 8.1.2, «Область видимости каталога», команды set() и unset() поддерживают ключевое слово PARENT_SCOPE, которое можно использовать для изменения переменной в области видимости вызывающей функции, а не локальной переменной внутри функции. Хотя это не то же самое, что возвращать значения из функции, это позволяет передавать значения обратно в вызывающую область видимости для достижения аналогичного эффекта.
Вывод:
9.4.2. Возвращение значений из макросов
Макросы могут «возвращать» определенные переменные так же, как и функции, указывая имена переменных, которые нужно установить, передавая их в качестве аргументов. Единственное отличие заключается в том, что ключевое слово PARENT_SCOPE не должно использоваться внутри макроса при вызове set(), поскольку макрос уже изменяет переменные в области видимости вызывающей стороны. Фактически, единственная причина, по которой можно использовать макрос вместо функции, - это необходимость установить много переменных в вызывающей области видимости. Макрос будет влиять на область видимости при каждом вызове set() или unset(), в то время как функция влияет на область видимости только в том случае, если явным образом задан PARENT_SCOPE для set() или unset().
Последний пример из предыдущего раздела можно эквивалентно реализовать в виде макроса:
Поведение return() внутри макроса сильно отличается от поведения функции. Поскольку макрос не вводит новую область видимости, поведение оператора return() зависит от места вызова макроса. Вспомните, что макрос эффективно вставляет свои команды в место вызова. Таким образом, любой оператор return() из макроса делает возврат из области видимости того, кто вызвал макрос, а не из самого макроса. Рассмотрим следующий пример:
Выходные данные будут такими:
Чтобы понять, почему второе сообщение в теле функции никогда не печатается, вставьте содержимое тела макроса в то место, где он вызывается:
Теперь стало гораздо понятнее, почему оператор return() приводит к выходу из функции, даже если изначально она была вызвана изнутри макроса. Это подчеркивает опасность использования return() внутри макросов. Поскольку макросы не создают собственную область видимости, результат выполнения оператора return() часто оказывается не таким, как ожидалось.
9.5. Переопределение команд
Когда функция или макрос вызывается для определения новой команды, если команда с таким именем уже существует, недокументированное поведение CMake заключается в том, чтобы сделать старую команду доступной с тем же именем, но с добавлением подчеркивания. Это касается как старого имени встроенной команды, так и пользовательской функции или макроса. Разработчики, знающие об этом поведении, иногда испытывают искушение воспользоваться им, чтобы попытаться создать обертку вокруг существующей команды, например, так:
Если команда переопределяется только один раз, она работает, но если она переопределяется снова, то исходная команда становится недоступной. Добавление одного подчеркивания для «сохранения» предыдущей команды применяется только к текущему имени, оно не применяется рекурсивно ко всем предыдущим переопределениям. Это может привести к бесконечной рекурсии, как показывает следующий надуманный пример:
Наивно было бы ожидать, что результат будет следующим:
Но вместо этого первая реализация никогда не вызывается, потому что вторая в итоге вызывает саму себя в бесконечном цикле. Когда CMake обрабатывает вышеописанное, происходит вот что:
- Создается первая реализация printme, которая становится доступной в виде команды с таким именем. Ранее команды с таким именем не существовало, поэтому дальнейшие действия не требуются.
- Встречается вторая реализация printme. CMake находит существующую команду с таким именем, поэтому он определяет имя _printme для указания на старую команду и устанавливает printme для указания на новое определение.
- Встречается третья реализация printme. И снова CMake находит существующую команду с таким именем, поэтому он переопределяет имя _printme, чтобы оно указывало на старую команду (которая является второй реализацией), и устанавливает printme, чтобы оно указывало на новое определение.
Когда вызывается printme(), выполнение переходит к третьей реализации, которая вызывает _printme(). Она переходит во вторую реализацию, которая также вызывает _printme(), но _printme() снова указывает на вторую реализацию, и получается бесконечная рекурсия. Выполнение так и не достигает первой реализации.
В целом, переопределять функцию или макрос можно до тех пор, пока он не пытается вызвать предыдущую реализацию, как в приведенном выше обсуждении. Проекты должны просто считать, что новая реализация заменяет старую, а старая считается больше недоступной.
9.6. Специальные переменные для функций
В CMake 3.17 добавлена поддержка ряда переменных для помощи в отладке и реализации функций. Во время выполнения функции будут доступны следующие переменные:
- CMAKE_CURRENT_FUNCTION Указывает имя выполняемой в данный момент функции.
- CMAKE_CURRENT_FUNCTION_LIST_FILE Содержит полный путь к файлу, в котором определена функция, выполняемая в данный момент.
- CMAKE_CURRENT_FUNCTION_LIST_DIR Содержит полный путь на каталог, содержащий файл, в котором определена функция, выполняемая в данный момент.
- CMAKE_CURRENT_FUNCTION_LIST_LINE Указывает номер строки, на которой текущая выполняемая функция была определена в файле, определившем ее.
Переменная CMAKE_CURRENT_FUNCTION_LIST_DIR особенно полезна, когда функция должна ссылаться на файл, являющийся внутренней реализацией функции. Значение CMAKE_CURRENT_LIST_DIR будет содержать каталог файла, в котором вызывается функция, а CMAKE_CURRENT_FUNCTION_LIST_DIR будет содержать каталог, в котором определена функция. Чтобы увидеть, как это можно использовать, рассмотрим следующий пример. Он демонстрирует распространенную схему, когда функция использует команду configure_file() для копирования файла из того же каталога, что и файл, определяющий функцию (более подробное обсуждение см. в разделе 21.2, «Копирование файлов»):
До CMake 3.17 вышеописанное обычно реализовывалось следующим образом (собственные модули CMake использовали эту технику до CMake 3.17):
Этот второй пример основан на том, что переменная __writeSomeFile_DIR остается видимой в момент вызова функции. Обычно это разумное предположение, но поскольку функции имеют глобальную область видимости, проекты могут технически определять функцию в одном месте и вызывать ее в несвязанной области видимости. Хотя это технически законно, но не рекомендуется. Также следует быть особенно осторожным, если в файлах, определяющих функции, используется команда include_guard() (см. раздел 8.4, «Раннее завершение обработки»).
Переменные CMAKE_CURRENT_FUNCTION... обновляются только для функций, внутри макросов они не изменяются. При выполнении кода макроса эти переменные будут иметь те значения, которые они имели на момент вызова макроса.
9.7. Другие способы вызова кода CMake
Функции и макросы - это мощные способы определения кода, который будет выполняться позже. Они являются важной частью повторного использования общей логики для схожих или повторяющихся задач. Тем не менее, бывают ситуации, когда проекты хотят определить код CMake для выполнения таким образом, что одними функциями и макросами не обойтись.
В CMake 3.18 добавлена команда cmake_language(), с помощью которой можно вызывать произвольный код CMake напрямую, без необходимости определять функцию или макрос. Эта функциональность не призвана заменить функции или макросы, а скорее дополнить их, обеспечив более лаконичный код и возможность выражать логику способами, которые ранее были невозможны. CMake 3.18 предоставляет две подкоманды: CALL и EVAL CODE:
Подкоманда CALL вызывает одну команду CMake с аргументами, если требуется. Она позволяет параметризировать вызываемую команду без необходимости жесткого ввода всех доступных вариантов. Некоторые встроенные команды не могут быть вызваны таким образом, в частности команды, которые начинают или заканчивают блок, такие как if(), endif(), foreach(), endforeach() и так далее.
Следующий пример демонстрирует, как можно определить общую обертку для набора функций, в имени которых указан номер версии:
В приведенном выше примере предполагается, что переменная QT_DEFAULT_MAJOR_VERSION была установлена ранее. По мере выпуска будущих основных версий Qt приведенный выше пример будет продолжать работать, пока будет предоставляться соответствующая команда с указанием версии. Альтернативой может быть реализация постоянно расширяющегося набора тестов if() для каждой версии в отдельности.
Подкоманда CALL довольно ограничена в своей полезности. Подкоманда EVAL CODE гораздо мощнее, так как она поддерживает выполнение любого корректного сценария CMake. Одно из ее преимуществ заключается в том, что она не вмешивается в переменные, которые обновляются внутри вызова функции, такие как ARGV, CMAKE_CURRENT_FUNCTION и так далее. В следующем примере это поведение используется для реализации трассировки вызовов функций:
Вывод:
Обратите внимание, как код, хранящийся в myProjTraceCall, использует различные переменные ARG*, а также переменную CMAKE_CURRENT_FUNCTION. Синтаксис скобок [=[ и ]=] используется для предотвращения извлечения этих переменных при установке myProjTraceCall. Переменные будут извлечены только при вызове cmake_language(), поэтому они будут отражать детали вложенной функции. По причине отложенного извлечения переменных код трассировки не будет работать так, как ожидается, внутри макроса, поэтому используйте его только внутри функции.
Еще один особенно интересный пример использования подкоманды EVAL CODE приведен в разделе 9.8.2, «Пересылка аргументов».
В CMake 3.19 добавлен набор подкоманд DEFER. Они позволяют поставить команду в очередь на выполнение в более позднее время и управлять набором команд, находящихся в очереди в данный момент. Вот форма создания отложенной команды:
① Извлечение переменных внутри команд и аргументов не соответствует обычному поведению большинства других команд CMake. Важные различия описаны в разделе 9.8.3, «Особые случаи расширения аргументов».
Команда command и ее аргументы args... будут поставлены в очередь на выполнение в конце текущей области видимости каталога. Опция DIRECTORY может быть задана для указания другой области видимости каталога. В этом случае каталог dir должен быть уже известен CMake и в нем не должно быть завершенной обработки. На практике это означает, что это должен быть либо текущий каталог, либо один из родительских каталогов.
Каждая команда в очереди имеет идентификатор, связанный с ней. Несколько команд могут быть связаны с одним и тем же идентификатором, чтобы ими можно было управлять как группой (см. далее). Обычно проект позволяет CMake автоматически назначать новый идентификатор при постановке в очередь новой отложенной команды. Опция ID_VAR может быть использована для захвата автоматически присвоенного идентификатора, который затем может быть повторно использован в последующих вызовах с опцией ID для добавления других команд к тому же идентификатору.
Другие подкоманды DEFER могут запрашивать и отменять отложенные команды на основе идентификаторов:
Форма GET_CALL_IDS возвращает список идентификаторов всех команд, находящихся в очереди для указанной области видимости каталога, или текущей области видимости каталога, если опция DIRECTORY не указана. Форма GET_CALL возвращает первую команду и ее аргументы, связанные с указанным идентификатором. Невозможно получить вторую или последующие команды для данного идентификатора, а также подсчет количества команд, связанных с идентификатором. Форма CANCEL_CALL отбрасывает все отложенные команды, связанные с любым из указанных идентификаторов.
На этом этапе вполне естественно начать думать о том, как можно использовать функциональность DEFER. Прежде чем это сделать, обратите внимание на следующие замечания:
- Особые правила применяются к извлечению переменных в отложенных командах и их аргументах (см. раздел 9.8.3, «Особые случаи для расширения аргументов»). Это может привести к тонким проблемам, которые трудно отследить.
- Отложенные команды затрудняют разработчикам отслеживание хода выполнения. Это особенно верно, когда отложенные команды создаются внутри функций или макросов, и их создание не является очевидным.
- Откладывая команды, проект может делать предположения о том, что может произойти между откладыванием и выполнением команд. Гарантировать, что эти предположения останутся верными, может быть довольно сложно, особенно для команд, отложенных в родительской области видимости, или когда отложенные команды создаются внутри функции или макроса, который может быть вызван откуда угодно.
- Отложенные команды могут быть признаком того, что CMake API проекта пытается сделать слишком много в одной функции или макросе.
Учитывая вышесказанное, если есть выбор, отдайте предпочтение другим техникам или рефакторингу кода, вместо использования отложенных команд. Например, функция может вызывать команду, создающую цель, затем вызвать другие команды, использующие свойства этой цели, прежде чем вернуть управление (свойства рассматриваются в следующей главе). Инкапсулируя все это в одну функцию, вызывающая сторона не имеет возможности изменить свойства цели до того, как они будут использованы. Вместо того чтобы откладывать выполнение команд, использующих цель, чтобы вызывающая сторона могла изменить свойства цели, подумайте о том, чтобы разделить функцию так чтобы на нее не возлагалось так много обязанностей. Одним из альтернативных решений для этого конкретного примера может быть требование передачи уже созданной цели через параметр вместо ее создания.
9.8. Проблемы с обработкой аргументов
Реализация аргументов команд в CMake имеет несколько тонких особенностей поведения. По большей части это поведение не приводит к проблемам, но иногда оно может вызвать путаницу или привести к неожиданным результатам. Чтобы понять, почему это происходит и как с этим безопасно работать, нужно разобраться в том, как CMake строит и передает аргументы командам.
Рассмотрим следующие эквивалентные вызовы, где someCommand может быть любой допустимой командой:
Аргументы разделяются пробелами, причем последовательные пробелы рассматриваются как один разделитель. Точки с запятой также служат разделителями аргументов, поэтому следующие выражения также эквивалентны приведенным выше:
Если аргумент должен содержать встроенные пробелы или точки с запятой, необходимо использовать кавычки:
Во всех трех приведенных выше вызовах команде передается три аргумента. Первый вызов передает b b в качестве второго аргумента, два других вызова передают b;b в качестве второго аргумента.
Пробел и точка с запятой отличаются тем, как они обрабатываются, когда происходит извлечение переменных и аргументы не заключены в кавычки:
При первом вызове someCommand() передается три аргумента, а при втором - четыре. Встроенный пробел в containsSpace не выступает в качестве разделителя аргументов, а вот встроенная точка с запятой в containsSemiColon выступает. Пробелы выступают в качестве разделителей аргументов только перед выполнением извлечения переменной. Взаимодействие этих двух различных моделей поведения может привести к неожиданным результатам:
Из всего вышесказанного следует сделать несколько важных примечаний:
- Пробелы никогда не отбрасываются и не служат разделителями аргументов, если они являются результатом извлечения переменных.
- Одна или несколько точек с запятой в начале или в конце аргумента без кавычек отбрасываются.
- Последовательные точки с запятой, не находящиеся в начале или в конце аргумента, не заключенного в кавычки, объединяются и действуют как один разделитель.
Большей части путаницы можно избежать, заключив аргументы в кавычки, если они содержат извлечения переменных. Это устраняет любую специальную интерпретацию встроенных пробелов или точек с запятой. Хотя в целом это и не вредно, но не всегда желательно. Как будет показано в следующем подразделе, есть ситуации, в которых аргументы должны быть без кавычек именно потому, что они полагаются на вышеописанное поведение.
9.8.1. Надежный разбор аргументов
Рассмотрим команду cmake_parse_arguments(), обсуждавшуюся ранее в разделе 9.3, «Ключевые слова в качестве аргументов». В оригинале эта команда обычно используется следующим образом:
Обратите внимание на кавычки вокруг извлечения переменных noValues, singleValues и multiValues. При оценке каждая из этих переменных выдает строку, содержащую точку с запятой. Например, ${singleValues} будет извлечена как FORMAT;ARCH. Кавычки нужны для того, чтобы точка с запятой не выступала в качестве разделителя аргументов. В итоге cmake_parse_arguments() в качестве первого аргумента примет ARG, второго - ENABLE_A;ENABLE_B, третьего - FORMAT;ARCH и четвертого - SOURCES;IMAGES.
В ${ARGV}, передаваемом в конце вызова, нет кавычек. Это сделано специально для того, чтобы воспользоваться тем фактом, что встроенные точки с запятой будут выступать в качестве разделителей аргументов. Команда cmake_parse_arguments() интерпретирует пятый и последующие аргументы, которые она получает, как аргументы для парсинга. Используя ${ARGV} без кавычек, cmake_parse_arguments() видит тот же набор аргументов, который был передан в func().
Проблема в том, что использование ${ARGV} не позволяет сохранить исходные аргументы в двух конкретных случаях. Рассмотрим следующие вызовы:
При первом вызове, внутри func(), оценка ${ARGV} будет a;;c. Однако, как ранее говорилось в примечании 3, две точки с запятой будут объединены, и cmake_parse_arguments() увидит только a и c в качестве аргументов для разбора. Пустые аргументы отбрасываются. Для второго вызова оценка ${ARGV} будет a;b;c;1;2;3. В исходном вызове func() первым аргументом был a;b;c, а вторым - 1;2;3, но они сглаживаются при разрешении переменной ${ARGV}, и команда cmake_parse_arguments() видит шесть отдельных аргументов, а не два списка. Обе эти проблемы можно решить, используя другую форму команды cmake_parse_arguments(), чтобы не оценивать ${ARGV} напрямую:
На практике команда cmake_parse_arguments() часто используется в ситуациях, когда отбрасывание пустых аргументов или сглаживание списков не имеет реального значения. В этих случаях можно смело вызывать любую форму команды cmake_parse_arguments(). Если же необходимо сохранить аргументы в том виде, в котором они были переданы, всегда следует использовать форму PARSE_ARGV.
9.8.2. Пересылка аргументов
Относительно часто требуется создать некую обертку для существующей команды. Разработчик может захотеть поддержать некоторые дополнительные опции или удалить существующие, или выполнить определенную обработку до или после вызова. Сохранить аргументы и переслать их дальше, не изменив их структуру и не потеряв информацию, может быть удивительно сложно.
Рассмотрим следующий пример и его вывод, который развивает один из пунктов предыдущего подраздела:
Вывод:
Аргументы printArgs() заключены в кавычки, поэтому функция действительно видит только два аргумента. Однако при формировании значения для ${ARGN} эти два списка объединяются точкой с запятой, и в результате получается один список из шести элементов. В результате такого сглаживания списков первоначальная форма аргументов теряется. Рассмотрим последствия этого для команды-обертки, когда функция пытается передать аргументы далее:
Вывод:
Функция outer() пытается передать аргументы в точности в функцию inner(), но, как видно из приведенного выше вывода, количество аргументов, которые видит inner(), отличается. Оценка ${ARGN} как способа передачи аргументов в inner() вызывает поведение сглаживания списка, описанное ранее. Первоначальная структура аргументов теряется. Форма PARSE_ARGV команды cmake_parse_arguments() может быть использована для того, чтобы избежать этого:
При отсутствии ключевых слов для разбора все аргументы, переданные функции outer(), будут помещены в FWD_UNPARSED_ARGUMENTS. Как уже отмечалось в разделе 9.3, «Ключевые слова в качестве аргументов», когда форма PARSE_ARGV функции cmake_parse_arguments() заполняет FWD_UNPARSED_ARGUMENTS, она экранирует все встроенные точки с запятой из исходных аргументов. Поэтому, когда эта переменная передается в inner(), экранирование сохраняет структуру исходных аргументов, и inner() будет видеть те же аргументы, что и outer().
К сожалению, у приведенной выше техники есть недостаток. Как следствие примечаний 2 и 3 из раздела 9.8 «Проблемы с обработкой аргументов», она не сохраняет пустые аргументы. Чтобы избежать потери пустых аргументов, каждый аргумент должен быть перечислен отдельно и заключен в кавычки. Команда cmake_language(EVAL CODE), доступная в CMake 3.18 или более поздней версии, обеспечивает необходимую функциональность:
Обратите внимание на использование скобочной формы для цитирования. Это гарантирует, что любые аргументы со встроенными кавычками также будут обработаны надежно.
Приведенная выше реализация обеспечивает надежную переадресацию аргументов, но требует минимальной версии CMake 3.18 или выше. Для более ранних версий команда cmake_language() недоступна. Эквивалентную возможность можно реализовать, записав выполняемую команду в файл и попросив CMake обработать этот файл через вызов include(), но это очень неэффективно и не рекомендуется в качестве общего решения.
Приведенная выше техника работает только для функций. Форма PARSE_ARGV команды cmake_parse_arguments() не может быть использована с макросами, а значит, сглаживания списка не избежать. Однако, если сглаживание списка не является проблемой, можно, по крайней мере, сохранить пустые строки. Следующая реализация демонстрирует один из способов достижения этой цели в предположении, что ни одно значение никогда не должно содержать точку с запятой:
См. раздел 41.2.6, «Делегирование провайдеров», где приведен пример сценария, в котором может понадобиться вышеописанная техника.
9.8.3. Особые случаи для расширения аргументов
Хотя описанные выше приемы в целом работают хорошо, некоторые встроенные команды обрабатывают свои аргументы особым образом, что заставляет их отклоняться от ожидаемого поведения. Эти исключения делятся на две основные категории: cmake_language() и оценка булевых выражений.
9.8.3.1 Оценка переменных для cmake_language()
cmake_language(CALL) предоставляет альтернативный способ выполнения команды. Команда для вызова может быть задана переменной вместо того, чтобы быть жестко закодированной. Для того чтобы точно воспроизвести аргументы и обработку кавычек для аргументов, передаваемых во вложенную команду, эти аргументы должны быть отделены от самой команды в вызове cmake_language(CALL). Это демонстрируется в следующем примере:
Команда cmake_language(DEFER CALL) имеет аналогичные ограничения, но у нее есть и другие отличия. Извлечение переменных выполняется немедленно для команды, которую нужно выполнить, но для аргументов команды извлечение откладывается. В следующем примере показано такое поведение:
Вывод:
Извлечение ${cmd} происходит немедленно, но ${args} не извлекается до тех пор, пока не будет вызвана отложенная команда. В конце области видимости каталога значение args будет соответствовать значению after deferral.
Если извлечение переменных в аргументах команды должно быть выполнено немедленно, необходимо обернуть cmake_language(DEFER CALL) внутри вызова cmake_language(EVAL CODE). Примером такого сценария может служить ситуация, когда отложенная команда создается внутри функции или макроса, а аргументы отложенной команды должны быть вычислены из аргументов этой функции или макроса:
Обратите внимание, что вокруг извлечения ${msg} используется синтаксис кавычек со скобками, чтобы обеспечить правильную обработку пробелов.
9.8.3.2 Булевы выражения
Команды, которые рассматривают свои аргументы как булевы выражения, также имеют некоторые специальные правила, связанные с кавычками и расширением аргументов. Лучше всего это демонстрирует команда if(), но эти правила применимы и к while(). Рассмотрим следующий пример, который демонстрирует тонкое поведение того, как аргументы без кавычек могут рассматриваться либо как имена переменных, либо как строковые значения (более подробное обсуждение этого поведения см. в разделе 7.1.1, «Выражения»):
Пытаясь воспроизвести описанное выше, используя переменные для аргументов команд if(), можно попытаться сделать что-то вроде следующего:
Значение withQuotes использует синтаксис скобок, чтобы сделать кавычки частью хранимого значения. Идея заключается в том, чтобы попытаться заставить команду if() рассматривать xxxx как аргумент, заключенный в кавычки, но этого не происходит. Команда if() проверяет наличие кавычек перед извлечением переменной, поэтому в данном случае кавычки воспринимаются как часть значения. При использовании механизма извлечения значения переменной в команде if(), не существует способа заставить полученный аргумент восприниматься в кавычках, как в приведенном выше примере.
Еще один особый случай, о котором следует знать, - это то, как квадратные скобки влияют на интерпретацию точек с запятой для списков, как обсуждалось ранее в разделе 6.9.1, «Проблемы с несбалансированными квадратными скобками». Точки с запятой между несбалансированными квадратными скобками не интерпретируются как разделители списков при извлечении переменной. Однако это не распространяется на то, как CMake собирает аргументы команд, как показано в следующем примере и его выводе:
Вывод:
Команда func() действительно видит пять исходных аргументов, что демонстрирует первый цикл foreach(). При оценке переменной ARGV второй командой foreach() встроенные несбалансированные квадратные скобки мешают интерпретации переменной как списка.
На практике сравнительно редко встречаются ситуации, когда несбалансированные квадратные скобки могут быть оправданы. Иногда встречаются сбалансированные квадратные скобки, но, как показывает аргумент c[c]c в приведенном выше примере, они не мешают интерпретации списка.
9.9. Рекомендуемые практики
Функции и макросы - отличный способ повторно использовать один и тот же фрагмент кода CMake в проекте. В целом, лучше использовать функции, а не макросы, так как использование новой области видимости переменных в функции лучше изолирует эффекты функции от области видимости вызывающей функции. Макросы следует использовать только в тех случаях, когда содержимое тела макроса действительно должно быть выполнено в области видимости вызывающей функции. Такие ситуации должны быть относительно редкими. Чтобы избежать неожиданного поведения, также не вызывайте return() внутри макроса.
Предпочтите передавать все значения, необходимые функции или макросу, в качестве аргументов, а не полагаться на то, что переменные будут установлены в вызывающей области видимости. Это, как правило, делает реализацию более устойчивой к будущим изменениям, а также делает ее более понятной и простой в сопровождении.
Для всех функций и макросов, кроме самых тривиальных, настоятельно рекомендуется использовать обработку аргументов на основе ключевых слов, предоставляемую cmake_parse_arguments(). Это повышает удобство использования и надежность вызывающего кода (например, уменьшает вероятность перепутать аргументы). Кроме того, это позволяет легче расширять функцию в будущем, так как нет зависимости от порядка следования аргументов или необходимости всегда предоставлять все аргументы, даже если они не важны.
Остерегайтесь опускания пустых аргументов и сглаживания списков при разборе или передаче аргументов команд. Если минимальная версия CMake проекта позволяет, предпочитайте использовать внутри функций форму PARSE_ARGV функции cmake_parse_arguments(). При передаче аргументов используйте cmake_language(EVAL CODE) для цитирования каждого аргумента по отдельности, если требуется сохранить пустые аргументы и списки.
Предпочтительно избегать откладывания команд с помощью cmake_language(DEFER), если есть другие альтернативы. Отложенные команды вносят хрупкость, затрудняют отладку проекта и могут быть признаком того, что функции и макросы CMake нуждаются в рефакторинге.
Вместо того чтобы распределять функции и макросы по всем каталогам с исходными текстами, принято назначать определенный каталог (обычно чуть ниже верхнего уровня проекта), в котором могут быть собраны различные файлы XXX.cmake. Эта директория действует как набор необходимого функционала, к которому можно обращаться из любой точки проекта. Каждый из файлов может предоставлять функции, макросы, переменные и другие возможности. Использование суффикса в имени файла .cmake позволяет команде include() находить файлы как модули, что подробно рассматривается в главе 12, «Модули». Он также позволяет инструментам IDE распознавать тип файла и применять подсветку синтаксиса CMake.
Не определяйте и не вызывайте функцию или макрос с именем, начинающимся с одного подчеркивания. В частности, не полагайтесь на недокументированное поведение, при котором старая реализация команды становится доступной по такому имени, когда функция или макрос переопределяет существующую команду. Если команда была переопределена более одного раза, ее первоначальная реализация становится недоступной. Это недокументированное поведение может быть даже удалено в будущей версии CMake, поэтому его не следует использовать. Аналогично, не переопределяйте ни одну встроенную команду CMake. Считайте их запрещенными к переопределению, чтобы всегда можно было предположить, что встроенные команды ведут себя в соответствии с официальной документацией, и не было возможности сделать оригинальную команду недоступной.
Если минимальная версия CMake в проекте установлена на 3.17 или более позднюю, предпочтите использовать CMAKE_CURRENT_FUNCTION_LIST_DIR для ссылки на любой файл или каталог, который должен существовать в месте относительно файла, содержащего определение функции.
Это был ознакомительный фрагмент книги Professional CMake: A Practical Guide by Craig Scott