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

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

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

【问题讨论】:

  • 很好的问题。很多人都知道如何做到这一点,但那些不知道的人最好学习这种有价值的技术。

标签: c frameworks module


【解决方案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 dlopensee 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) 找到它的库?

          1. 使用ld链接器
          2. 使用环境变量LD_LIBRARY_PATH
          3. 使用标准路径

          第一种方式,在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
          

          我们如何在中使用它?
          只要我知道你不能,因为g++ 会破坏函数名称,而gcc 不会,因此你应该使用:例如extern "C" int func1();

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

          【讨论】:

          • 不错!根据 dlopen 手册页“如果文件名包含斜杠(“/”),则有第 4 种方式,则将其解释为(相对或绝对)路径名。所以'handle = dlopen("./libfunc.so", RTLD_LAZY);'允许按照描述进行编译,只需成功执行“./a.out”即可,无需执行任何其他操作。
          猜你喜欢
          • 1970-01-01
          • 2016-05-15
          • 2016-02-07
          • 1970-01-01
          • 2010-09-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-25
          相关资源
          最近更新 更多