在C中创build一个模块系统(dynamic加载)

如何在运行时加载编译的C代码,然后调用其中的函数? 不像简单地调用exec()。

编辑:加载模块的程序是在C.

在Linux / UNIX中,您可以使用POSIX dlopen / dlsym / dlerror / dlclose函数dynamic打开共享库并访问它们提供的符号(包括函数),请参阅手册页以获取详细信息。

dlopen是要走的路。 这里有一些例子:

用dlopen加载一个插件:

 #include <dlfcn.h> ... int main (const int argc, const char *argv[]) { char *plugin_name; char file_name[80]; void *plugin; ... plugin = dlopen(file_name, RTLD_NOW); if (!plugin) { fatal("Cannot load %s: %s", plugin_name, dlerror ()); } 

编译上面的内容:

 % cc -ldl -o program program.o 

然后,假设这个插件的API:

 /* The functions we will find in the plugin */ typedef void (*init_f) (); init_f init; typedef int (*query_f) (); query_f query; 

在插件中查找init()的地址:

 init = dlsym(plugin, "init"); result = dlerror(); if (result) { fatal("Cannot find init in %s: %s", plugin_name, result); } init(); 

用另一个函数query()返回一个值:

 query = dlsym (plugin, "query"); result = dlerror(); if (result) { fatal("Cannot find query in %s: %s", plugin_name, result); } printf("Result of plugin %s is %d\n", plugin_name, query ()); 

你可以在线检索完整的例子。

看到这个问题已被回答,但认为对这个主题感兴趣的其他人可能会喜欢从旧的基于插件的应用程序跨平台的例子。 这个例子在win32或者linux上工作,并且在文件参数中指定的dynamic加载的.so或者.dll文件中search并调用一个名为“constructor”的函数。 这个例子是在c + +中,但程序应该是一样的c。

 //firstly the includes #if !defined WIN32 #include <dlfcn.h> #include <sys/types.h> #else #include <windows.h> #endif //define the plugin's constructor function type named PConst typedef tcnplugin* (*PConst)(tcnplugin*,tcnplugin*,HANDLE); //loads a single specified tcnplugin,allmychildren[0] = null plugin int tcnplugin::loadplugin(char *file) { tcnplugin *hpi; #if defined WIN32 //Load library windows style HINSTANCE hplugin=LoadLibrary(file); if (hplugin != NULL) { PConst pinconstruct = (PConst)GetProcAddress(hplugin,"construct"); #else //Load it nix style void * hplugin=dlopen(file,RTLD_NOW); if (hplugin != NULL) { PConst pinconstruct = (PConst)dlsym(hplugin,"construct"); #endif if (pinconstruct != NULL) { //Try to call constructor function in dynamically loaded file, which returns a pointer to an instance of the plugin's class hpi = pinconstruct(this, this, hstdout); } else { piprintf("Cannot find constructor export in plugin!\n"); return 0; } } else { piprintf("Cannot open plugin!\n"); #if !defined WIN32 perror(dlerror()); #endif return 0; } return addchild(hpi); //add pointer to plugin's class to our list of plugins } 

可能还会提到,如果你想调用的函数模块是用c ++编写的,你必须用extern“C”声明该函数,例如:

 extern "C" pcparport * construct(tcnplugin *tcnptr,tcnplugin *parent) { return new pcparport(tcnptr,parent,"PCPARPORT",0,1); } 

你也可以看看cpluff 。 它是一个纯c上的插件pipe理库。

如果你愿意考虑这个框架,Qt提供了QPluginLoader: Qt 5 docs (或者对于旧的Qt 4.8文档见这里 )

如果您需要/想要更细致的控制,Qt还提供了一种使用QLibrary随时加载库的方法: Qt 5文档 (或者对于旧的Qt 4.8文档,请参阅此处 )

更好的是,这些平台是可移植的。

像Perl这样的dynamic语言一直都在这样做。 Perl解释器是用C语言编写的,很多Perl模块都是用C语言编写的。当需要这些模块时,编译后的C语言组件将被dynamic地加载。 正如另一个答案所指出的那样,存储这些模块的机制是Windows上的DLL,以及UNIX上的共享库(.so文件)。 我相信在UNIX上调用共享库是dlopen()。 您可能可以find指针,指出如何在UNIX上完成此操作,从该调用的文档开始。 对于Windows,您需要研究DLL并学习如何在运行时dynamic加载它们。 [或者可能通过Cygwin UNIX仿真层,这可能会允许您在Windows上使用与在UNIX上相同的调用,但是除非您已经在使用和编译Cygwin,否则我不build议这样做。

请注意,这与链接到共享库不同。 如果您事先知道将要调用哪些代码,则可以针对共享库进行构build,并且构build将“dynamic链接”到该库; 没有任何特殊的处理,只有当程序实际调用它们时,库中的例程才会被加载到内存中。 但是如果您打算编写一些能够加载任意目标代码的代码,那么在编译时就不能识别代码,而是等待在运行时以某种方式被选中。 为此,你将不得不使用dlopen()和它的Windows表亲。

您可以看看Perl或其他dynamic语言这样做的方式来看看一些真实的例子。 负责这种dynamic加载的Perl库是DynaLoader; 它有一个Perl和一个C组件,我相信。 我确定像Python这样的其他dynamic语言也有类似的东西,你可以看看。 而Parrot是未发布的Perl 6的虚拟机,当然也有这样做的机制(或将来)。

为此,Java通过它的JNI(Java本地接口)接口完成了这个任务,所以你可以看看OpenJDK的源代码,看看Java在UNIX和Windows上如何实现这一点。

有一种DIY方法。 虽然这样做的方法(和可能性)因系统而异,但总体思路是打开文件,将文件内容读入内存,使内存可执行,将函数指针初始化为此内存中的有效位置,你在那里。

当然这是假设它只是可执行代码 – 不太可能。 代码可能需要将数据加载到RAM中,并且可能需要空间来存放全局/静态variables。 你可以自己加载这个,但是你需要进入可执行代码并调整其中的所有内存引用。

大多数操作系统允许dynamic链接,这一切都为你做。

在Windows下,这是我如何做到这一点:

  • 生成代码(在C中,因为它很容易find编译器,并且库的需求是最小的)
  • 产生一个工作来编译/链接到一个DLL
  • 用LoadLibrary加载它
  • 用GetProcAddress获取函数指针

生成/编译/链接步骤通常不到一秒钟。

为GNU / Linux用户

dynamic加载库是一个机制,在这个帮助下,我们可以运行我们的程序,并在运行时决定我们要使用哪个函数。 我认为在某些情况下, staticvariables也是可能的。

首先看到man 3 dlopen或在网上看到它

所需的头文件是: dlfcn ,因为这不是标准的一部分,所以你应该喜欢它到这个库的目标文件: libdl.(so/a) ,因此你需要这样的东西:

 gcc yours.c -ldl 

那么你有一个文件名为a.out ,你可以运行它, 它不能正常工作,我会解释它为什么。


一个完整的例子:

首先分别创build两个文件func1.cfunc2.c 。 我们想在运行时调用这些函数。

func.c

 int func1(){ return 1; } 

func2.c

 const char* func2(){ return "upgrading to version 2"; } 

现在我们有两个function,让我们来创build我们的模块:

 ALP ❱ gcc -c -fPIC func1.c ALP ❱ gcc -c -fPIC func2.c ALP ❱ gcc -o libfunc.so -shared -fPIC func1.o func2.o 

询问关于-fPIC => PIC的思想

现在你有一个dynamic library名称: libfunc.so

我们来创build一个想要使用这些函数的主程序(= temp.c )。

头文件

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

和主程序

 int main() { // pointer function to func1 and func2 int ( *f1ptr )(); const char* ( *f2ptr )(); // for pointing to the library void* handle = NULL; // for saving the error messages const char* error_message = NULL; // on error dlopen returns NULL handle = dlopen( "libfunc.so", RTLD_LAZY ); // check for error, if it is NULL if( !handle ) { fprintf( stderr, "dlopen() %s\n", dlerror() ); exit( 1 ); } /* according to the header file: When any of the above functions fails, call this function to return a string describing the error. Each call resets the error string so that a following call returns null. extern char *dlerror (void) __THROW; */ // So, reset the error string, of course we no need to do it just for sure dlerror(); // point to func1 f1ptr = (int (*)()) dlsym( handle, "func1" ); // store the error message to error_message // because it is reseted if we use it directly error_message = dlerror(); if( error_message ) // it means if it is not null { fprintf( stderr, "dlsym() for func1 %s\n", error_message ); dlclose( handle ); exit( 1 ); } // point the func2 f2ptr = (const char* (*)()) dlsym( handle, "func2" ); // store the error message to error_message // because it is reseted if we use it directly error_message = dlerror(); if( error_message ) // it means if it is not null { fprintf( stderr, "dlsym() for func2 %s\n", error_message ); dlclose( handle ); exit( 1 ); } printf( "func1: %d\n", ( *f1ptr )() ); printf( "func2: %s\n", ( *f2ptr )() ); // unload the library dlclose( handle ); // the main return value return 0; } 

现在我们只需要编译这个代码(= temp.c ),因此可以尝试:

 ALP ❱ gcc temp.c -ldl ALP ❱ ./a.out libfunc.so: cannot open shared object file: No such file or directory 

这是行不通的! 为什么容易; 因为我们的a.out程序不知道在哪里find相关的库: libfunc.so ,因此它告诉我们cannot not open ...

如何告诉程序(= a.out )find它的库?

  1. 使用ld连接器
  2. 使用环境variablesLD_LIBRARY_PATH
  3. 使用标准path

第一种方式,在ld帮助下

使用-Wl,-rpath,pwd ,并将path作为参数

 ALP ❱ gcc temp.c -ldl ALP ❱ ./a.out libfunc.so: cannot open shared object file: No such file or directory ALP ❱ pwd /home/shu/codeblock/ALP ALP ❱ gcc temp.c -ldl -Wl,-rpath,/home/shu/codeblock/ALP ALP ❱ ./a.out func1: 1 func2: upgrading to version 2 

第二种方式

 ALP ❱ gcc temp.c -ldl ALP ❱ ./a.out libfunc.so: cannot open shared object file: No such file or direc ALP ❱ export LD_LIBRARY_PATH=$PWD ALP ❱ echo $LD_LIBRARY_PATH /home/shu/codeblock/ALP ALP ❱ ./a.out func1: 1 func2: upgrading to version 2 ALP ❱ export LD_LIBRARY_PATH= ALP ❱ ./a.out libfunc.so: cannot open shared object file: No such file or 

和第三种方式

你有libfunc.so当前path,因此你可以将它复制到库的标准path。

 ALP $ sudo cp libfunc.so /usr/lib ALP ❱ gcc temp.c -ldl ALP ❱ ./a.out func1: 1 func2: upgrading to version 2 

你可以从/usr/lib删除它并使用它。 它是由你决定。

注意

如何发现我们a.out知道它的path?
简单:

 ALP ❱ gcc temp.c -ldl -Wl,-rpath,/home/shu/codeblock/ALP ALP ❱ strings a.out | grep \/ /lib/ld-linux.so.2 /home/shu/codeblock/ALP 

我们如何在c ++中使用它?
只要我知道你不能这样做,因为g++损坏函数名,而gcc不会,所以你应该使用: extern "C" int func1(); 例如。

有关更多详细信息,请参阅手册页和Linux编程书籍。