9

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

我在Linux上,怎么能做到这一点?

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

是否可以在不生成 .so 文件的情况下对其进行编译,编译后的输出是否可以以某种方式进入内存然后重新加载?我想快速重新加载编译的代码。

4

4 回答 4

13

您想要做的事情是合理的,而我正是在MELT(扩展 GCC 的高级域特定语言;MELT 编译为 C,通过本身用 MELT 编写的翻译器)中做的。

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

其次,生成 C 代码的主要原因是利用良好的优化编译器(另一个原因是 C 的可移植性和普遍性)。如果您不关心生成代码的性能(并且 TCC 非常快地将 C 编译成非常幼稚和缓慢的机器代码),您可以使用其他一些方法,例如使用一些 JIT 库,如Gnu 闪电(非常快速地生成慢速机器代码)、Gnu LibjitASMJIT(生成的机器代码更好一点)、LLVMGCCJIT(生成的机器代码很好,但生成时间与编译器相当)。

因此,如果您生成 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文件中包含一些构造函数,这些构造函数将dlopengenerated*.so

您的基础应用程序应该已经定义了关于一组dlsym名称(通常是函数)以及如何调用它们的约定。它应该只在你的generated*.so直通dlsym函数指针中直接调用函数。在实践中,您会决定,例如,每个都generated*.c定义一个函数void dynfoo(int)int dynbar(int,int)并使用anddlsym并通过函数指针调用这些函数指针(由 返回)。您还应该定义如何以及何时调用这些和的约定。您最好将您的基础应用程序链接起来,以便您的文件可以调用您的应用程序函数。"dynfoo""dynbar"dlsymdynfoodynbar-rdynamicgenerated*.c

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

您可能不会打扰dlclose动态加载的共享对象,除非在应用程序清理和退出时(但我根本不打扰dlclose)。如果您执行dlclose一些动态加载的generated*.so文件,请确保其中没有使用任何内容:不存在指针,甚至不存在调用帧中的返回地址。

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

于 2012-09-07T17:22:15.490 回答
4

您最好的选择可能是TCC编译器,它允许您完全做到这一点 --- 编译源代码、将其添加到您的程序、运行它,所有这些都无需接触文件。

对于更健壮但非基于 C 的解决方案,您可能应该查看LLVM项目,该项目的作用大致相同,但从生成 JIT 的角度来看。您不必通过 C 语言,而是使用一种抽象的可移植机器代码,但生成的代码加载速度更快,并且正在更积极的开发中。

OTOH,如果您想手动完成所有操作,只需使用 gcc,编译 a.so然后自己加载它,dlopen()然后dlclose()执行您想要的操作。

于 2012-09-07T13:47:20.550 回答
2

您确定 C 是正确的答案吗?有各种解释语言,例如 Lua、Bigloo Scheme,甚至可能是 Python,它们都很好地嵌入到了现有的 C 应用程序中。您可以使用扩展语言编写动态部分,这将支持在运行时重新加载代码。

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

于 2012-09-07T13:47:24.773 回答
1

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

要在内存中编译你的库,我认为你能做的最好的事情就是创建内存文件系统,如此处所述

于 2012-09-07T15:40:58.857 回答