在C语言中调用一个存储在字符串变量中的函数

64

我想使用一个变量调用一个函数。在C语言中是否可能实现这一点?

实际上,我的意图是从用户处获取函数名称并将其存储在变量中。现在,我想要调用具有已存储名称的函数。请问如何在C语言中实现这一点?

我想为一款双人游戏开发一个人工智能游戏引擎。两个没有main函数的程序将被提供给游戏引擎,它们实现了赢得游戏的逻辑。

请注意,程序名称与实现赢得游戏逻辑的主要函数名称相同。因此,当用户输入第一和第二玩家的名称时,我可以将它们存储在2个不同的变量中。现在,由于主函数名与程序名相同,我打算使用包含程序名的变量来调用函数。


3
我不认为这是可能的,但你能详细说明一下为什么想要这样做吗?即使你可以,这听起来很危险。 - Thomas Owens
你想要实现什么,你的目标操作系统/编译器是什么? - AndersK
7
当他回答时,那将是非常特别的。 - John Saunders
@thomas owens,既然我已经详细说明了为什么我想要做这个...你能帮忙吗?有人建议我使用查找表,但是这不起作用,因为用户可以自由命名函数和文件。在他输入名称后,我应该能够根据他输入的名称调用函数,并且包含同名函数的文件。 - wantobegeek
12个回答

42

C语言不支持这种操作(具有反射的语言可以)。你最好能做的就是创建一个从函数名到函数指针的查找表,并使用它来确定调用哪个函数。或者你可以使用switch语句。


1
是的,没有内置功能可以做到这一点,但你可以自己制作一个。 - Jonas
同样的答案,但我有代码,所以我决定留下它。 - GManNickG
9
你在谈论哪个“开关”? - Bogdan Alexandru

39

你最好做的就是像这样:

#include <stdio.h>

// functions
void foo(int i);
void bar(int i);

// function type
typedef void (*FunctionCallback)(int);
FunctionCallback functions[] = {&foo, &bar};

int main(void)
{
    // get function id
    int i = 0;
    scanf("%i", &i);

    // check id
    if( i >= sizeof(functions))
    {
        printf("Invalid function id: %i", i);
        return 1;
    }

    // call function
    functions[i](i);

    return 0;
}

void foo(int i)
{
    printf("In foo() with: %i", i);
}

void bar(int i)
{
    printf("In bar() with: %i", i);
}

这里使用数字而不是字符串来标识函数,但使用字符串只需要将字符串转换为相应的函数即可。

你正在做什么?如果只是出于好奇,那么可以这样做,但如果你试图通过这种方式解决问题,我相信有更适合你任务的方法。

编辑

关于你的编辑,你应该选择onebyone的答案。

你想让用户构建动态库(在Linux中是共享对象[.so],在Windows中是动态链接库[.dll])。

一旦完成了这个步骤,如果他们提供给您他们库的名称,您可以请求操作系统为您加载该库,并请求在该库中查找一个函数的指针。


4
如果你想用这个方法来解决问题,我相信一定有更适合你任务的方法。+1 - John Carter
这是一个稍早的帖子,但是否有任何方法可以像您上面所做的那样,但参数数量不同?我想不出来。 - gnychis
@gnychis:你的意思是参数数量直到运行时才确定吗?(比如,一个回调函数调用可能有3个参数,另一个可能有4个?) - GManNickG

36
虽然这不是一个切实可行的解决方案,但我敢打赌你可以通过让程序读取自己的可执行文件并解析符号表来通过字符串调用函数。符号表应该包含函数的名称以及其第一条指令地址。然后,您可以将此地址放入函数指针变量中并调用它。
我想我可以试着搞定这个问题。
编辑:请不要像这样编写真实的代码,但以下是如何使用字符串调用Linux ELF二进制文件的函数(需要libelf):
#include <fcntl.h>
#include <stdio.h>
#include <elf.h>
#include <libelf.h>
#include <stdlib.h>
#include <string.h>

void callMe() {
  printf("callMe called\n");
}

int main(int argc, char **argv) {
  Elf64_Shdr *    shdr;
  Elf64_Ehdr *    ehdr;
  Elf *        elf;
  Elf_Scn *    scn;
  Elf_Data *    data;
  int cnt;
  void (*fp)() = NULL;

  int fd = 0;

  /* This is probably Linux specific - Read in our own executable*/
  if ((fd = open("/proc/self/exe", O_RDONLY)) == -1)
    exit(1);

  elf_version(EV_CURRENT);

  if ((elf = elf_begin(fd, ELF_C_READ, NULL)) == NULL) {
    fprintf(stderr, "file is not an ELF binary\n");
    exit(1);
  }
    /* Let's get the elf sections */
    if (((ehdr = elf64_getehdr(elf)) == NULL) ||
    ((scn = elf_getscn(elf, ehdr->e_shstrndx)) == NULL) ||
    ((data = elf_getdata(scn, NULL)) == NULL)) {
      fprintf(stderr, "Failed to get SOMETHING\n");
      exit(1);
    }

    /* Let's go through each elf section looking for the symbol table */
    for (cnt = 1, scn = NULL; scn = elf_nextscn(elf, scn); cnt++) {
      if ((shdr = elf64_getshdr(scn)) == NULL)
    exit(1);

      if (shdr->sh_type == SHT_SYMTAB) {
    char *name;
    char *strName;
    data = 0;
    if ((data = elf_getdata(scn, data)) == 0 || data->d_size == 0) {
      fprintf(stderr, "No data in symbol table\n");
      exit(1);
    }

    Elf64_Sym *esym = (Elf64_Sym*) data->d_buf;
    Elf64_Sym *lastsym = (Elf64_Sym*) ((char*) data->d_buf + data->d_size);

    /* Look through all symbols */ 
    for (; esym < lastsym; esym++) {
      if ((esym->st_value == 0) ||
          (ELF64_ST_BIND(esym->st_info)== STB_WEAK) ||
          (ELF64_ST_BIND(esym->st_info)== STB_NUM) ||
          (ELF64_ST_TYPE(esym->st_info)!= STT_FUNC)) 
        continue;

      name = elf_strptr(elf,shdr->sh_link , (size_t)esym->st_name);

      if(!name){
        fprintf(stderr,"%sn",elf_errmsg(elf_errno()));
        exit(-1);
      }
      /* This could obviously be a generic string */
      if(strcmp("callMe", name) == 0 ) {
        printf("Found callMe @ %x\n", esym->st_value);
        fp = esym->st_value;
      }
    }    
    /* Call and hope we don't segfault!*/
    fp();
    elf_end(elf);
    return 0;
  }   

4
你也可以使用 dlopen(0, ...) 和 dlsym(symbol) 替代循环方式。 - Bryan Drewery
7
当你打开/proc/self/exe时,你会知道自己正在做一些特别的事情...我也曾经有过这样的经历;) - Torsten Marek
在编译错误的情况下,请使用fp = (void(*)())(esym->st_value); - Antonin GAVREL

15
#include <stdio.h>
#include <string.h>

void function_a(void) { printf("Function A\n"); }
void function_b(void) { printf("Function B\n"); }
void function_c(void) { printf("Function C\n"); }
void function_d(void) { printf("Function D\n"); }
void function_e(void) { printf("Function E\n"); }

const static struct {
  const char *name;
  void (*func)(void);
} function_map [] = {
  { "function_a", function_a },
  { "function_b", function_b },
  { "function_c", function_c },
  { "function_d", function_d },
  { "function_e", function_e },
};

int call_function(const char *name)
{
  int i;

  for (i = 0; i < (sizeof(function_map) / sizeof(function_map[0])); i++) {
    if (!strcmp(function_map[i].name, name) && function_map[i].func) {
      function_map[i].func();
      return 0;
    }
  }

  return -1;
}

int main()
{
  call_function("function_a");
  call_function("function_c");
  call_function("function_e");
}

15

在纯C语言中是不可能的,但你可以使用dll进行技巧性操作。将所有想要调用的函数放入一个dll文件中,然后使用dlsym(或Windows中的GetProcAddress,或其他系统提供的API)通过名称获取函数指针,并使用该指针进行调用。

这种方法在某些平台上无法工作,因为它们根本没有dll,或者像Symbian一样,dll中的函数只能通过编号而非名称在运行时访问。

请注意,如果用户欺骗您选择了一个参数与您所需调用的不符的函数,则程序将出现错误。C语言确实不适合处理这种情况。


或者使用您的操作系统上可用的任何动态库API。 :) - GManNickG
2
顺带一提,如果你想使用 dlsym 访问函数,它们实际上不必在动态链接库中;你可以以适当的方式链接可执行文件,并将可执行映像作为动态链接库来处理。 - Williham Totland
一个接一个,没错,这就是 dlopen(NULL, 实现的功能(你还需要使用 -rdynamic 选项告诉链接器你想让动态加载器可用的符号)。 - caf

5
我尝试了这个解决方案:使用dlopen()打开可执行文件作为动态库。
在我的Linux Ubuntu上工作得很好,但不幸的是,在我的嵌入式ARM目标上不行。我不知道为什么,可能是glibc或libdl版本的问题。
在我的ARM目标上,消息很清楚:“./test8:无法动态加载可执行文件”。
无论如何,我使用的代码是这个:
我使用以下命令进行编译:
gcc test8.c -ldl -rdynamic
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int TEST_MyTestFunction( char *pArgPos , int Size , char Param )
{
    printf("TEST_MyTestFunction\n");
    return 0;
}

int main(int argc, char **argv)
{
    int ret;
    void *handle;
    char *error;

    int(*func)(char *,int, char);

    handle = dlopen(argv[0], RTLD_LAZY);
    dlerror();

    func = (int(*)(char *,int, char)) dlsym(handle, "TEST_MyTestFunction");

    error = dlerror();

    char *p1 = "param1";
    int p2 = 5;
    int p3 = 'A';

    ret = func(p1, p2, p3);

    return ret;
}

https://dev59.com/zobca4cB1Zd3GeqPT0zW - Motomotes

3
正如其他人所说,C语言确实没有反射机制。但是你可以通过使用动态加载库/共享对象来实现这种行为。事实上,你可以加载一个动态库,然后通过它们的名称调用dll/so中的函数!虽然不是C和操作系统特定的方法,但这是一种可行的方式。在Linux上使用dlopen,在Windows上使用LoadLibrary。你可以找到一些为你完成这项工作的库,比如gtk/glib

2

我刚刚尝试了Steve Jessop的方法,使用静态链接库,正如Williham Totland在评论中建议的那样,结果证明这并不容易。

首先,在网上(包括维基百科)会找到很多地方告诉你,打开主程序作为库的方法是像这样调用dlopen(3):dlopen(NULL, 0)。但对于glibc来说,必须指定一个绑定标志,正如手册中明确说明的那样:

flag参数必须包含下列两个值之一:
RTLD_LAZY
执行懒惰绑定。只有当代码需要访问符号时才解析它们...
RTLD_NOW
如果指定了此值或者环境变量中设置了...

我认为在这里选择哪一个并不重要,因为你将把所有符号从静态库链接到可执行文件中。

这就带来了下一个问题。链接器不会在可执行文件中包含静态库中的符号,因为它们没有被引用。强制GNU链接器包含这些符号的方法是使用-Wl,--whole-archive path/to/static/lib.a -Wl,--no-whole-archive,正如那个答案所描述的。强制Mac OS X链接器包含静态库中所有符号的方法是使用-Wl,-force_load path/to/static/lib.a


1

你是想要这个吗:

void foo()
{
    printf("foo called\n");
}

void bar()
{
    printf("bar called\n");
}


int main()
{
    char fun[10] = {'\0'};

    printf("Enter function name (max 9 characters):");
    scanf("%s",fun);

    if(strcmpi(fun, "foo") == 0)
    {
    foo();
    }
    else if(strcmpi(fun, "bar") == 0)
    {
    bar();
    }
    else
    {
    printf("Function not found\n");
    }


    return 0;
}

1
如果我正确理解了您的问题,您想要使用后期绑定来调用一个 C 函数。这在 C 中通常是不可能的。符号名称(例如函数名)不会存储在 C 编译器生成的代码中。您可能可以让编译器发出符号,然后使用这些符号来执行后期绑定,但是这种技术因编译器而异,可能并不值得麻烦。
像 C# 和 Java 这样的语言支持反射,使得执行后期绑定变得容易。

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