允许CMake函数返回(值)

2020-08-19 02:51:01

这是一个实现CMake功能的故事,我称之为命令引用(类似于现有的变量引用),即使用命令调用的结果作为参数。我有这个想法很长一段时间了,从来没有足够的时间去深入研究它。现在,由于失业,我决定在找下一份工作之前至少试一试。虽然没有我想象的那么容易,但我对结果相当满意。

第二部分解释了一些实现细节,比如为什么需要新的词法分析器和解析器。

在我职业生涯的大部分时间里,我使用Visual Studio,当我切换到Linux时,我有点震惊。与MSVS相比,Make文件给人的感觉就像是弓箭对准机关枪。然后我发现了CMake,它感觉好多了,我们得到了一种独特的语言,带有命令和变量,而不是晦涩难懂的makefile。因为它只是另一种语言,所以同样的规则也适用于它的代码:有意义的名称、小函数、抽象分离等等。不幸的是,许多CMake文件看起来像是一个非常大的函数,它混合了其中的所有内容。CMake允许我们正确处理几乎所有的事情,除了一件事-它没有返回值,因此限制了函数抽象的有用性。因此,您的CMakeList的某些部分看起来很糟糕。

IF(${CMAKE_CURRENT_LIST_DIR}STREQUAL${CMAKE_SOURCE_DIR})#是顶级列表?IF(Win32)#与其他CMake varsif(CMAKE_CXX_COMPILER_ID STREQUAL";GNU";)IF(CMAKE_CXX_COMPILER_ID STREQUAL";CLANG";)OR(CMAKE_CXX_COMPILER_ID STREQUAL";CLANG";)OR。

这类代码的问题在于它不表达逻辑,只表达实现细节。它要求您记住所有那些冗长而棘手的变量名称、魔术值以及它们之间的关系。

我们可以把它移到一个函数中,但这并不能解决问题。我们很懒,没有人愿意写两行而不是一行:

这比直接的价值操纵要好,但是很难看。现在,您需要查看文档以了解此输出参数的名称,直观的候选项是RESULT、RESULT_VAR、OUTPUT,或者根本没有这样的参数:check_feature_available(is_feature_available)(or CHECK_FEATURE_Available()WITH FATAL_ERROR)。它还迫使您考虑变量的名称,其中许多变量只使用一次。我们可以用任何流行的语言清楚地写出以上所有内容:

再说一次,您可以处理所有这些事情,CMake已经成功使用多年了。但是为什么不让它变得更简单呢?CMake是C++的重要组成部分,那么为什么我们不能像处理C++本身那样让新手更容易呢?

最初的想法是允许类似get_name_by_id(get_id())的内容,但很快发现它是错误的,原因有两个:

CMake语法太简单,它没有关键字,命令名不受限制,任何东西都是字符串(包括括号)。例如表达式if(x and(Y OR Z))表示调用if_impl(";x";,";和";,";(";,";y";,";OR";,";z";,";)"。其中,AND、OR和PARENS只是由if_impl以特定方式处理的普通字符串。这里唯一的要求是花括号应该匹配,例如,如果(x和(Y)是不允许的。正因为如此,这是模棱两可的:

函数(和a b c)endfunction()if(x and(Y Or Z))#if_impl(x,and,(,y,OR,z,))or#if_impl(x,and_impl(y,OR,z))?

您可以将这种情况扩展到命名参数,与布尔操作不同,命名参数可以具有非平凡的名称。

但最主要的原因是上述形式不够灵活。如何在带引号的参数中使用它,或者将其与普通字符串混合使用?

语法模拟变量引用:${COMMAND_NAME(args...。)}(请注意,在命令名之前和最后的Paren之后没有空格)。它的工作方式与您的预期不谋而合:

它可以在任何可以使用变量引用的地方使用。也可以使用注释、嵌套调用和列表,让我们将它们混合在一起:

函数(FORMAT_NAME FIRST_LAST)RETURN(";FIRST:${FIRST},LAST:${last}";)endfunction()function(get_first_name)RETURN(";约翰";)#Return QuoteDendFunction()函数(GET_LAST_NAME)RETURN(DoE)#Return unquotedendfunction()function(get_first_and_last)Return([[John]]Doe)#Return listendFunction()message(${FORMAT_NAME(#PASS FORMAT_NAME${GET_FIRST_NAME()}#COMMENTS${GET_LAST_NAME()}#[[内部命令参考]])})})#FIRST:John,LAST:Doemessage(${FORMAT_NAME(#PASS AS A LIST,在两个参数${GET_FIRST_AND_LAST()}}中展开)#First:John,Last:Doe#return()成为返回其参数消息(${CMAKE_${return(";版本";)}})#3.18.1-...

当前实施基于CMake 3.18.1版本。您可以从源代码构建它,也可以使用预构建的二进制文件:

某些CMake功能或策略可能无法工作,特别是与语法或变量扩展相关的功能或策略。我知道的一个这样的政策是CMP0053的旧部分。与语法相关的错误消息也略有不同。所有其他事情应该可以工作,我已经成功地建立了谷歌测试,谷歌基准和FMT,使用它。我不是CMake开发人员,集成在某些地方相当肮脏,所以不要指望它马上就能投入生产。

一开始,我天真地认为,如果CMake已经可以解析单个命令调用,那么只需递归地对每个参数调用该函数就足够了:)但事实证明,这是一种更加复杂的方法,需要全新的词法分析器和解析器。我已经使用Flex&;Bison创建了它,您可以在这里找到执行解析和伪求值的独立项目。

当前的实现相对简单(但不是其代码)。它由基于Flex的扫描器和手写解析器组成,扫描器检测分开的参数及其类型,因为我们知道每个参数是如何开始和结束的,这很容易。当前的解析器主要验证有效分隔符、参数匹配等基本语法规则。例如,命令(a";${b}";)被解析为call(";command";).with_args(unquoted_arg{";a";},QUOTED_ARG{";${b}";})。请注意,变量引用${b}是作为纯文本传递的。在命令执行期间,每个参数都会被另一个解析器再次解析,该解析器可以检测、验证和评估变量引用。如果这样的命令出现在一个循环中,它会在每次迭代中执行额外的解析。此外,如果您在引用内部出错,则在计算表达式之前不会检测到它:

If(${ALWAYS_TRUE_IN_YOUR_ENV})#您的计算机消息上没有错误或警告(";hello world";)ELSE()#另一台计算机消息(${@:-:@})在运行时出现语法错误(${@:-:@})endif()。

现在,当我们允许另一个命令出现在参数内时,参数分隔就不那么容易了:

您可以看到高亮笔用黑色标记“a”,因为它认为参数是";result:${get_result(";,a,";b)}";。现有的CMake解析器以同样的方式看待它。要正确地分隔参数,我们必须能够在遇到命令引用时递归地进行解析。

只有不同之处在于,该命令引用可能出现在参数内部,而不仅仅是作为一个单独的参数。

正如您所看到的,现在我们需要比现有解析器更深入地解析它,尝试扩展它是没有意义的,而且手动编写递归规则的解析器也不是一件容易的事情,所以我别无选择,只能从头开始编写扫描器和解析器。之所以选择Flex和Bison,是因为它们已经用于CMake。

COMMAND_INVOCATION::=标识符空间*';(';参数';)';引号参数::==';";';(引号元素|引用)*';";';UNQUOTED_ARGUMENT::=(UNQUOTED_ELEMENT|REFERENCE)+REFERENCE::=var_reference|command_reference evar_reference::=var_ref_open(Variable_name|reference)*ref_closecommand_reference::=cmd_ref_open command_invocation ref_closevar_ref_open::==";${";|";$ENV{";|";}";QUOTED_ELEMENT::=<;检查官方文档>;unQUOTED_ELEMENT::=<;检查官方文档>;Variable_Name::=<;检查官方文档>;

与现有实现不同的是,我希望避免在执行期间进行解析,并在一次传递中获取所有详细信息。现在,每个带引号/不带引号的参数都由字符串(QUOTED/UNQUOTED_ELEMENT+)和引用组成。为了在运行时获得它的真正价值,我们需要评估和连接它的所有部分。例如,a_${b}_c有3个元素:string(";a_";)、var_ref(";b";)、string(";_c";)。在运行时,我们获得b的值并将它们连接在一起:a_B_value_c。

第一种方法是使用经典的解释器模式,并将表达式组合到树中。因为每个表达式都是类似列表的,所以我们可以将它们全部表示为一个std::vector<;std::unique_ptr<;IExpression>;>;.。它可以工作,但即使是简单的命令也变得非常复杂,命令(a,b)大致用。

Vector{//参数向量";命令&34;,//命令名称向量{//每个参数本身都是一个向量";a";},向量{";b";}}。

向量{";命令";,向量{";a";},向量{向量{//Reference也是向量";b";},";_c";}}。

反向波兰语(或后缀)表示法是参数位于运算符之前的表示法。当您需要表示没有分支的“线性”表达式时,它会大放异彩,而且它不需要花括号来表示优先级:

正常(中缀)表示法:a+bRPN:a,b+正常:(a+b)*cRPN:a,b+c*

Vector<;IExpression>;{StringExpr{";command";},//命令名称StringExpr{";a";},UnquotedArg{1},//1表示VarRefExpr{";b";},VarRefExpr{1},UnquotedArg{1},//与VarRefExpr{";b";},VarRefExpr{1},//相同的子表达式数。

无论表达式有多复杂,使用AST方法时只有一个向量,而不是四个向量,Win:)。

由于发现符号的顺序,它也非常适合像Bison这样的自下而上解析器。在上面的示例中,Bison将精确地按照符号在该向量中的顺序发现符号,您可以在不了解以前的符号或其他上下文的情况下直接推送表达式。

RPN使用堆栈进行评估。每个表达式都知道它的数量(参数数量),它将它们从堆栈中弹出并返回结果。但这里有个小问题。CMake将列表字符串展开为多个参数:

这意味着如果我们的CallExpr的arity=1,那么在运行时它可能变成包括零在内的任何数字。经典的RPN评估在这里不起作用。要克服这一点,我们需要调整数量的定义:现在,数量是指其结果应该作为参数的表达式的数量。我们需要额外的堆栈来跟踪这个结果计数。考虑以上示例的RPN表示:

MY_LIST扩展为3个参数,CallExprality为2,因此实际的数量是Results_Count堆栈中最后两个元素的和,这将是其参数1+3=4的最终数量。

使用Bison编写语法规则可以更容易地更改、理解、检查和支持手工编写的解析器。

野牛使符号位置跟踪几乎是自动的。使用简单的操作,您只需要手动跟踪线条。

F(${@})#1.5:语法错误,意外无效令牌,需要命令名或#引用开始或引用结束或变量名。

CMake支持BOM头,但只允许UTF-8。我们可以使用解析器中的另一个规则轻松地处理它,而不是手动读取。

在文件读取过程中,CMake通过替换Flex的输入例程将所有\r\n转换为\n。坦率地说,我不能完全理解该代码。假设它只是用\r\n和memcpy()替换\r\n,我想要更好的东西。在许多地方,我们只能在扫描仪规则中使用\r?\n regexp结尾。从理论上讲,字符串可能包含\r\r\n,这应该变成\r\n(我说的是原始字节0x0D 0x0A,而不是转义)。为了处理这个问题,当在扫描程序中的字符串中遇到\n时,我删除了尾随\r(如果有的话)。因为编写规则是逐行接受输入的,所以不涉及太多开销。这些简单的解决方案允许消除自定义读取例程和大量的memcpy()调用。

当然,这不是CMake的官方功能。如果你喜欢它,让我或CMake开发人员知道,以增加在未来的CMake版本中使用它的机会。