我希望能够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*.so
到dlsym
-ed函数指针的函数。 在实践中,你会决定例如每个generated*.c
定义一个函数void dynfoo(int)
和int dynbar(int,int)
并使用dlsym
和"dynfoo"
和"dynbar"
并调用这些thru函数指针(由dlsym
)。 你还应该定义这些dynfoo
和dynbar
被调用的方式和时间的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
得到一个指向你的库的任何函数/变量的指针。
为了在内存中编译你的库,我认为你可以做的最好的事情就是创建这里描述的内存文件系统。