如何在Linux上热重载共享库

时间:2019-05-28 02:27:52

标签: c++ c dll shared-libraries elf

我正在尝试从Casey Muratori受欢迎的Handmade Hero series中复制一个很酷的技巧。在win32上,Casey能够重新加载DLL,并且仅几毫秒的等待时间就可以看到他的代码更改。

我正在尝试使用dlopen,dlsym,dlclose和stat在linux上复制此行为,但是我遇到了以下行为,并且我直觉我对ELF有一些误解,例如,链接器,或者共享对象的概念。

在win32上,我能够轻松完成他的代码的工作,所以我觉得这是我所缺少的特定于Linux的东西。

我正在使用CMake进行构建,但我并不特别相信CMake是罪魁祸首。

我复制共享库dynamic.so,然后加载它。每当原始共享库的mtime更新时,我都会关闭旧副本的句柄,创建新副本,然后尝试加载新副本。

我想指出的是,我打算在第一次更改后打破循环,因为我只是想弄清楚这一点。


#include <stdio.h>                                                                                                                                                                                                                                                                                                                                                                                                                                   
#include <dlfcn.h>                                                                                                                                                                                                                        
#include <time.h>                                                                                                                                                                                                                         
#include <sys/stat.h>                                                                                                                                                                                                                     
#include <unistd.h>      

void
CopyFile(const char* src, const char* dest)
{
  FILE* fsrc;
  FILE* fdest;
  unsigned char buffer[512];
  size_t bytes;

  fprintf(stderr, "copy from: %s to %s!\n", src, dest);

  fsrc = fopen(src, "rb");
  if ( fsrc == NULL )
    ┆   fprintf(stderr, "failed to open file: %s for reading\n", src);

  fdest = fopen(dest, "wb");
  if ( fdest == NULL )
    ┆   fprintf(stderr, "failed to open file: %s for reading\n", src);

  while ( (bytes = fread(buffer, 1, sizeof(buffer), fsrc)) > 0 )
    {
    ┆   fwrite(buffer, 1, bytes, fdest);
    }

  fclose(fsrc);
  fclose(fdest);

  fprintf(stderr, "copy complete!\n");
}

int main(int argc, char** argv)
{

const char* libpath = "/home/bacon/dynamic.so";
const char* copypath = "/home/bacon/dynamic-copy.so";
CopyFile(libpath, copypath);

void* handle = dlopen(copypath, RTLD_NOW | RTLD_GLOBAL);
if ( handle == NULL )
    fprintf(stderr, "failed to load %s, error = %s\n", copypath, dlerror());

struct stat s;
stat(libpath, &s);
time_t oldtime = s.st_mtime;
while (true)
{
    stat(libpath, &s);
    if ( oldtime != s.st_mtime )
    {
        if ( handle != NULL )
        {
            if ( dlclose(handle) )
                fprintf(stderr, "dlclose failed: %s\n", dlerror());
            else
                handle = NULL;
        }

        CopyFile(libpath, copypath);

        handle = dlopen(copypath, RTLD_NOW | RTLD_GLOBAL);
        if ( handle == NULL )
            fprintf(stderr, "failed to load %s, error = %s\n", copypath, dlerror());

        break;
    }
}
}

对于动态库,任何事情都应该做(示例标头):

#ifndef DYNAMIC_HEADER
#define DYNAMIC_HEADER 1

#define DYNAMIC_API __attribute__ ((visibility("default")))

extern "C" DYNAMIC_API int
Add(int x, int y);

#endif /* DYNAMIC_HEADER */

和源文件:

#include "Dynamic.h"

int
Add(int x, int y)
{
    return x + y;
}

共享库只是提供了一些例程来将一些数字加在一起,并且我已经验证了我能够进行dlopen和dlsym,而无需进行热重装。

我还验证了我的复制例程是否确实复制了共享对象。

我希望最初的dlopen成功,并且dlsym正确链接Add(这样做)。然后,我将编辑Dynamic.cpp,并可能返回x + x + y或其他内容,保存文件并重新编译,并期望while循环在st_mtime中获取更改。

我注意到当我运行代码并进行更新时,我收到了错误消息:

dlopen: file too short

当然,当我对包含共享库的目录执行ls -la时,副本的大小为0。

以某种方式更新stat报告的st_mtime,但是共享对象的实际内容为空?链接器是否锁定共享库并阻止读取?

如果我的代码不是非常错误,我该如何规避这种行为?

我不愿意睡觉和重试,因为这是一个相当瞬时的更新。

1 个答案:

答案 0 :(得分:0)

  

如果我的代码不是完全错误

这是完全错误的:您的代码正在使用(静态)链接程序(由makecmake调用)。

运行make时,它最终会调用:

gcc -shared -o /home/bacon/dynamic.so foo.o bar.o ...

然后,链接程序将执行open("/home/bacon/dynamic.so", O_WRONLY|O_CREAT, ...)(或等效操作),一段时间后将write,最后是close文件。

m_time之后open发生更改后close,您的程序将唤醒,并尝试复制文件。如果您的副本在最终Makefile之前 之前的任何时间发生,那么您可能会得到部分副本(包括包含0字节的部分副本)。

最明显的解决方案是Zsigmond建议的解决方案:您必须修改mv才能将正在观看的文件链接到一个不同文件,然后执行make到最后一个目的地(原子)。

另一种解决方案是使dynamic.so的目标依赖于dynamic.so.done: dynamic.so touch dynamic.so.done ,例如

m_time

在程序中,您将在dynamic.so.done中查看dynamic.so,只有在文件更新后,才执行close的副本(即保证到那时new Date("2019-05-29") Wed May 29 2019 05:30:00 GMT+0530 (India Standard Time) new Date("11111-05-29") Mon May 29 11111 00:00:00 GMT+0530 (India Standard Time) 已经结束。