我一直在尝试在C中制作一个eval函数.
目前,我的想法是String -> function pointer使用所有标准库C函数和我所做的所有函数进行散列,这样我就可以处理函数调用(在已经定义的函数上).
但是,用字符串定义函数(即调用eval("int fun(){return 1;}"))仍然是一个问题,我不知道如何在运行时处理它,有没有人有任何想法?
变量定义似乎不是太大的问题,因为我可以使用另一个哈希var_name -> pointer并在需要变量时使用该指针.
顺便说一句,我不关心性能,我想让这个工作.
vyo*_*yom 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);
Run Code Online (Sandbox Code Playgroud)
(为简洁起见,省略了错误处理)。除了这个基本示例之外,如果想在动态函数中使用主机程序的变量,还需要做更多的工作。如果我有一个变量int N;并且我想使用它,我需要两件事: 在代码字符串中:
... "extern int N;"
Run Code Online (Sandbox Code Playgroud)
告诉tcc:
tcc_add_symbol(S, "N", &N);
Run Code Online (Sandbox Code Playgroud)
同样,还有 API 可以注入宏、打开整个库等。HTH。
试图解析 C 在背后是一个真正的痛苦;但我们已经知道如何解析 C;调用 C 编译器!这里我们将 eval 代码编译成动态库并加载它。
您可能会遇到动态代码在您自己的代码中找不到其他函数或变量的问题;对此的简单解决方案是将除 main() 之外的整个程序编译为库并将动态代码库链接到它。您可以通过将库加载地址设置为仅比主加载地址高几个 K 来避免 -fpic 惩罚。在 Linux 上,库中未解析的符号如果没有被剥离,可以被可执行文件解析,而 glibc 依赖于这个功能;然而,编译器优化有时会成为障碍,因此可能需要总库方法。
下面的示例代码适用于 Linux。这可以通过少量工作应用于其他 Unix,包括 Mac OSX。在 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);
}
Run Code Online (Sandbox Code Playgroud)
这是可能的,但做起来很痛苦。您需要编写将文本作为输入并生成语法树的解析器;那么您需要简化构造(例如,将循环转换为 goto 语句并将表达式简化为只有 1 个操作的单静态赋值)。然后,您需要将语法树中的所有模式与目标机器上执行相同任务的指令序列相匹配。最后,您需要为每条指令选择要使用的寄存器,如有必要,将它们溢出到堆栈中。
简而言之,用 C 编写 eval 的实现是可能的,但是大量的工作需要计算机科学多个领域的大量专业知识和知识。编写编译器的复杂性正是大多数编程语言被解释或使用带有自定义字节码的虚拟机的确切原因。像 clang 和 llvm 这样的工具使这变得容易多了,但它们是用 C++ 编写的,而不是 C。
| 归档时间: |
|
| 查看次数: |
6071 次 |
| 最近记录: |