如何dynamic加载经常重新生成的C代码快速?

我希望能够dynamic生成C代码,并将其快速重新加载到正在运行的C程序中。

我在Linux上,这怎么可能呢?

Linux上的库.so文件是否可以在运行时重新编译和重新加载?

它可以被编译而不产生一个.so文件,编译的输出可以不知何故去内存,然后重新加载? 我想快速重新加载编译的代码。

你想做的事情是合理的,我正在做MELT (一个高级领域专用语言来扩展GCC; MELT被编译成C,通过一个由MELT编写的翻译器)。

首先,在生成C代码(或许多其他源语言)时,一个好的建议是在内存中保留某种抽象语法树 (AST)。 因此,首先构建生成的C代码的整个AST,然后将其作为C语法发出。 不要想到你的代码生成框架没有明确的AST(换句话说,用一堆printf生成C代码是一个维护噩梦,你想有一些中间表示)。

其次,生成C代码的主要原因是要利用一个好的优化编译器(另一个原因是C的可移植性和无处不在)。 如果你不关心生成的代码的性能(和TCC非常迅速地编译成一个非常天真和慢的机器代码),你可以使用一些其他的方法,例如使用一些JIT库,如Gnu闪电 (非常快的一代慢速机代码), Gnu Libjit或ASMJIT (生成的机器代码稍好一些), LLVM或GCCJIT (生成良好的机器代码,但生成时间与编译器相当)。

所以,如果你生成C代码,并希望它快速运行,C代码的编译时间是不可忽略的(因为你可能会分叉gcc -O -fPIC -shared命令,使一些共享对象foo.so生成foo.c )。 根据经验,生成C代码所需的时间要比编译C代码要少得多(使用gcc -O )。 在MELT中,C代码的生成速度比GCC编译速度快10倍以上(通常快30倍)。 但是C编译器所做的优化是值得的。

一旦你发出你的C代码,把它编译成一个.so共享对象,你就可以把它编译。 不要害羞,我的manydl.c示例表明,在Linux上可以减少大量的共享对象(数十万)。 真正的瓶颈是编译生成的C代码。 实际上,你并不需要在Linux上使用dlclose (除非你编写的服务器程序需要运行好几个月)。 一个未使用的共享模块可以保持实际的运行状态,而且你主要泄露进程地址空间(这是一个便宜的资源),因为大部分未使用的.so将被换出。 dlopen快速完成,需要花费时间的是编译C源代码,因为您确实希望由C编译器完成优化。

你可以使用许多其他不同的方法,例如,有一个字节码解释器,并为该字节码生成,使用Common Lisp(例如Linux上的SBCL,可以动态编译为机器码),LuaJit,Java,MetaOcaml等。

正如其他人所建议的那样,您不必太在意编写C文件的时间,并且它将在实践中保留在文件系统缓存中(另见本文 )。 编写它比编译要快得多,所以留在内存中是不值得的。 如果您担心I / O时间,请使用一些tmpfs

附加物

你问

Linux上的库.so文件可以在运行时重新编译和重新加载吗?

当然是的:你应该分叉一个命令来从生成的C代码构建库(例如,一个gcc -O -fPIC -shared generated.c -o generated.so ,但是你可以通过运行make -j ,尤其是如果generated.so的.so足够大,可以将generated.c的C分割成几个C生成的文件!),然后用dlopen动态加载你的库(给出一个像/some/file/path/to/generated.so这样的完整路径) /some/file/path/to/generated.so ,可能是RTLD_NOW标志),你必须使用dlsym来查找里面的相关符号。 不要以为再次加载(第二次)相同generated.so ,最好是发出一个独特的generated1.c (然后generated2.c等…)C文件,然后将其编译为一个唯一的 generated1.so (第二次generated2.so dlopen ,等等…),然后将其dlopen (这可以完成数十万次)。 您可能希望在发出的generated*.c文件中有一些构建函数,这些函数将在generated*.so dlopen时间执行

你的基础应用程序应该已经定义了一套有关dlsym -ed名字(通常是函数)的集合以及它们是如何被调用的。 它应该只能直接调用你generated*.sodlsym -ed函数指针的函数。 在实践中,你会决定例如每个generated*.c定义一个函数void dynfoo(int)int dynbar(int,int)并使用dlsym"dynfoo""dynbar"并调用这些thru函数指针(由dlsym )。 你还应该定义这些dynfoodynbar被调用的方式和时间的dynbar 。 您最好将您的基础应用程序与-rdynamic以便generated*.c文件可以调用您的应用程序功能。

希望您generated*.so 重新定义 现有的名称。 例如,你不想在你generated*.c重新定义malloc ,并希望所有的堆分配函数神奇地使用你的新变体(这可能不会工作,如果即使这样做也是危险的)。

除了在应用程序清理和退出时(除非我完全不打扰dlclose ),您可能不会打扰动态加载的共享对象。 如果你做了一些动态加载的generated*.so文件,确保没有任何东西被使用:没有指针,甚至没有返回地址在调用帧,是存在的。

PS MELT翻译器目前已将57KLOC的MELT代码翻译成接近1770KLOC的C代码。

你最好的选择可能是TCC编译器,它可以让你做到这一点—编译源代码,将其添加到你的程序,运行它,而不用触摸文件。

对于一个更强大但非基于C的解决方案,您应该检查一下LLVM项目,该项目从生成JIT的角度来看,其功能大致相同。 你不能通过C去使用,而是使用一种抽象的便携式机器代码,但生成的代码加载速度更快,并且处于更积极的发展阶段。

OTOH如果你想手动完成所有的工作,那么你可以通过shell去编译一个.so ,然后自己加载, dlopen()dlclose()会自己做。

你确定C在这里是正确的答案吗? 有各种各样的解释语言,比如Lua,Bigloo Scheme,或者甚至可以嵌入到现有C应用程序中的Python。 您可以使用扩展语言编写动态部分,这将支持在运行时重新加载代码。

性能明显的缺点是 – 如果你绝对需要编译C的原始速度,那么这些可能是不可行的。

如果你想动态地重新加载一个库,你可以使用dlopen函数(见mans)。 它打开一个库.so文件并返回一个void *指针,然后你可以用dlsym得到一个指向你的库的任何函数/变量的指针。

为了在内存中编译你的库,我认为你可以做的最好的事情就是创建这里描述的内存文件系统。