在C或C++中有没有一种在运行时编译额外代码的方法?

22

我想要做的是:

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

我希望能够在类Unix系统(尤其是Linux和Mac OS X)上使用gcc(以及最终的Java),使用CC ++来实现这一目标。基本思路是为这些语言实现一个读入-求值-输出循环,即在输入表达式和语句时编译它们,并使用它们来修改现有的数据结构(这在脚本语言中经常发生)。我正在使用python编写此工具,该工具生成C/C ++文件,但这并不重要。

我已经尝试使用共享库来完成这个任务,但了解到修改共享库不会影响已经在运行的程序。我还尝试使用共享内存,但找不到一种将函数加载到堆上的方法。我也考虑过使用汇编代码,但还没有尝试过。

除非绝对无法在gcc中实现,否则我不想使用除gcc以外的任何编译器。

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


1
你应该看看llvm/clang。但是为C或C++创建一个REPL听起来是一项相当艰巨的任务。 - Mat
有关的东西? - jperelli
我宁愿使用真正的脚本语言,除非你能告诉我这一切是为了什么。 - LeleDumbo
6个回答

15

有一个简单的解决方案:

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

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

structs.h:

struct S {
    int a,b;
};

main.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节省资源。


非常有用的信息,但您如何将main.cpp包含在tmp.cpp中? - dreamer_999
好的,我本来想编辑问题来回答你,但没有必要 :) 你不能将main.cpp包含在tmp中。如果你想分享一些数据,那么你必须使用头文件(或直接写入文件)并将结构传递给动态创建的函数 :) - kravemir
谢谢!当使用头文件时,共享变量的值不同。因此,我最终不得不将它们传递给函数。我想知道是否有某种方法可以避免传递变量。 - dreamer_999
如果您将头文件包含到cpp中,那么它就相当于您已经将其内容写入了cpp中。因此,您最终会得到两个变量实例(main.cpp和动态库),但是如果您在同一库中的两个对象(cpp-s)中包含了定义变量的头文件,则编译时会出现错误。您必须在头文件中使用"extern"关键字,告诉编译器这些变量不是在当前对象(cpp)中实例化的,并且将由链接器链接。您可以使变量“静态”,它们将在每个对象中私有实例化,但是您无论如何都不会共享任何东西。 - kravemir

14

我认为您可以使用动态库并在运行时加载它们(使用 dlopen 等函数)来实现这一点。

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

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

你显然必须在进行编译的同时逐步更新代码,但如果你不断替换mynewcode.so,我认为这对你有用。


2
应该支持加载,但我不确定在所有情况下是否支持卸载 - Chris Stratton
@ChrisStratton:我承认我对运行时加载远非专家,但手册让我相信符号在dlclose(特别是RTLD_NODELETE标志)时被卸载。不过,这一切都要带着点保留 :)。 - Stephen Newell
@ChrisStratton,我不了解“所有”情况,但在我的一个项目中,我从未见过*dlclose()未卸载符号,除非当然通过传递RTLD_NODELETE*,这种情况下它不会卸载它们。 - Pryftan
注意:*cosine = (double (*)(double)) dlsym(handle, "cos");* 根据 ISO C 标准,像上面这样在函数指针和 'void *' 之间进行强制类型转换会产生未定义的结果。POSIX.1-2003 和 POSIX.1-2008 接受了这种情况,并提出了以下解决方法:*(void **) (&cosine) = dlsym(handle, "cos");*(1/2) - Pryftan
现在再加上一个东西。至于*RTLD_LAZY,只是为了澄清它的含义:对于函数(而不是变量),它仅在调用函数时解析未解决的符号。根据函数的使用和重要性,这可能是不可取的 - 例如,如果在此时无法解析符号,则可能希望中止程序。在这种情况下,您可以使用RTLD_NOW。您可以使用dlerror()来报告最近相关函数的错误。当然,您需要这些函数的链接器标志-ldl*。我想这就是我现在所拥有的全部! - Pryftan
显示剩余4条评论

5
尽管LLVM现今主要用于编译中的优化和后端角色,但其核心是低级虚拟机。
LLVM可以即时编译代码,尽管返回类型可能相当不透明,因此如果您准备将自己的代码包装在其周围并不太担心将发生的强制转换,它可能会对您有所帮助。
然而,C和C ++并不适合这种情况。

3

3

使用OpenCL可以实现可移植性

OpenCL 是一个得到广泛支持的标准,主要用于将计算任务卸载到专门的硬件上,例如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
}

2
如果其他方法都不起作用,特别是如果在您的运行时平台上不支持卸载共享库,则可以采用困难的方式。
1)使用system()或其他方法执行gcc或make或其他编译代码
2)将其链接为平面二进制文件或解析链接器在您的平台上输出的任何格式(elf?)
3)通过mmap()可执行文件或者通过设置执行位的匿名mmap获取可执行页面,并将代码复制/解压缩到那里(不是所有平台都关心该位,但让我们假设您拥有一个关心该位的平台)
4)刷新任何数据和指令高速缓存(因为两者之间的一致性通常不能保证)
5)通过函数指针或其他方式调用它
当然还有另一种选择 - 根据您需要的交互级别,您可以构建一个单独的程序,然后启动它并等待结果,或者分叉并启动它,并通过管道或套接字与它通信。 如果这能满足您的需求,那么会更容易些。

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