我有一个有点不寻常的问题。假设我有N个函数:
void function1(){ //这里有一些代码 }void function2(){ //这里有一些代码 }...void functionN(){ //这里有一些代码 }
有没有一种方法,可以在不使用IF语句的情况下动态计算或找出应该使用哪个函数?并且根据函数名称来调用它?让我用伪代码来更好地描述这种情况:
for(int I=1;I<=N;I++){ functionI(); }
我的意思是,是否有可能计算出(例如在一个字符数组中,或者通过其他任何方式)某种代码,然后我可以稍后插入并使用它。但不是作为字符串,而是直接作为代码。让我用另一个例子来演示:
int num=3;char functionInString[]="printf(num);//一些代码,可能会改变functionInString中的字母,例如转换为由不同字母组成的函数//然后,在这里执行functionToString中所写的内容
如果我的描述不够清楚,我很抱歉。有人能告诉我,在C语言或其他任何语言中是否有可能实现这个功能,以及这个概念叫什么吗?
回答:
你可能需要了解什么是闭包和回调。函数指针很有用,但单独使用可能不够(阅读更多内容以了解函数指针如何用于实现闭包)。
你应该学习更多关于C语言的知识,所以阅读一些关于C编程的好书。我推荐下载C11标准的n1570并浏览一下。非常重要的一个概念是未定义行为,你应该对它感到害怕。
是否有可能计算出(例如在一个字符数组中,或者通过其他任何方式)某种代码,然后我可以稍后插入并使用它。但不是作为字符串,而是直接作为代码。
这在纯粹的标准C代码中是不可能的(因为组成你的程序的翻译单元集是固定的),而且原则上任何有效的函数指针值都应该指向某个现有的函数(所以严格意义上调用任何其他函数指针值将是未定义行为)。然而,一些实现能够以某种特定于实现的方式构造出某种“有效”的新函数指针(但这是在C11标准之外的)。在纯粹的哈佛架构上,代码位于ROM中,这甚至是不可能的。
生成和动态加载插件
然而,如果你在现代操作系统下运行(我推荐Linux),你可以使用动态加载和插件设施。我特别关注Linux(对于Windows,细节非常不同,细节中的邪恶;阅读Levine的Linkers and Loaders书)。阅读Operating Systems: Three Easy Pieces以了解更多关于操作系统的知识。
所以你可以在Linux上做的是,在运行时生成一些C代码到某个临时文件中,例如/tmp/generated.c
;你需要定义关于该文件的约定(例如,它定义了一个void pluginfun(int)
函数,具有某些额外的理想属性);然后你可以fork一个编译命令来将它编译成一个共享对象(阅读How to write shared libraries by Drepper),也就是一个插件。因此你的程序可能会运行(可能使用system(3),或更低级别的系统调用如fork(2),execve(2),waitpid(2)等…)一个编译过程,例如gcc -Wall -O -fPIC /tmp/generated.c -shared -o /tmp/generatedplugin.so
;之后,你的主程序将使用dlopen(3)加载该插件:
void* dlh = dlopen("/tmp/generatedplugin.so", RTLD_NOW);if (!dlh) { fprintf(stderr, "dlopen /tmp/generatedplugin.so failed: %s\n", dlerror()); exit(EXIT_FAILURE);}
由于你关心函数指针,如果你将它们的签名声明为一个类型,你的代码会更易读:
typedef void pluginfun_type(int);
然后可以轻松声明指向该签名函数的函数指针:
pluginfun_type* fptr = NULL;
你的程序可以使用dlsym(3)获取插件中pluginfun
函数的地址:
fptr = (pluginfun_type*) dlsym(dlh, "pluginfun");if (!fptr) { fprintf(stderr, "dlsym of pluginfun failed: %s\n", dlerror()); exit(EXIT_FAILURE);}
最后使用(*fptr)(3)
来调用该函数。
你应该使用gcc -O -Wall foo.o bar.o -rdynamic -ldl -o foobarprog
来链接你的主程序。你可能最后会调用dlclose(3),但如果你在插件内仍有活动的调用框架,你不应该这样做。
这种生成插件的方法在Linux上非常有效。我的manydl.c是一个玩具程序,生成“随机”的C代码,fork一个编译过程将生成的C代码编译成生成的插件,并加载该插件(并且可以重复多次)。它显示在实践中,一个Linux程序可以生成并加载数十万(甚至数百万,如果你有足够的耐心)插件。代码段泄漏在实践中是可以接受的(可以通过小心使用dlclose
来避免)。
现在的计算机非常快。你可以在每次REPL交互时生成一个小插件(少于一千行C代码),并编译它并加载它(对于这样一个小插件,通常不到0.1秒),这实际上与人类交互兼容(我在我已经过时的GCC MELT项目中这样做过;我将在我的bismon项目中这样做,该项目在bismon-chariot-doc.pdf草案报告中有描述)。
使用JIT编译库
为了在运行时动态生成代码,你也可以使用一些JIT编译库。它们有很多,包括libgccjit,LLVM(用C++编写),asmjit,libjit,GNUlightning,tinycc及其libtcc
。其中一些能够快速生成机器代码,但生成的代码性能可能不是很好。其他则像一个好的C编译器一样进行优化(特别是libgccjit,它在内部使用GCC)。当然,这些优化需要一些时间,所以机器代码的生成速度会慢一些,但其性能与优化的C代码一样好。
嵌入解释器
在许多情况下,脚本语言提供某种形式的eval。几个解释器被设计为可以轻松嵌入到你的应用程序中(需要注意和警告),特别是Lua或Guile(以及Nim)。还有更多解释器(Ocaml,Python,Perl,Parrot,Ruby,…)也可以以某种方式嵌入到你的应用程序中(你可能需要了解垃圾回收术语)。当然,所有这些都需要一些编码约定。解释器在实践中比编译代码慢。
术语
有人能告诉我,在C语言或其他任何语言中是否有可能实现这个功能,以及这个概念叫什么吗?
你可能需要阅读更多关于元编程,eval,多阶段编程,同质性,反射,延续,类型自省,堆栈跟踪(考虑Ian Taylor的libbacktrace)。
你可能对Lisp类语言感兴趣。首先阅读SICP,然后阅读Queinnec的Lisp In Small Pieces书和Scott的Programming Language Pragmatics书。请注意,SBCL(一个Common Lisp实现)在每次REPL交互时生成机器代码。
由于你提到对人工智能感兴趣,你可以查看J.Pitrat的博客。他的CAIA系统是自举的,因此生成其全部C代码(近50万行代码)。