標籤:

在C語言中,可不可以實現使用一個字元串變數的內容為函數名,調用一個函數?

我想實現的功能主要是:我有一批函數,函數命名是規律性的,比如說ICQ_Process_1{}...ICQ_Process_n{}這樣,我想在程序中自動調用這些函數。

調整下需求吧,比如說函數名是固定幾個字元串的組合,象{ABC,FGH,OPQ,XYZ...},我可以有的組合ABC_OPQ{}、ABC_XYZ{}、FGH_OPQ{}......

程序中會有這些字元串的信息,是否能根據這些字元串,直接定位到對應的函數?

不想查表,維護一個表也蠻麻煩的,不查表的情況下有辦法嗎?


有辦法,可以不用自己編寫查表的代碼,也不用宏。思路大概是,把這一類函數編譯到一個動態鏈接庫裡邊,這樣函數的名字會作為符號保留下來。雖然這個方法不純粹是語言層面上的。具體比如在 Linux 里,可以使用 &,下面舉個例子。

首先你需要在 libfunctions.c 里實現這些函數:

//
// libfunctions.c
//
#include &

void ICQ_Process_1()
{
printf("I am ICQ_Process_1
");
}

void ICQ_Process_n()
{
printf("I am ICQ_Process_n
");
}

void ABC_OPQ()
{
printf("I am ABC_OPQ
");
}

void ABC_XYZ()
{
printf("I am ABC_XYZ
");
}

void FGH_OPQ()
{
printf("I am FGH_OPQ
");
}

然後用以下命令把源代碼 libfunctions.c 編譯成 libfunctions.so

gcc -shared -o libfunctions.so -fPIC libfunctions.c

再在你的主程序的源代碼中,使用 & 中的 dlsym 等從字元串讀取函數的指針:

//
// main.c
//
#include &
#include &

typedef void function_t();

int main(int argc, char const *argv[])
{
void *libfunctions;
function_t *func1, *func2;

libfunctions = dlopen("./libfunctions.so", RTLD_NOW);

func1 = dlsym(libfunctions, "ICQ_Process_1");
func1(); // I am ICQ_Process_1

func2 = dlsym(libfunctions, "ABC_OPQ");
func2(); // I am ABC_OPQ

dlclose(libfunctions);

return EXIT_SUCCESS;
}

用下面的命令編譯主程序:

gcc -L. main.c -ldl -lfunctions -o main.out

這個時候執行 ./main.out 就可以了:


首先你要知道,能夠動態的語言,函數都是假的、是化學成分的。。。它們的動態特性,基本上都是基於字元串為key的哈希表,然後在語言層面上把這些東西包起來。

然而C語言並沒有包裝這個東西,所以你不能在語言層面做到這個。如果你非常想要這麼干,你可以:

  • 淺度包裝為一系列函數。
  • 深度包裝為一系列宏和函數,參考GNU Gtk裡面的GObject的signal系統。
  • 改用C++,部分減輕字元串操作的負擔。
  • 把你的系統分割成宿主和插件,把要調用的東西編譯成動態鏈接庫的插件,這個是有字元串的符號表的。然後在運行時動態載入、按照字元串的名字獲取動態庫裡面的函數地址。

==============

補充:

如果你需要更動態的特性,比如整段代碼都是運行時從字元串變過來的,那就必須在運行時能訪問到語言的編譯器。對於腳本語言,都是一邊編譯一邊運行,自然就能拿到解釋器,所以大都提供了內置的eval能力。對於編譯型語言,你得自己嵌一個,比如tiny C。


遙記二十年前學編程的時候就一直為這個問題困擾(儘管當時用的basic和pascal),後面到大四實習的時候才知道有動態庫這個東東


可以用宏實現。

假設這些函數參數類型個數返回值類型都一樣

struct symtab{

char *name;

void (*pfunc)(int,int);

};

#define DEFFUNC(func)

extern void ##func(int a,intb);

struct symtab _sym_##func={"##func",func};

然後用上面宏申明所有函數。

DEFFUNC(first);

....

DEFFUNC(last);

這樣就建立了一個符號表,起始為_sym_first,結束為_sym_last

遍歷這個表匹配即可


想到了兩種方法。

第一種,把函數指針和函數字面名字綁定到一起。調用的時候需要查表。

struct

{

const char* name;

PFN fn;

};

第二種,使用宏。

#define FN(n) xxx##n


是可以的,請看我的博客文章,裡面用了除了表的兩種方法實現:用C實現JS里eval(「function(arg1, arg2)」)

在 C 語言里, 如何通過輸入函數名字來調用函數? 直接上代碼.

大致有三種方法:

  1. 用函數字典, 缺點是代碼耦合在一起, 無法復用.

#include &
#include & #include &
#include &
void foo() { std::cout &<&< "foo()"; } void boo() { std::cout &<&< "boo()"; } void too() { std::cout &<&< "too()"; } void goo() { std::cout &<&< "goo()"; } int main() { std::map&&> functions;
functions["foo"] = foo;
functions["boo"] = boo;
functions["too"] = too;
functions["goo"] = goo;

std::string func;
std::cin &>&> func;
if (functions.find(func) != functions.end()) {
functions[func]();
}
return 0;
}

  1. 利用 nm 或者 objdump, 在 Makefile 中在編譯階段將符號信息輸出到源代碼里. 缺點是每次在不同的環境里運行都要重新編譯一次.

objs = main.o reflect.o

main: $(objs)
gcc -o $@ $^
nm $@ | awk "BEGIN{ print "#include &"; print "#include "reflect.h""; print "struct sym_table_t gbl_sym_table[]={" } { if(NF==3){print "{"" $$3 "", (void*)0x" $$1 "},"}} END{print "{NULL,NULL} };"}" &> .reflect.real.c
gcc -c .reflect.real.c -o .reflect.real.o
gcc -o $@ $^ .reflect.real.o
nm $@ | awk "BEGIN{ print "#include &"; print "#include "reflect.h""; print "struct sym_table_t gbl_sym_table[]={" } { if(NF==3){print "{"" $$3 "", (void*)0x" $$1 "},"}} END{print "{NULL,NULL} };"}" &> .reflect.real.c
gcc -c .reflect.real.c -o .reflect.real.o
gcc -o $@ $^ .reflect.real.o

以上方法都可以在 Stackoverflow 上找到

  1. 直接到 ELF 里查符號表, 找出函數的名字與值.

方法大致是, 讀取編譯後的程序 (可執行文件也是 ELF), 找到 SHT_SYMTAB(符號表), 然後遍歷符號表, 找到與函數名一樣的符號.

因為現在的 C 語言已經不會在符號前加上下劃線了, 所以可以名字與符號名相同. 以及找到對應的 value, 直接用函數指針保存, 使用即可.

void (*fun)(void) = (void*)sym.st_value;
(*fun)();

所有 extern 函數的符號都會存在可執行文件中, 所以即便是多個模塊的編譯鏈接, 這個函數依然適用.

ELF(Executable and Linking Format)

ELF(Executable and Linking Format) 是一個開放標準, 各種 UNIX 系統的可執行文件都採用 ELF 格式, 它有四種不同的類型:

可重定位的目標文件 (Relocatable, 或者 Object File), Linux 的. o, Windows 的. obj 可執行文件 (Executable), Linux 的. out, Windows 的. exe 共享庫 (Shared Object, 或者 Shared Library), Linux 的. so, Windows 的. DLL 核心轉儲文件 (Core Dump File)a, Linux 下的 core dump


動態鏈接庫啊


C語言實現JS和PHP的eval函數,等同於自己再實現一個和編譯器支持的大部分特性相當的解釋器,這個非常難。

但是,如果只是根據寫死的幾個字元串來判斷要執行哪個函數,倒是可以查表,而且還要對字元串裡面傳入的參數再解析一下。


這就是宏/元編程的思想。


1. 自己建個map,映射字元串名字到函數指針

2.dlsym

3.用Libelf 讀自己的binary(大概了解下elf的section就夠了),用它找函數基地址,然後讀進程的/proc/pid/maps找到自己binary的load的地址,兩者結合可以resolve出函數的實際load地址,然後cast得到的地址到你的函數指針。你是C,不是C++,沒有mangling,所以你binary導出的符號名字不會逆天

C么得反射,所以自己動手豐衣足食


可以用轉移表的概念,參考以下代碼:
#include &
#include &
#define M 4
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
int (*oper_func[])(int, int) = {
add, sub, mul, div
};
char oper_sequence[M][10] = {
"add", "sub", "mul", "div"
};
int main()
{
char oper[10];
int seq;
int a,b;
int result;
int i;
printf("Operator:");
scanf("%s",oper);
printf("a:");
scanf("%d",a);
printf("b:");
scanf("%d",b);
for(i=0; i&


最近用 C 做了一個基於 socket 的類似 Flask 的 Web 後端玩具,在做路由功能時實現過通過字元串調用函數。(項目地址:t01y/J4F)

相關的代碼在 route.c

用法:

  • 先定義一個函數:

void test(int *fd_user) {
send(*fd_user, HTTP_RESPONSE(HTTP_STATUS_LINE_OK), sizeof(HTTP_RESPONSE(HTTP_STATUS_LINE_OK))-1, 0);
send(*fd_user, "&hello&", sizeof("&hello&")-1, 0);
}

  • 註冊函數:

route("/test", test);

  • 解析字元串並執行相應函數:

parse(strchr(buff, "/"), stack)(cltfd);

實現過程:

這裡我用鏈表來存儲函數地址, 通過 route 函數來構建鏈表, 通過 parse 函數來解析鏈表, 並返回函數地址以調用函數。

  • list:

typedef struct path{
struct path *hash[90];
void (*handler)(int*);
} st_path, *pst_path;

st_path stack[STACK_LEN];
long sp = -1; // Stack Pointer

  • route 函數

int route(char *c, void (*func)()) {
pst_path this = stack[0]; // Root of list
for(unsigned int n = 0; (*c)(n &< PATH_MAX_LEN); n++) { if(this-&>hash[*c-"!"] == NULL) {
if(sp+1 &< STACK_LEN) { NEW(); this-&>hash[*c-"!"] = stack[sp];
} else {
printf("Stack Overflow!");
return 1;
}
}
this = this-&>hash[*c++ -"!"];
}
this-&>handler = func;
return 0;
}

#define NEW() do{
stack[++sp].handler = NULL;
for(unsigned char i = 0; i&< 90; i++) { stack[sp].hash[i] = NULL; } }while(0);

  • parse 函數:

typedef void(*func_handle)(int*);

func_handle parse(char *token, pst_path this) {
if((this-&>hash[*token-"!"] != NULL) (*(token+1) != " ")) {
return parse(token+1, this-&>hash[*token-"!"]);
} else if(this-&>hash[*token-"!"]-&>handler != NULL) {
return this-&>hash[*token-"!"]-&>handler;
} else {
return errorPage;
}
return NULL;
}


可以自己實現一套函數調用約定。

吃力不討好,放棄吧。


不就是反射嗎


這種就沒有必要用C來實現了,奇淫技巧,會使代碼不好懂,並且移植性也差,也沒啥意義


寫一個我常用的方法,列個枚舉,然後建表:

enum {
FUNC_ICQ_Process_1,
FUNC_ICQ_Process_2,
FUNC_ICQ_Process_3,
};

void ICQ_Process_1() { printf("ICQ 1
"); }
void ICQ_Process_2() { printf("ICQ 2
"); }
void ICQ_Process_3() { printf("ICQ 3
"); }

void (*functions[])() = {
[FUNC_ICQ_Process_1] = ICQ_Process_1,
[FUNC_ICQ_Process_2] = ICQ_Process_2,
[FUNC_ICQ_Process_3] = ICQ_Process_3,
};

int main() {
functions[FUNC_ICQ_Process_1](); // 調用某一個函數,由於用整數查表,效率為O(1)
return 0;
}


其實包裝一下函數就行了。確保有一個函數名標籤不會因函數編譯而變化。也就是說你能通過函數名對應到函數入口。

但是從原理上來講。這個就是數據結構了。不是原生的函數了。


不可以,因為編譯後的二進位程序雖然還有字元串信息,但是,已經沒有函數名這種鬼東西了。

所以除非你的字元串是靜態不會運行後發生改變的可以用宏實現,否則只能用字元串綁定函數地址來查表了

( 在同一個程序內的話)


代碼層面可以實現,但是不是真正的自省


用回調函數可以實現你的想法。大概是這樣子的思路:1.typedef對應函數指針;2.typedef一個包含這些函數指針成員的結構體或函數指針數組;3.定義一個相應的變數並初始化;3.在程序中運用函數回調。

以上好像不是你的需求???

用宏吧!用宏的字元拼接##可以的!


很可惜,你用的是c而不是Java,PHP,並沒有內置自省功能,你只能在代碼層面模擬自省,查表調用函數咯


推薦閱讀:

C 語言指針怎麼理解?
同樣的數組參數,用sizeof求數組長度為何會產生不同的結果?
國外有什麼優秀的c語言入門教學視頻?
僅用C語言可以構造出Python中Dict那種數據結構嗎?
用c語言怎麼實現把一個文件中所有的字元串進行篩選,重複的字元串只留下一個?

TAG:C編程語言 |