您想要做的事情是合理的,而我正是在 MELT(一种用于扩展 GCC 的高级域特定语言;MELT 编译为 C,通过一个用 MELT 编写的翻译器本身)。
首先,在生成 C 代码(或许多其他源语言)时,一个好的建议是在内存中保留某种 abstract syntax tree (AST)。因此,首先构建生成的 C 代码的整个 AST,然后将其作为 C 语法发出。不要认为你的代码生成框架没有明确的 AST(换句话说,用一堆 printf 生成 C 代码是一场维护噩梦,你想要一些中间表示)。
其次,生成 C 代码的主要原因是利用良好的优化编译器(另一个原因是 C 的可移植性和普遍性)。如果您不关心生成代码的性能(并且 TCC 非常快地将 C 编译成非常幼稚和缓慢的机器代码),您可以使用其他一些方法,例如使用一些 JIT 库,如 Gnu lightning(非常快速地生成慢速机器代码)、Gnu Libjit 或 ASMJIT(生成的机器代码更好一点)、LLVM 或 GCCJIT(生成良好的机器代码,但生成时间与编译器相当)。
因此,如果您生成 C 代码并希望它快速运行,则 C 代码的编译时间不可忽略(因为您可能会 fork 一个 gcc -O -fPIC -shared 命令从生成的 @ 中生成一些共享对象 foo.so 987654335@)。根据经验,生成 C 代码比编译它(使用gcc -O)花费的时间要少得多。在 MELT 中,C 代码的生成速度比 GCC 编译快 10 倍以上(通常快 30 倍)。但是 C 编译器所做的优化是值得的。
一旦你发布了你的 C 代码,将它的编译分叉成一个.so 共享对象,你就可以dlopen 它。不要害羞,我的manydl.c 示例演示了在 Linux 上您可以 dlopen 大量共享对象(数十万个)。真正的瓶颈是生成的 C 代码的编译。在实践中,在 Linux 上您实际上并不需要 dlclose(除非您正在编写需要运行数月的服务器程序);一个未使用的共享模块实际上可以保留dlopen-ed,并且您主要是在泄漏进程地址空间(这是一种廉价资源),因为大部分未使用的.so 将被换出。 dlopen 做的很快,需要时间的是编译一个 C 源代码,因为你真的希望由 C 编译器完成优化。
您可以使用许多其他不同的方法,例如有一个字节码解释器并为该字节码生成,使用 Common 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 函数,这些函数将在dlopen 的generated*.so 时间执行
您的基础应用程序应该已经定义了关于dlsym-ed 名称集(通常是函数)以及如何调用它们的约定。它应该只直接调用您的 generated*.so 到 dlsym-ed 函数指针中的函数。例如,在实践中,您会决定每个generated*.c 定义一个函数void dynfoo(int) 和int dynbar(int,int),并将dlsym 与"dynfoo" 和"dynbar" 一起使用,并调用这些直通函数指针(由dlsym 返回)。您还应该定义如何以及何时调用这些dynfoo 和dynbar 的约定。您最好将您的基础应用程序与-rdynamic 链接起来,以便您的generated*.c 文件可以调用您的应用程序函数。
您不希望您的generated*.so重新定义 现有名称。例如,您不想在 generated*.c 中重新定义 malloc 并期望所有堆分配函数都能神奇地使用您的新变体(这可能行不通,即使这样做也会很危险) .
您可能不会打扰dlclose 动态加载的共享对象,除非在应用程序清理和退出时(但我根本不会打扰dlclose)。如果您执行dlclose 一些动态加载的generated*.so 文件,请确保其中没有使用任何内容:没有指针,甚至调用帧中的返回地址都不存在。
附: MELT 翻译器目前将 57KLOC 的 MELT 代码翻译成近 1770KLOC 的 C 代码。