在C语言中如何编写`eval()`函数

19

我一直在尝试制作C语言中的eval函数。

目前,我的想法是使用散列表字符串 -> 函数指针来存放所有标准库C函数和我自己定义的函数,这样就可以处理对已定义函数的调用了。

然而,使用字符串定义函数(例如调用eval("int fun(){return 1;}"))仍然是一个问题,在运行时我不知道该如何处理,有没有人有什么想法?

变量定义似乎不是太大的问题,因为我可以使用另一个散列表变量名 -> 指针,并在需要时使用该指针。

顺便说一下,我不关心性能,我只想让它工作。


2
Stackoverflow通常用于相对较小、具体和特定的问题。例如CA编译器错误,帮助找出代码崩溃的原因等。这是一个非常广泛的问题,可能涉及从编译器和解析器理论到解释器和字节码工作的所有内容。请阅读"我可以在这里问什么样的话题?""我应该避免问哪些类型的问题?"这两个部分的内容,来自帮助页面 - Some programmer dude
3
我投票支持重新开放这个问题,因为我知道答案,并不是“你不能”。答案不涉及编写编译器或解析C甚至不涉及字节码。实际上很简单。 - Joshua
2
@Joshua,你能在评论中给我们一个概述吗?如果好的话,我也会投票重新开放。 - Some programmer dude
2
@joshua:好的,我投票支持重新开放。我很好奇你如何在不解析原型的情况下调用已编译函数的解决方案。 - rici
3
XY问题?你为什么需要它?嵌入C的动态语言数不胜数,为什么要自己开发一个并让它像C一样呢? - n. m.
显示剩余13条评论
4个回答

10
几周前,我想做类似的事情,这是我遇到的第一个问题,因此现在回答这里,现在我对此有些掌握了 :) 我很惊讶没有人提到tcc(特别是libtcc),它可以让您从字符串编译代码并调用所定义的函数。例如:
int (*sqr)(int) = NULL;
TCCState *S = tcc_new();

tcc_set_output_type(S, TCC_OUTPUT_MEMORY);
tcc_compile_string(S, "int squarer(int x) { return x*x; }");
tcc_relocate(S, TCC_RELOCATE_AUTO);
sqr = tcc_get_symbol(S, "func");

printf("%d", sqr(2));
tcc_delete(S);

(出于简洁起见,错误处理被省略)。除了这个基本示例,如果想在动态函数中使用主程序的变量,则需要进行更多的工作。如果我有一个变量int N;并且我想使用它,我需要两件事情:

 ... "extern int N;"

告诉 tcc:
tcc_add_symbol(S, "N", &N);

同样地,有API可以注入宏、打开整个库等。希望对你有所帮助。

9
尝试解析C语言真是一项非常痛苦的工作;但我们已经知道如何解析C语言了;调用C编译器!在这里,我们将评估代码编译成动态库并加载它。
您可能会遇到问题,即您的动态代码无法在自己的代码中找到其他函数或变量。解决这个问题的简单方法是将整个程序编译为库(除了main()),并将动态代码库与其链接。您可以通过仅将库加载地址设置为主加载地址之上的几K来避免-fpic惩罚。在Linux中,如果未被剥离,则可以通过可执行文件解析库中未解决的符号,并且glibc依赖于此功能;但是,有时编译器优化会妨碍该功能,因此可能需要使用总库方法。
以下示例代码适用于Linux系统。这可以针对其他Unix系统进行少量修改以适配Mac OS X系统。尝试在Windows上实现也是可能的,但更难,因为您没有保证会有一个C编译器,除非您愿意提供一个;而且在Windows上有关多个C运行时的讨厌规则,因此您必须使用相同的编译器构建和发布,否则主程序中的符号将无法解析到库中(PE文件格式无法表达所需的功能)。
此示例代码不提供eval()代码保存状态的任何方式;如果您需要这样做,可以通过主程序中的变量或(首选)通过地址传递状态结构来实现。
如果您正在尝试在嵌入式环境中执行此操作,请勿如此。在嵌入式领域,这是一个坏主意。
回答rici的评论;我从未见过eval()块的参数类型和返回类型未从周围代码静态确定的情况;否则,您怎么能调用它呢?下面的示例代码可以拆分,提取共享部分,因此每种类型的部分仅有几行代码;练习留给读者完成。
如果您没有特定原因需要动态C,请尝试使用具有明确定义接口的嵌入式LUA。
/* gcc -o dload dload.c -ldl */

#include <dlfcn.h>
#include <stdio.h>

typedef void (*fevalvd)(int arg);

/* We need one of these per function signature */
/* Disclaimer: does not support currying; attempting to return functions -> undefined behavior */
/* The function to be called must be named fctn or this does not work. */
void evalvd(const char *function, int arg)
{
        char buf1[50];
        char buf2[50];
        char buf3[100];
        void *ctr;
        fevalvd fc;
        snprintf(buf1, 50, "/tmp/dl%d.c", getpid());
        snprintf(buf2, 50, "/tmp/libdl%d.so", getpid());
        FILE *f = fopen(buf1, "w");
        if (!f) { fprintf (stderr, "can't open temp file\n"); }
        fprintf(f, "%s", function);
        fclose(f);
        snprintf(buf3, 100, "gcc -shared -fpic -o %s %s", buf2, buf1);
        if (system(buf3)) { unlink(buf1); return ; /* oops */ }

        ctr = dlopen(buf2, RTLD_NOW | RTLD_LOCAL);
        if (!ctr) { fprintf(stderr, "can't open\n"); unlink(buf1); unlink(buf2); return ; }
        fc = (fevalvd)dlsym(ctr, "fctn");
        if (fc) {
                fc(arg);
        } else {
                fprintf(stderr, "Can't find fctn in dynamic code\n");
        }
        dlclose(ctr);
        unlink(buf2);
        unlink(buf1);
}

int main(int argc, char **argv)
{
        evalvd("#include <stdio.h>\nvoid fctn(int a) { printf(\"%d\\n\", a); }\n", 10);
}

正如在注释中所提到的,这似乎是一种绕路的方法,并且需要比真正的“eval”(动态解析代码,仅需要(可能是大量的)内存)更多的系统权限(创建文件,运行gcc等)。 - YoTengoUnLCD
10 是 evalvd 函数的第二个参数;这确实是预期的行为。 - Joshua
1
让我重新表述一下,你的缩进行为不是在任何常规的 eval 函数中,为什么要调用一个刚刚在那段代码中被定义的函数? - YoTengoUnLCD
在我的情况下(macOS),编译需要包含#include <stdlib.h>#include <unistd.h>。对我来说运行得非常完美,谢谢。 - user9869932
在gcc中,库dl的含义是什么? - Rick
显示剩余2条评论

5
可以实现,但是实现过程会很繁琐。你需要编写解析器,将文本作为输入,并生成语法树;然后需要简化结构(例如将循环转换为goto语句,并将表达式简化为单一静态分配只有1个操作的表达式)。接下来,需要将语法树中的所有模式与目标机器上执行相同任务的指令序列进行匹配。最后,需要选择用于这些指令的寄存器,必要时将它们溢出到堆栈中。
简而言之,在C语言中编写eval实现是可能的,但需要大量工作和在计算机科学的多个领域内具备广泛的专业知识。编写编译器的复杂性正是绝大多数编程语言采用解释器或使用自定义字节码的虚拟机的精确原因。像clang和llvm这样的工具使这变得更加容易,但它们是用C++编写的,而不是C。

仍然存在调用函数的问题。您可能需要查看Foreign Function Interface库(libffi或在Github上libffi)。或者可能有其他方法来完成这项工作。 - Jonathan Leffler
@JonathanLeffler 我本来想提到libffi,但我认为他可能可以读出我的意思,即他正在尝试做的事情完全不切实际。像其他人一样,他最好使用Lua或Python。 - DeftlyHacked

0
考虑到一些限制,使用OpenCL可能是在C/C++中实现eval的一种可能方式。一旦您的OpenCL实现提供了编译内核和执行内核的能力,无论在CPU还是GPU(或其他“加速器”设备)上,这意味着您可以在C/C++应用程序运行时生成内核代码字符串,将其编译并排队执行。此外,OpenCL API提供了查找内核编译、链接和执行错误的能力。因此,请看看OpenCL。

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