如何从用户输入修改C++代码

19

我目前正在编写一个程序,它位于C++解释器之上。用户在运行时输入C++命令,然后将其传递给解释器。对于某些模式,我希望将给定的命令替换为修改后的形式,以便提供额外的功能。

我想要替换任何形如:

A->Draw(B1, B2)
MyFunc(A, B1, B2).

我的第一反应是使用正则表达式,但这样做会很容易出错,因为AB1或者B2中的任何一个都可以是任意的C++表达式。由于这些表达式本身可能包含带引号的字符串或括号,因此使用正则表达式来匹配所有情况会非常困难。此外,这个表达式可能存在多个嵌套形式。

我的下一个想法是调用clang作为子进程,使用"-dump-ast"来获取抽象语法树,修改它,然后重新构建成一个命令传递给C++解释器。但是,这将需要跟踪任何环境变化,例如包含文件和前向声明,以便为clang提供足够的信息来解析表达式。由于解释器不公开这些信息,这似乎也不可行。

第三个想法是使用C++解释器自己的内部解析来转换成抽象语法树,然后从那里构建。然而,我没有找到这个解释器以任何方式公开抽象语法树。

是否有任何建议如何继续,无论是沿着其中一个路线还是完全沿着不同的路线?


7
为了确保您不是在做一个X-Y问题,能否告诉我们您试图通过进行替换来实现什么目标? - Mark B
两个问题:命令会分别显示在单独的行中吗?能否在同一行上看到对 Draw 的重复调用 (A->Draw(...)->...->Draw(...))? - Alexander Feterman
一行中可能有多个命令,例如 A->Draw(B); C->Draw(D) 或者 func(A->Draw(B), C->Draw(D))。由于 Draw 返回一个整数值,所以不会发生重复调用所示模式的情况。 - Eldritch Cheese
在评估 A->Draw(B1, B2) 的过程中,可以进行宏扩展吗? - serge-sans-paille
原则上,宏展开可能会发生,并且会在控制离开我的函数后发生。实际上,在这里我没有看到任何使用宏的情况,因此我可以安全地忽略宏展开。 - Eldritch Cheese
显示剩余2条评论
6个回答

3
您需要的是一个程序转换系统。这些工具通常允许您表达对源代码的更改,使用源级模式编写,本质上是说:
 if you see *this*, replace it by *that*

但是,操作抽象语法树使匹配和替换过程比字符串处理更可靠。

这些工具必须具有所需源语言的解析器。源语言为C ++,这使得这个过程相当困难。

Clang有点符合要求;毕竟它可以解析C ++。OP对象无法在没有所有环境上下文的情况下执行此操作。就OP输入(格式正确)程序片段(语句等)到解释器中而言,Clang可能会[I自己没有太多经验]难以专注于片段是什么(语句?表达式?声明?...)。最后,Clang并不真正是PTS;其树修改程序不是源到源转换。对于方便性很重要,但可能不会阻止OP使用它;表面语法重写规则很方便,但您始终可以用更多的努力替换过程树操作。当有多个规则时,这开始变得非常重要。

GCC with Melt在与Clang相同的方式下有点符合要求。我认为Melt使GCC在这种工作中最好也只是稍微好一点。YMMV。

我们的DMS软件重构工具包与其完整的C++14 [编辑于2018年7月:C++17]前端绝对符合要求。DMS已被用于对大规模C++代码库进行大规模转换。
DMS可以在不预先告知语法类别的情况下解析任意(良好形式的)C++片段,并使用其模式解析机制返回正确语法非终端类型的AST。[您可能会得到多个解析结果,如歧义,需要决定如何解决,有关更多讨论,请参见为什么不能用LR(1)解析器解析C++?]如果您愿意在解析时放弃宏展开,并坚持预处理指令(它们也会被解析)与代码片段有良好的结构关系(#if foo{#endif不允许),则可以在不使用“环境”的情况下执行此操作,但这对于交互输入的代码片段不太可能是真正的问题。
然后,DMS提供完整的过程化AST库来操作已解析的树(搜索、检查、修改、构建、替换),然后可以从修改后的树重新生成表面源代码,给出OP文本以馈送给解释器。
在这种情况下,它的优点在于OP可以直接将大部分修改作为源到源语法规则编写。对于他的示例,他可以向DMS提供一个重写规则(未经测试但非常接近正确):
rule replace_Draw(A:primary,B1:expression,B2:expression):
        primary->primary
    "\A->Draw(\B1, \B2)"     -- pattern
rewrites to
    "MyFunc(\A, \B1, \B2)";  -- replacement

DMS将接受任何包含左侧“...Draw...”模式的解析AST,并在替换A、B1和B2匹配项后,用右侧替换该子树。引号是元引号,用于区分C++文本和规则语法文本;反斜杠是元转义符,在元引号内用于命名元变量。有关规则语法中可用内容的更多详细信息,请参见DMS Rewrite Rules
如果OP提供了这样一组规则,就可以要求DMS应用整个规则集。
所以我认为这对于OP来说完全可以胜任。这是一个相当重量级的机制,用于“添加”到他想提供给第三方的软件包中;DMS及其C++前端几乎不是“小”程序。但现代计算机具有大量资源,因此我认为问题在于OP需要多么严重地做到这一点。

0

可能有一种方法可以主要使用正则表达式来完成这个任务。

由于在 Draw( 后出现的任何内容都已经正确格式化为参数,所以你不需要完全解析它们来实现你所概述的目标。

从根本上讲,重要的部分是 "SYMBOL->Draw("

SYMBOL 可以是任何解析为重载了 -> 的对象或实现了 Draw(...) 的指针类型的表达式。如果将其简化为两种情况,你可以快速处理解析。

对于第一种情况,一个简单的正则表达式可以搜索任何有效的 C++ 符号,类似于 "[A-Za-z_][A-Za-z0-9_\.]",再加上文字表达式 "->Draw("。这将给你需要重写的部分,因为该部分后面的代码已经格式化为有效的 C++ 参数。

第二种情况是针对返回重载对象或指针的复杂表达式。这需要更多的工作,但是可以惊人地轻松编写一个短小的解析例程来向后遍历一个复杂表达式,因为您不必支持块(C ++中的块不能返回对象,因为lambda定义不会调用lambda本身,并且实际嵌套的代码块 {...} 无法直接内联返回任何东西,该模式也适用于它们)。请注意,如果表达式不以)结尾,则必须是此上下文中的有效符号,因此如果您找到),只需将嵌套)与(匹配并提取前面的符号,然后再提取相邻的SYMBOL(...(...)...)->Draw()模式。这可能可以使用正则表达式完成,但在普通代码中也应该非常容易。
一旦您拥有符号或表达式,替换就很容易了,从
SYMBOL->Draw(...
转变成
YourFunction(SYMBOL, ...
而无需处理Draw()的其他参数。
作为额外的好处,使用此模型免费解析链接的函数调用,因为可以递归迭代代码,例如
A->Draw(B...)->Draw(C...)

第一次迭代识别第一个A->Draw(并将整个语句重写为:
YourFunction(A, B...)->Draw(C...)

然后它识别第二个 ->Draw,使用表达式“YourFunction(A, ...)->”在其前面,并将其重写为

YourFunction(YourFunction(A, B...), C...)

其中B...和C...是良好形式的C++参数,包括嵌套调用。

如果不知道您的解释器支持的C++版本或您将要重写的代码类型,我真的无法提供任何有价值的示例代码。


在我看来,你在争论正则表达式可以解析任意的C++代码。事实上,正则表达式并不能解析所有的上下文无关语言,更不用说C++了。你或许可以将自定义编码与正则表达式结合使用,但通常这种方法会导致疯狂的结果。我认为你的方法非常脆弱。你是否曾经成功地在某个地方实现过这个想法? - Ira Baxter
@Ira Baxter:不一定。我说过:“这可能可以通过正则表达式实现,但在普通代码中也应该很容易。”解析代码的目标是编译它和解析代码的目标是确定表达式的起始/结束位置之间存在区别。至于我是否已经实现了它,我为自己编写了一个C++服务器页面编译器,使用不到两百行的C++(转换为GCC或Visual Studio可以编译的标准C++源代码),采用了这种方法,但没有使用正则表达式。它可能会脆弱,这取决于情况。 - Matt Jordan
要处理C++表达式,你必须挑选出C++词元,对吧?你不可能合理地提出你可以用200行代码编写一个C++词法分析器,更别说挑选子表达式了,所以我不明白你是如何准确地完成这个任务的。其次,当解释为声明和表达式时,有些东西的解析方式是不同的;你的方案如何处理这个问题呢?最后,要替换表达式,你必须在语句的所有层级中找到它们;由于嵌套的存在,这就是解析。我发现你暗示你的C++服务器页面可以处理任意的C++代码,这一点很难让人相信。 - Ira Baxter
编写一个能够为完整编译器生成解析树的C++解析器将非常困难;然而,编写一个能够区分和提取代码块中的C++代码的解析器实际上非常容易。不需要构建完整的解析树,像递归遍历代码这样的操作相当简单,因为C++对代码的内容有非常严格的限制(例如,在C++中是否可以存在一个 [ 而没有一个 ]?除了转义的 " 之外,字符串中的其他内容会影响跳过吗?)。原帖并不需要重写声明,只需要处理表达式,所以忽略声明就足够作为反例。 - Matt Jordan
我的团队有编写完整的C++前端的经验;是的,这很难,然后你需要弄清楚子表达式的类型。 OP似乎正在将任意的C++代码元素输入到他的解释器中。 他输入的表达式具有类型。 即使OP输入的是微不足道的表达式,他的“替换”也必须与类型兼容。 因此,您不仅需要解析,还需要根据C++规则解析名称。 为什么您坚持认为这很容易超出了我的理解。 - Ira Baxter

0
当某人获取Draw成员函数(auto draw = &A::Draw;)并开始使用draw时会发生什么?显然,您希望在这种情况下调用相同的改进的Draw功能。因此,我认为我们可以得出结论,您真正想要的是用自己的函数替换Draw成员函数。
由于似乎您无法直接修改包含Draw的类,因此解决方案可能是从A派生自己的类并在其中覆盖Draw。然后,您的问题就简化为让用户使用您的新改进的类。
您可能再次考虑自动将类A的用法转换为您的新派生类的问题,但是如果没有完整的C++实现的帮助,这似乎仍然非常困难。也许有一种方法可以通过聪明地使用头文件来隐藏A的旧定义,并代之以您的替换,但是根据您告诉我们的内容,我无法确定是否是这种情况。
另一个可能性是使用LD_PRELOAD进行一些动态链接器黑客技巧,以在运行时替换被调用的Draw函数。

0
如果用户想要在应用程序中输入复杂算法,我的建议是将脚本语言集成到应用程序中。这样用户就可以编写代码[以定义的方式编写函数/算法],然后应用程序可以在解释器中执行它并获取最终结果。例如:Python、Perl、JS等。
由于您需要在解释器中使用C++,http://chaiscript.com/可能是一个不错的选择。

目前已经有一种脚本语言,C++。虽然C++通常不用作脚本语言,但我使用的库包括一个C++解释器。我可以在将命令传递到解释器之前修改它们,但我无法修改库代码本身,因为其他用户将针对未修改的库版本进行编译。 - Eldritch Cheese

0
尝试修改标头以抑制该方法,然后编译,您将找到错误并能够替换所有核心。
只要您拥有C ++解释器(如CERN的Root),我猜您必须使用编译器来拦截所有绘图。一种简单而干净的方法是在标头中声明绘图方法为私有,并使用某些定义。
 class ItemWithDrawMehtod
 {
 ....
 public:
 #ifdef CATCHTHEMETHOD
     private:
 #endif
 void Draw(A,B);
 #ifdef CATCHTHEMETHOD
     public:
 #endif
 ....
 };

然后编译为:

 gcc -DCATCHTHEMETHOD=1 yourfilein.cpp

很抱歉,我不确定这样做会有什么好处。因为我的程序将分发给其他用户,他们将编译未经修改的库版本,修改库中的调用以调用“MyFunc”而不是“Draw”并不能解决问题。此外,用户习惯于将“Draw”命令传递到解释器中,而我想要修改的就是这些命令。 - Eldritch Cheese
这个解决方案没有实际的好处,不适用于生产环境,只适用于开发。关键是制造一个强制性错误,让编译器为你完成工作。在源代码中,这是“你的责任”,你可以使用这个技巧来捕获所有错误并为其他程序员提供你的函数。但是他们有责任使用新的函数。 - Kiko Albiol Colomer

-1
一种方法是将用户代码作为DLL加载(类似于插件), 这样,您不需要编译实际的应用程序,只需编译用户代码,应用程序将动态加载它。

虽然这是在应用程序中执行用户代码的一种方式,但这不是本例所期望的(用户在运行时输入“实时”代码)。 - crashmstr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接