【问题标题】:Is there any way to compile additional code at runtime in C or C++?有没有办法在运行时用 C 或 C++ 编译附加代码?
【发布时间】:2012-05-20 20:03:45
【问题描述】:

这是我想做的:

  1. 运行程序并初始化一些数据结构。
  2. 然后编译可以访问/修改现有数据结构的附加代码。
  3. 根据需要重复第 2 步。

我希望能够在类 Unix 系统(尤其是 Linux 和 Mac OS X)上使用 gcc(最终是 Java)对 CC++ 执行此操作。这个想法基本上是为这些语言实现一个 read-eval-print 循环,在输入表达式和语句时编译它们并使用它们来修改现有的数据结构(在脚本语言中一直都在这样做)。我正在python 中编写这个工具,它会生成C/C++ 文件,但这应该不相关。

我已经探索过使用共享库来执行此操作,但了解到修改共享库不会影响已经运行的程序。我也尝试过使用共享内存,但找不到将函数加载到堆上的方法。我也考虑过使用汇编代码,但还没有尝试过。

我不希望使用除gcc 之外的任何编译器,除非在gcc 中绝对没有办法这样做。

如果有人有任何想法或知道如何做到这一点,我们将不胜感激。

【问题讨论】:

  • 你应该看看llvm/clang。但是 C 或 C++ 的 REPL 听起来像是一项艰巨的任务。
  • this有什么关系?
  • 我宁愿使用真正的脚本语言,除非你能说出这一切是为了什么

标签: c++ c linux gcc runtime-compilation


【解决方案1】:

有一个简单的解决方案:

  1. 创建自己的具有特殊功能的库
  2. 加载创建的库
  3. 执行该库中的函数,将结构作为函数变量传递

要使用您的结构,您必须包含与宿主应用程序相同的头文件。

structs.h:

struct S {
    int a,b;
};

ma​​in.cpp:

#include <iostream>
#include <fstream>
#include <dlfcn.h>
#include <stdlib.h>

#include "structs.h"

using namespace std;

int main ( int argc, char **argv ) {

    // create own program
    ofstream f ( "tmp.cpp" );
    f << "#include<stdlib.h>\n#include \"structs.h\"\n extern \"C\" void F(S &s) { s.a += s.a; s.b *= s.b; }\n";
    f.close();

    // create library
    system ( "/usr/bin/gcc -shared tmp.cpp -o libtmp.so" );

    // load library        
    void * fLib = dlopen ( "./libtmp.so", RTLD_LAZY );
    if ( !fLib ) {
        cerr << "Cannot open library: " << dlerror() << '\n';
    }

    if ( fLib ) {
        int ( *fn ) ( S & ) = dlsym ( fLib, "F" );

        if ( fn ) {
            for(int i=0;i<11;i++) {
                S s;
                s.a = i;
                s.b = i;

                // use function
                fn(s);
                cout << s.a << " " << s.b << endl;
            }
        }
        dlclose ( fLib );
    }

    return 0;
}

输出:

0 0
2 1
4 4
6 9
8 16
10 25
12 36
14 49
16 64
18 81
20 100

您还可以创建可变程序,它会改变自己(源代码),重新编译自己,然后用 execv 替换它的实际执行,并使用共享来节省资源记忆。

【讨论】:

  • 非常有用的信息,但是您将如何在 tmp.cpp 中包含 main.cpp?
  • 好吧,我打算编辑问题来回答你,但没有必要:) 你不能在 tmp 中包含 main.cpp。如果要共享一些数据,则必须使用标头(或将其直接写入文件)并将结构传递给动态创建的函数:)
  • 谢谢!但是,当使用标头时,共享变量的值不一样。所以我最终不得不将它们传递给函数。我想知道是否有某种方法可以绕过传递变量
  • 如果你在cpp中包含header,就如同你将它的内容写入cpp一样。因此,您最终会得到两个变量实例(main.cpp 和动态库),但是如果您将标头(定义变量)包含在同一个库中的两个对象(cpp-s)中,那么它将在编译中引发错误。您必须在标头中使用“extern”关键字来告诉编译器,这些变量不会在当前对象(cpp)中实例化,并且将由链接器链接。您可以将变量设置为“静态”,它们将在每个对象中私下实例化,但您不会共享任何内容。
【解决方案2】:

我认为您可以使用动态库并在运行时加载它们(使用dlopen 和朋友)来完成此操作。

void * lib = dlopen("mynewcode.so", RTLD_LAZY);
if(lib) {
    void (*fn)(void) = dlsym(lib, "libfunc");

    if(fn) fn();
    dlclose(lib);
}

您显然必须在进行过程中编译新代码,但如果您继续替换 mynewcode.so,我认为这对您有用。

【讨论】:

  • 应该支持加载,但是我不确定在所有情况下都支持 un 加载。
  • @ChrisStratton:我承认我在运行时加载方面的专家,但手册页让我相信符号是在 dlclose 卸载的(特别是RTLD_NODELETE 标志)。不过,把这一切都放在一粒盐里:)。
  • @ChrisStratton 我不知道“所有”情况,但在我的一个项目中,我从未见过 dlclose() 不卸载符号。除非当然 RTLD_NODELETE 被传递,在这种情况下它确实卸载它们。
  • 注: cosine = (double (*)(double)) dlsym(handle, "cos"); .. 根据 ISO C 标准,如上所述,在函数指针和“void *”之间进行强制转换会产生未定义的结果。 POSIX.1-2003 和 POSIX.1-2008 接受了这种情况并提出了以下解决方法:*(void **) (&amp;cosine) = dlsym(handle, "cos"); (1/2)
  • 这个(笨拙的)强制转换符合 ISO C 标准,可以避免任何编译器警告。 POSIX.1-2008(又名 POSIX.1-2013)的 2013 年技术勘误通过要求符合要求的实现支持将“void *”转换为函数指针来改进问题。然而,一些编译器(例如,带有 '-pedantic' 选项的 gcc)可能会抱怨这个程序中使用的强制转换。(来自 dlopen(3))就像关于这个问题的注释一样:) (2/2)
【解决方案3】:

尽管 LLVM 现在主要用于编译中的优化和后端角色,但它的核心是低级虚拟机。

LLVM 可以 JIT 代码,即使返回类型可能非常不透明,所以如果您准备好将自己的代码包装在它周围并且不要太担心将要发生的转换,它可能会有所帮助你。

但是 C 和 C++ 对这种事情并不友好。

【讨论】:

    【解决方案4】:

    是的 - 您可以使用 Runtime Compiled C++(或查看 RCC++ blog and videos)或其alternatives 之一来执行此操作。

    【讨论】:

      【解决方案5】:

      这可以通过 OpenCL 轻松完成

      OpenCLwidely supported 标准,主要用于将计算卸载到专用硬件,例如 GPU。但是,它在 CPU 上也能正常工作,并且实际上执行 C99 类代码的运行时编译作为其核心功能之一(这就是实现硬件可移植性的方式)。较新的版本 (2.1+) 也接受 C++14 的大部分子集。

      此类运行时编译和执行的基本示例可能如下所示:

      #ifdef __APPLE__
      #include<OpenCL/opencl.h>
      #else
      #include<CL/cl.h>
      #endif
      #include<stdlib.h>
      int main(int argc,char**argv){//assumes source code strings are in argv
          cl_int e = 0;//error status indicator
          cl_platform_id platform = 0;
          cl_device_id device = 0;
          e=clGetPlatformIDs(1,&platform,0);                                      if(e)exit(e);
          e=clGetDeviceIDs(platform,CL_DEVICE_TYPE_ALL,1,&device,0);              if(e)exit(e);
          cl_context context = clCreateContext(0,1,&device,0,0,&e);               if(e)exit(e);
          cl_command_queue queue = clCreateCommandQueue(context,device,0,&e);     if(e)exit(e);
          //the lines below could be done in a loop, assuming you release each program & kernel
          cl_program program = clCreateProgramWithSource(context,argc,(const char**)argv,0,&e);
          cl_kernel kernel = 0;                                                   if(e)exit(e);
          e=clBuildProgram(program,1,&device,0,0,0);                              if(e)exit(e);
          e=clCreateKernelsInProgram(program,1,&kernel,0);                        if(e)exit(e);
          e=clSetKernelArg(kernel,0,sizeof(int),&argc);                           if(e)exit(e);
          e=clEnqueueTask(queue,kernel,0,0,0);                                    if(e)exit(e);
          //realistically, you'd also need some buffer operations around here to do useful work
      }
      

      【讨论】:

        【解决方案6】:

        如果没有其他办法 - 特别是,如果卸载共享库最终在您的运行时平台上不受支持,您可能会遇到困难。

        1) 使用 system() 或其他任何东西来执行 gcc 或 make 或其他任何东西来构建代码

        2)要么将其链接为平面二进制文件,要么自己解析链接器在您的平台上输出的任何格式(elf?)

        3) 获取一些可执行页面,通过 mmap()'ing 一个可执行文件或使用设置了执行位的匿名 mmap 并在那里复制/解压缩您的代码(并非所有平台都关心该位,但让我们假设你有一个这样做的)

        4) 刷新所有数据和指令缓存(因为通常不能保证两者之间的一致性)

        5) 通过函数指针或其他方式调用它

        当然还有另一种选择 - 根据您需要的交互级别,您可以构建一个单独的程序并启动它并等待结果,或者分叉并启动它并通过管道或套接字与其通信。如果这能满足您的需求,那就不会那么棘手了。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-02-16
          • 1970-01-01
          • 1970-01-01
          • 2016-04-18
          相关资源
          最近更新 更多