在 C 中创建模块系统(动态加载)

Posted

技术标签:

【中文标题】在 C 中创建模块系统(动态加载)【英文标题】:Creating a module system (dynamic loading) in C 【发布时间】:2010-09-27 21:37:05 【问题描述】:

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

编辑:加载模块的程序是 C 语言。

【问题讨论】:

很好的问题。很多人都知道如何做到这一点,但那些不知道的人最好学习这种有价值的技术。 【参考方案1】:

在 Linux/UNIX 中,您可以使用 POSIX dlopen / dlsym / dlerror / dlclose 函数来动态打开共享库并访问它们提供的符号(包括函数),请参阅man page详情。

【讨论】:

poco库的原理是这样的吗?【参考方案2】:

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

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

大多数操作系统都允许动态链接,这一切都会为您完成。

【讨论】:

将可执行文件读入内存、正确设置所有保护设置并找到正确的符号是很困难的。既然有标准的操作系统功能可以为您做得更好,为什么还要重新发明***? 关于“将文件内容读入内存,使所述内存可执行”的部分涵盖了很多内容,因为在加载时通​​常会有很多重定位和代码调整。我真的试过一次。不适合懦夫。【参考方案3】:

像 Perl 这样的动态语言一直都在这样做。 Perl 解释器是用 C 编写的,许多 Perl 模块部分是用 C 编写的。当需要这些模块时,编译后的 C 组件会动态加载。如另一个答案中所述,存储这些模块的机制是 Windows 上的 DLL,以及 UNIX 上的共享库(.so 文件)。我相信在 UNIX 上加载共享库的调用是 dlopen()。从该调用的文档开始,您可能会找到有关如何在 UNIX 上完成此操作的指针。对于 Windows,您需要研究 DLL 并了解如何在运行时动态加载它们。 [或者可能通过 Cygwin UNIX 仿真层,这可能允许您在 Windows 上使用与在 UNIX 上相同的调用,但我不建议您这样做,除非您已经在使用 Cygwin 并针对 Cygwin 进行编译。]

请注意,这与仅链接到共享库不同。如果您提前知道您将调用什么代码,您可以针对共享库进行构建,并且构建将“动态链接”到该库;无需您进行任何特殊处理,仅当您的程序实际调用它们时,库中的例程才会加载到内存中。但是,如果您打算编写能够加载任何任意目标代码的东西,那么您就不能这样做,这些代码您现在无法在构建时识别,而是等待被选中不知何故在运行时。为此,您必须使用 dlopen() 及其 Windows 表亲。

您可能会查看 Perl 或其他动态语言执行此操作的方式以查看一些真实示例。负责这种动态加载的 Perl 库是 DynaLoader;我相信它同时具有 Perl 和 C 组件。我敢肯定,像 Python 这样的其他动态语言也有类似的东西,你可能会喜欢看; Parrot,未发布的 Perl 6 的虚拟机,肯定也有这样做的机制(或者将来会这样做)。

就此而言,Java 通过其 JNI(Java 本地接口)接口实现这一点,因此您可能可以查看 OpenJDK 的源代码以了解 Java 如何在 UNIX 和 Windows 上实现这一点。

【讨论】:

【参考方案4】:

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 ());

您可以检索完整的示例on line。

【讨论】:

你介意把完整的例子放到 github 上吗?在那里阅读会更容易。 如果使用 c++ 编译器,在使用dlsym 时使用错位字符串函数名是否标准?或函数上的extern "c" 仅使用dlsym 上的普通函数名称?【参考方案5】:

看到此问题已得到解答,但认为其他对此主题感兴趣的人可能会欣赏基于旧插件的应用程序的跨平台示例。该示例适用于 win32 或 linux,并在文件参数中指定的动态加载的 .so 或 .dll 中搜索并调用名为“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);

【讨论】:

在 Linux 上运行需要哪些头文件? '::' 意味着它是 C++,而不是 C,不是吗?【参考方案6】:

您也可以查看cpluff。是纯c上的插件管理库。

【讨论】:

【参考方案7】:

在 Windows 下,我是这样做的:

生成代码(使用 C 语言,因为它很容易找到编译器,并且库要求最低) 生成作业以将其编译/链接到 DLL 中 用 LoadLibrary 加载它 使用 GetProcAddress 获取函数指针

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

【讨论】:

【参考方案8】:

如果您愿意考虑该框架,Qt 提供 QPluginLoader:Qt 5 docs(或者对于旧的 Qt 4.8 文档,请参阅 here)

如果您需要/想要更细粒度的控制,Qt 还提供了一种使用 QLibrary 动态加载库的方法:Qt 5 docs(或者对于旧的 Qt 4.8 文档,请参阅 here)

更好的是,它们可以跨平台移植。

【讨论】:

【参考方案9】:

适用于 GNU/Linux 用户

动态加载库是一种机制,我们可以使用它运行我们的程序,并在运行时决定我们要使用/调用什么函数。我认为在某些情况下static 变量也是可能的。

第一次看到man 3 dlopen 或see it online

所需的头文件是:dlfcn,由于这不是标准的一部分,您应该将其链接到使用此库的目标文件:libdl.(so/a),因此您需要一些东西喜欢:

gcc yours.c -ldl

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


一个完整的例子:

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

func.c

int func1()
    return 1;

func2.c

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

现在我们有 2 个函数,让我们制作我们的模块:

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

让我们创建想要使用这些功能的主程序 (= 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 程序不知道在哪里可以找到相关库:libfunc.so,因此它告诉我们cannot not open ...

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

    使用ld链接器 使用环境变量LD_LIBRARY_PATH 使用标准路径

第一种方式,在ld的帮助下

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

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,因此您可以将其复制到库的标准路径中。

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 知道它的路径? 简单:

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 编程书籍。

【讨论】:

不错!根据 dlopen 手册页“如果文件名包含斜杠(“/”),则有第 4 种方式,则将其解释为(相对或绝对)路径名。所以'handle = dlopen("./libfunc.so", RTLD_LAZY);'允许按照描述进行编译,只需成功执行“./a.out”即可,无需执行任何其他操作。

以上是关于在 C 中创建模块系统(动态加载)的主要内容,如果未能解决你的问题,请参考以下文章

我们可以像在 Hive 中一样在 Big Query 中创建动态分区吗?

如何在cordova中动态加载CSS

如何在AngularJS中动态添加组件?

在 C++ 中创建动态类型

C/C++:std::thread构造函数死锁问题:WIN32下不可以在DllMain中创建线程

如何实现加载c ++的动态模块(在头文件中)