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

9

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

我在Linux上,怎么做呢?

在Linux上,可以重新编译和重新加载库文件.so吗?

是否可以在不生成.so文件的情况下进行编译,将编译后的输出存储在内存中,然后重新加载?我希望能够快速重新加载已编译的代码。


你确定时间是如此重要的限制因素吗?(你测量过吗?) - Basile Starynkevitch
我很想知道你为什么这样问,以及你正在编写什么类型的应用程序! - Basile Starynkevitch
我正在编写一种使用Goto语句和全局变量的语言。我相信函数隐藏了函数的实现细节(封装),而面向对象则封装了数据。我反对隐藏任何东西,并认为人们在“goto是邪恶的”论文之后没有尝试过Goto和全局变量。无论如何,我不会在这里再争论这个问题!哈哈...有点疯狂。所以我选择LLVM,因为我需要Goto,而C/C++不再允许使用Goto(除了标签)。 - Phil
4个回答

13
"What you want to do is reasonable, and I am doing exactly that in MELT (a high-level domain-specific language for extending GCC; MELT is compiled to C via a translator written in MELT itself).
Firstly, when generating C code (or many other source languages), it is advisable to maintain some form of abstract syntax tree (AST) in memory. Therefore, the entire AST of the generated C code should be built first and then emitted as C syntax. Generating C code with only printf statements is difficult to maintain, so an intermediate representation is necessary."

其次,生成C代码的主要原因是为了利用优秀的优化编译器(另一个原因是C的可移植性和普及性)。如果您不关心生成代码的性能(TCC将C编译成非常天真且缓慢的机器码),则可以使用其他方法,例如使用一些JIT库,如Gnu lightning(生成缓慢的机器码非常快速),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上可以加载大量共享对象(数十万个)。真正的瓶颈是生成的C代码的编译。实际上,在Linux上,您不需要真正使用dlclose(除非您正在编写需要运行数月的服务器程序);未使用的共享模块可以保持实际上被dlopen,并且您主要会泄漏进程地址空间(这是一种廉价的资源),因为大多数未使用的.so将被交换出去。dlopen很快,花费时间的是C源代码的编译,因为您确实希望C编译器完成优化。
您可以使用许多其他不同的方法,例如具有字节码解释器并为该字节码生成代码,使用通用Lisp(例如在Linux上编译动态机器代码的SBCL),LuaJit,Java,MetaOcaml等。
根据其他人的建议,您不太需要关心编写C文件的时间,在实践中它会留在文件系统缓存中(也请参阅this)。而且编写它比编译它要快得多,所以留在内存中并不值得麻烦。如果您担心I/O时间,请使用一些tmpfs

补充

您问道:

在Linux上,库文件.so能否在运行时重新编译和重新加载?

当然,是的:你应该派生一个命令来从生成的C代码构建库(例如 gcc -O -fPIC -shared generated.c -o generated.so),但你也可以间接地这样做,例如通过运行 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 文件中有一些 constructor 函数,在 generated*.sodlopen 时间执行它们。

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

您不希望generated*.so重新定义现有名称。例如,您不希望在您的generated*.c中重新定义malloc并期望所有堆分配函数自动使用您的新变量(这可能不起作用,��使起作用也很危险)。

您可能不会费心去dlclose一个动态加载的共享对象,除非在应用程序清理和退出时(但我根本不费心去dlclose)。如果您确实dlclose了一些动态加载的generated*.so文件,请确保没有任何指针,甚至是调用框架中的返回地址存在于其中。

P.S. MELT翻译器目前是57KLOC的MELT代码转换为将近1770KLOC的C代码。


这就是它的吸引之处,C编译器生成良好的机器代码,而且C是一种众所周知且易于理解的语言。学习LLVM代码需要付出一些努力,也许对我来说现在还有点太多了。其他选项不能生成高效的代码(不如C)。 - Phil
这是最好的答案,但最终我选择使用LLVM,因为我需要一些纯C++不提供的功能,即跳转到任意位置的GOTO语句(尽管那是邪恶的)。尽管如此,我认为这仍然是对问题最好的回答。 - Phil

4
你最好的选择可能是TCC编译器,它允许你做到这一点——编译源代码,将其添加到程序中,运行它,所有这些都不需要触碰文件。
对于一个更强大但非基于C的解决方案,你应该查看LLVM项目,它从生成JIT的角度做了很多相同的事情。你不能通过C来进行操作,而是使用一种抽象的可移植机器码,但生成的代码速度更快,并且正在积极开发中。
另一方面,如果你想手动完成所有操作,通过调用gcc,编译.so,然后自己加载它,dlopen()dlclose()会实现你想要的功能。

TCC看起来很有前途,尽管我在网站上看到了这个注释:“注意:我不再从事TCC的工作。请查看邮件列表以获取最新信息。”一方面,我看到他们的git repo有许多最近的更改。但另一方面,最后一次发布是3年前。不太确定这会导致什么结果... - Justin Ethier
邮件列表仍然活跃,但Fabrice Bellard现在正在从事其他工作。嗯...值得指出的是,tcc确实起源于一个模糊的C竞赛参赛者。它是一种宏伟的黑客技术,并且非常擅长做它所做的事情,但它并不漂亮。几年前,我曾非常成功地使用它在一个不支持加载可执行代码的平台上加载可执行代码。 - David Given
但是TCC确实有一个弱点:虽然它的编译时间很快,但生成的机器代码非常慢。(TCC将C代码非常快地编译成慢速的机器代码)。如果生成C代码,则可能需要采用一些不同的方法... - Basile Starynkevitch
所以,如果你关心动态加载的代码快速运行,TCC并不适合你,因为TCC生成的代码很慢(有时比gcc -O生成的代码慢2到3倍)。 - Basile Starynkevitch

2

你确定C语言在这里是正确的答案吗?有许多解释性语言,比如Lua、Bigloo Scheme,甚至可能是Python,它们非常适合嵌入到现有的C应用程序中。您可以使用扩展语言编写动态部分,该语言将支持在运行时重新加载代码。

明显的缺点是性能 - 如果您绝对需要编译后的C语言原始速度,则这些可能不可行。


1
如果你想动态重新加载一个库,可以使用dlopen函数(参见 mans)。它打开一个库.so文件并返回一个void*指针,然后你可以使用dlsym获取库中任何函数/变量的指针。
要在内存中编译你的库,我认为最好的方法是创建一个内存文件系统,如here所述。

1
它是 dlopendlsym(不是 dl_open……)! - Basile Starynkevitch
非常感谢,我总是犯这个错误 :( - Pupkov-Zadnij

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