我希望能够动态生成 C 代码并将其快速重新加载到我正在运行的 C 程序中。
我在Linux上,怎么能做到这一点?
Linux 上的库 .so 文件可以在运行时重新编译和重新加载吗?
是否可以在不生成 .so 文件的情况下对其进行编译,编译后的输出是否可以以某种方式进入内存然后重新加载?我想快速重新加载编译的代码。
您想要做的事情是合理的,而我正是在MELT(扩展 GCC 的高级域特定语言;MELT 编译为 C,通过本身用 MELT 编写的翻译器)中做的。
首先,在生成 C 代码(或许多其他源语言)时,一个好的建议是在内存中保留某种抽象语法树(AST)。因此,首先构建生成的 C 代码的整个 AST,然后将其作为 C 语法发出。不要认为你的代码生成框架没有明确的 AST(换句话说,用一堆 printf 生成 C 代码是一场维护噩梦,你想要一些中间表示)。
其次,生成 C 代码的主要原因是利用良好的优化编译器(另一个原因是 C 的可移植性和普遍性)。如果您不关心生成代码的性能(并且 TCC 非常快地将 C 编译成非常幼稚和缓慢的机器代码),您可以使用其他一些方法,例如使用一些 JIT 库,如Gnu 闪电(非常快速地生成慢速机器代码)、Gnu Libjit或ASMJIT(生成的机器代码更好一点)、LLVM或GCCJIT(生成的机器代码很好,但生成时间与编译器相当)。
因此,如果您生成 C 代码并希望它快速运行,则 C 代码的编译时间不可忽略(因为您可能会派生一个gcc -O -fPIC -shared
命令以foo.so
从您生成的foo.c
. 根据经验,生成 C 代码比编译它(使用gcc -O
)花费的时间要少得多。在 MELT 中,C 代码的生成速度比 GCC 编译快 10 倍以上(通常快 30 倍)。但是 C 编译器所做的优化是值得的。
一旦你发布了你的 C 代码,将它的编译分叉到一个.so
共享对象中,你就可以dlopen
了。不要害羞,我的manydl.c示例演示了在 Linux 上您可以 dlopen 大量共享对象(数十万个)。真正的瓶颈是生成的 C 代码的编译。实际上,您实际上并不需要dlclose
在 Linux 上编写(除非您正在编写一个需要运行数月的服务器程序);未使用的共享模块实际上可以保留dlopen
,并且您主要是在泄漏进程地址空间(这是一种廉价的资源),因为大部分未使用的模块.so
将被换出。dlopen
很快就完成了,需要时间的是编译 C 源代码,因为您确实希望由 C 编译器完成优化。
您可以使用许多其他不同的方法,例如拥有一个字节码解释器并为该字节码生成,使用 Common Lisp(例如 Linux 上的 SBCL,它动态编译为机器代码)、LuaJit、Java、MetaOcaml 等。
正如其他人所建议的那样,您不太关心编写 C 文件的时间,实际上它会保留在文件系统缓存中(另请参阅this)。而且编写它比编译它要快得多,所以留在内存中是不值得的。如果您担心 I/O 时间,请使用一些tmpfs 。
您问
Linux 上的库
.so
文件可以在运行时重新编译和重新加载吗?
当然是的:你应该派生一个命令来从生成的 C 代码构建库(例如 a gcc -O -fPIC -shared generated.c -o generated.so
,但你可以间接地做到这一点,例如通过运行 a make -j
,特别是如果generated.so
它足够大以使其与generated.c
在几个 C 生成的拆分相关文件!),然后你用dlopen动态加载你的库(给它一个完整的路径/some/file/path/to/generated.so
,可能还有RTLD_NOW
标志),你必须使用它dlsym
来查找里面的相关符号。不要考虑重新加载(第二次)相同的generated.so
,最好发出一个唯一的generated1.c
(然后generated2.c
等...)C文件,然后将其编译为一个唯一 generated1.so
的(第二次等generated2.so
...)然后至dlopen
它(这可以做几十万次)。您可能希望在发出的generated*.c
文件中包含一些构造函数,这些构造函数将dlopen
在generated*.so
您的基础应用程序应该已经定义了关于一组dlsym名称(通常是函数)以及如何调用它们的约定。它应该只在你的generated*.so
直通dlsym
函数指针中直接调用函数。在实践中,您会决定,例如,每个都generated*.c
定义一个函数void dynfoo(int)
,int dynbar(int,int)
并使用anddlsym
并通过函数指针调用这些函数指针(由 返回)。您还应该定义如何以及何时调用这些和的约定。您最好将您的基础应用程序链接起来,以便您的文件可以调用您的应用程序函数。"dynfoo"
"dynbar"
dlsym
dynfoo
dynbar
-rdynamic
generated*.c
您不希望您generated*.so
重新定义 现有名称。malloc
例如,您不想重新定义generated*.c
并期望所有堆分配函数都能神奇地使用您的新变体(这可能不起作用,即使它起作用,也会很危险)。
您可能不会打扰dlclose
动态加载的共享对象,除非在应用程序清理和退出时(但我根本不打扰dlclose
)。如果您执行dlclose
一些动态加载的generated*.so
文件,请确保其中没有使用任何内容:不存在指针,甚至不存在调用帧中的返回地址。
PS MELT 翻译器目前将 57KLOC 的 MELT 代码翻译成近 1770KLOC 的 C 代码。
您确定 C 是正确的答案吗?有各种解释语言,例如 Lua、Bigloo Scheme,甚至可能是 Python,它们都很好地嵌入到了现有的 C 应用程序中。您可以使用扩展语言编写动态部分,这将支持在运行时重新加载代码。
明显的缺点是性能——如果你绝对需要编译 C 的原始速度,那么这些可能是不行的。
如果要动态重新加载库,可以使用dlopen
函数(参见 mans)。它打开一个库 .so 文件并返回一个指向它的 void* 指针,然后您可以使用 .so 获取指向库的任何函数/变量的指针dlsym
。
要在内存中编译你的库,我认为你能做的最好的事情就是创建内存文件系统,如此处所述。