【问题标题】:How to modify C++ code from user-input如何从用户输入修改 C++ 代码
【发布时间】:2015-10-27 03:39:09
【问题描述】:

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

我想替换任何形式的东西

A->Draw(B1, B2)

MyFunc(A, B1, B2).

我的第一个想法是正则表达式,但这很容易出错,因为 AB1B2 中的任何一个都可能是任意 C++ 表达式。由于这些表达式本身可能包含带引号的字符串或括号,因此很难用正则表达式匹配所有情况。此外,这个表达式可能有多种嵌套形式

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

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

是否有任何关于如何继续前进的建议,无论是沿着规定的路线之一,还是完全沿着不同的路线?

【问题讨论】:

  • 只是为了确保您没有在这里做 x-y 问题,您能告诉我们您通过替换想要完成什么吗?
  • C++ 解释器也有一些可供用户使用的功能。我想为这些函数添加额外的行为,但它们没有任何机制可以在其中添加钩子。一旦我将一个字符串传递给解释器,我就无法直接重新获得控制权。因此,我想在传入之前修改字符串,以便在适当的时候将控制权交还给我。
  • 两个问题:这些命令会单独一行吗?是否可以在同一行中看到对Draw 的重复调用? (A->Draw(...)->...->Draw(...))?
  • 一行中可能有多个命令,例如A->Draw(B); C->Draw(D)func(A->Draw(B), C->Draw(D))。不会重复调用您显示的模式,因为Draw 返回一个整数值。
  • A->Draw(B1, B2)的求值过程中会发生宏扩展吗?

标签: c++ clang root-framework


【解决方案1】:

尝试修改头文件来抑制方法,然后编译你会发现错误并且可以替换所有核心。

只要你有一个 C++ 解释器(作为 CERN 的根),我猜你必须使用编译器来拦截所有的 Draw,一个简单而干净的方法是在头文件中将 Draw 方法声明为私有,使用一些定义

 class ItemWithDrawMehtod
 {
 ....
 public:
 #ifdef CATCHTHEMETHOD
     private:
 #endif
 void Draw(A,B);
 #ifdef CATCHTHEMETHOD
     public:
 #endif
 ....
 };

然后编译为:

 gcc -DCATCHTHEMETHOD=1 yourfilein.cpp

【讨论】:

  • 恐怕我不确定这会有什么好处。由于我的程序将分发给将针对未修改版本的库进行编译的其他用户,因此将库本身中的调用修改为调用 MyFunc 而不是 Draw 并不能解决问题。此外,用户习惯于将Draw 命令传递给解释器,而我要更改的正是这些命令。
  • 这个解决方案没有真正的好处,不是用于生产而是用于开发。关键是产生一个强制错误,让编译器为你完成这项工作。在“您的责任”的源代码中,您可以使用此技巧来捕获所有错误并为其他编码人员提供您的功能。但是使用新功能是他们的责任吗?
【解决方案2】:

如果用户想向应用程序输入复杂的算法,我建议将脚本语言集成到应用程序中。这样用户就可以编写代码[函数/算法以定义的方式],以便应用程序可以在解释器中执行它并获得最终结果。例如:Python、Perl、JS 等。

由于您在解释器中需要 C++,http://chaiscript.com/ 将是一个建议。

【讨论】:

  • 目前,已经有一种脚本语言,C++。虽然 C++ 通常不用作脚本语言,但我使用的库包含 C++ 解释器。我可以在命令传递到解释器之前对其进行修改,但我不能修改库代码本身,因为其他用户将针对未修改的库版本进行编译。
【解决方案3】:

当有人获得 Draw 成员函数 (auto draw = &A::Draw;) 然后开始使用 draw 时会发生什么?大概您也希望在这种情况下调用相同的改进绘图功能。因此,我认为我们可以得出结论,您真正想要的是将Draw 成员函数替换为您自己的函数。

由于您似乎无法直接修改包含Draw 的类,因此解决方案可能是从A 派生您自己的类并在其中覆盖Draw。然后你的问题就减少到让你的用户使用你新改进的类。

您可能会再次考虑将类A 的使用自动转换为您的新派生类的问题,但是如果没有完整的C++ 实现的帮助,这似乎仍然相当困难。也许有一种方法可以隐藏 A 的旧定义并以该名称显示您的替换,通过巧妙地使用头文件,但我无法确定您告诉我们的情况是否如此。

另一种可能是使用一些使用 LD_PRELOAD 的动态链接器来替换在运行时调用的函数 Draw

【讨论】:

    【解决方案4】:

    可能有一种方法可以主要使用正则表达式来完成此操作。

    由于 Draw( 之后出现的任何内容都已正确格式化为参数,因此您无需完全解析它们以达到您概述的目的。

    从根本上说,重要的部分是“SYMBOL->Draw(”

    SYMBOL 可以是解析为重载对象的任何表达式 -> 或解析为实现 Draw(...) 的类型的指针。如果将其减少到两种情况,则可以缩短解析。

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

    第二种情况适用于返回重载对象或指针的复杂表达式。这需要更多的努力,但是可以轻松地编写一个简短的解析例程来向后遍历一个复杂的表达式,因为您不必支持块(C++ 中的块不能返回对象,因为 lambda 定义不调用lambda 本身和实际的嵌套代码块 {...} 不能直接返回任何适用于此处的内联代码)。请注意,如果表达式不以 ) 结尾,那么它在此上下文中必须是有效符号,因此如果您发现 a ) 只需将嵌套的 ) 与 ( 匹配并提取嵌套 SYMBOL(...(.. .)...)->Draw() 模式。这可以通过正则表达式实现,但在普通代码中也应该相当容易。

    只要你有了符号或表达式,替换就很简单了,从

    符号->绘制(...

    你的函数(符号,...

    无需处理 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:不一定。我说“这可能通过正则表达式实现,但在普通代码中也应该相当容易。”以编译为目标的解析代码与以识别表达式的开始/结束位置为目标的解析代码之间存在差异。至于我是否实现了它,我在不到两百行 C++ 中编写了一个 C++ Server Page 编译器供我自己使用(转换为 GCC 或 Visual Studio 可以使用预构建项目编译的标准 C++ 源代码),使用这个方法,但不使用正则表达式。它可能很脆弱,这取决于。
    • 要处理 C++ 表达式,你必须选择 C++ 词位,同意吗?你不能合理地建议你可以用 200 行编写一个 C++ 词法分析器,更不用说挑选子表达式了,所以我看不出你是如何准确地做到这一点的。其次,有些东西在解释为声明与表达式时的解析方式不同;你打算如何处理这个?最后,要替换表达式,您必须在语句的所有级别上找到它们;由于嵌套,此 is 正在解析。我发现您暗示您的 C++ 服务器页面处理任意 C++ 令人难以置信。
    • 编写一个为完整编译器生成解析树的 C++ 解析器将非常困难;但是,编写一个可以区分和提取块中的 C++ 代码的解析器实际上非常容易。无需构建完整的解析树,像递归遍历代码这样的事情相当容易,因为 C++ 对代码可以包含的内容非常严格(例如,[ 可以在没有 ] 的情况下存在于 C++ 中吗?除了转义的“,还有其他在字符串中影响跳过它吗?)。OP不需要代码来重写声明,只需要表达式,因此对于该反例来说忽略声明就足够了。
    • 我的团队有编写完整的 C++ 前端的经验;是的,这很难,然后你要弄清楚子表达式的类型。 OP 似乎正在将任意 C++ 代码元素输入到他的解释器中。他输入的表达式有类型。即使 OP 正在输入琐碎的表达式,大概他的“替换”必须是类型兼容的。因此,您不仅需要解析,还需要根据 C++ 规则解析名称。为什么你坚持这很容易我无法理解。
    【解决方案5】:

    你想要的是Program Transformation System。 这些工具通常可以让您表达对源代码的更改,以源代码级别的模式编写,基本上说:

     if you see *this*, replace it by *that*
    

    但在抽象语法树上运行,因此匹配和替换过程是 比你通过字符串黑客获得的更值得信赖。

    此类工具必须具有用于感兴趣的源语言的解析器。 源语言是 C++ 使得这相当困难。

    Clang 有点资格;毕竟它可以解析C++。操作对象 如果没有所有环境上下文,它就无法做到这一点。的程度 OP 正在输入(格式良好的)程序片段(语句等)。 进入解释器,Clang可能[我没有太多经验 我自己] 无法专注于片段是什么(语句?表达式?声明?...)。最后,Clang 并不是真正的 PTS。它的树修改过程不是源到源的转换。这对方便很重要,但可能不会阻止 OP 使用它;表面语法重写规则很方便,但您总是可以更加努力地替换程序树黑客。当规则多于几条时,这开始变得很重要。

    GCC with Melt 的限定方式与 Clang 相同。 我的印象是 Melt 最多使 GCC 少一点 不能容忍这种工作。 YMMV。

    我们的DMS Software Reengineering Toolkit 及其full C++14 [EDIT July 2018: C++17] front end 绝对符合条件。 DMS已被用于进行大规模改造 在大规模 C++ 代码库上。

    DMS 可以parse arbitrary (well-formed) fragments of C++ 无需事先被告知语法类别是什么,并使用其模式解析机制返回正确语法非终结符类型的 AST。 [您可能会得到多个解析,例如歧义,您将决定如何解决,请参阅Why can't C++ be parsed with a LR(1) parser? 以获得更多讨论] 如果您愿意在解析时不使用宏扩展并坚持预处理器指令(它们也得到解析)相对于代码片段结构很好(#if foo{#endif not allowed),但这对于交互式输入的代码片段来说不太可能是一个真正的问题。

    然后,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 将采用任何包含左侧“...绘制...”模式的已解析 AST,并在将匹配项替换为 A、B1 和 B2 之后用右侧替换该子树。引号是 metaquotes,用于区分 C++ 文本和规则语法文本;反斜杠是在元引号内用于命名元变量的元转义。有关您可以在规则语法中表达的更多详细信息,请参阅DMS Rewrite Rules

    如果 OP 提供了一个集合这样的规则,则可以要求 DMS 应用整个集合。

    所以我认为这对 OP 来说效果很好。这是一个相当重量级的机制,可以“添加”到他想要提供给第 3 方的包中; DMS 及其 C++ 前端几乎不是“小”程序。但是现代机器拥有大量资源,所以我认为这是一个问题,即 OP 需要做到这一点。

    【讨论】:

      【解决方案6】:

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

      【讨论】:

      • 虽然这一种在应用程序中执行用户代码的方式,但在这种情况下它并不是我们想要的(用户在运行时输入“实时”代码)。跨度>
      猜你喜欢
      • 2015-12-16
      • 1970-01-01
      • 2017-09-12
      • 1970-01-01
      • 1970-01-01
      • 2021-05-02
      • 1970-01-01
      • 1970-01-01
      • 2014-04-11
      相关资源
      最近更新 更多