欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

linux动态库编译和使用详细剖析 - 后续

程序员文章站 2022-03-30 13:49:53
动态库后续补充, 本身内容有点多, 这里简单分享一下. 希望有鱼渔 : ) ......

引言 - 也许是修行

  很久以前写过关于动态库科普文章, 废话反正是说了好多. 核心就是在 linux 上面玩了一下 dlopen : )

   linux动态库编译和使用详细剖析 - https://www.cnblogs.com/life2refuel/p/5332358.html

 

  本文是上面文章的补充部分. 因为单纯的 linux 玩还是不太通用 ~

  动态库最简单理解是为了解决操作系统级别的代码复用出现的技术. 现在服务器开发技术中,

几乎不再出现. 首先不好用, 其次多环境中常容易出错. 繁荣期应该在上古时代(2000-2005),  动态库技术

是一个很考验程序员的修养的基本功. (当前服务器主流是静态库, 客户端应该还是被动态库统治) 这里不妨扯一些

  Windows 和 Unix 下动态链接库的区别  https://blog.codingnow.com/2006/11/windows_unix_dynamic_library.html

  (没想到, 当年云风, 也会被上古的 winds 老前辈们 摩擦摩擦 ~.~ )

 

  想深入了解动态库原理, 可以多看几遍 <<程序员自我修养>> and <<高级C/C++ 编译技术>> : )

虽然看完没什么暖用. 但也可以解闷(特别是后面那本小册子)不是吗 ?      

  那开始代码之旅, 不来虚的 ~  

 

前言 - 准备测试环境

  动态库当你面试时候碰到的话, 实在答不上上来. 就别继续说了概念了.  就简单说我'不会', 但我会写会用的很溜.

也许能到 61 分吧.  把这篇文章代码手打出来 : ) 咱们就玩实心的.

先看编译模块 Makefile

main.exe:
    gcc -fPIC -O2 -Wall -shared -o foo.dll foo.c
    gcc -g -Wall -O2 -o main.exe main.c dllso.c -ldl

clean:
    -rm -rf *.o *.so *.exe *.out *.dll

 

待编译成动态库的文件 foo.h foo.c

#ifndef _F_FOO
#define _F_FOO

#include <stdio.h>

#ifndef extern
#   if defined(_MSC_VER)
#       define extern extern __declspec(dllexport)
#   else 
#       define extern extern
#   endif 
#endif//extern

extern void * foo(int hoge);

#undef extern

#endif//_F_FOO

这里构建的 extern 宏, 是不是很飘. 用于解决 cl 和 gcc 对于动态库导出约束不一样.

cl 默认没有 __declspec(dllexport) 就不导出. gcc 默认全部导出, __attribute__((visibility("hidden"))) 可以设置不可见.

#include "foo.h"

void * 
foo(int hoge) {
    static int _id;

    ++_id; // 简单自增长
    printf("foo(%d) = %d\n", hoge, _id);
    return &_id;
}

实现没有什么好说的. 

 

其中动态库协助接口设计 dllso.h 

#ifndef _H_DLLSO
#define _H_DLLSO

//
// ds_create - 构造加载动态库文件 
// path     : 动态库文件路径
// return   : 失败返回 NULL
//
extern void * ds_create(const char * path);

//
// ds_parse - 解析动态库文件, 返回执行函数
// so       : 动态库对象
// name     : 待解析的函数名称
// return   : 返回解析的函数地址, NULL 是失败
//
extern void * ds_parse(void * so, const char * name);

//
// ds_delete - 释放卸载动态库文件
// so       : 动态库对象
// return   : void
//
extern void ds_delete(void * so);

#endif//_H_DLLSO

 

最终测试文件 main.c

#include "dllso.h"
#include <stdio.h>
#include <stdlib.h>

#define _STR_FOO "./foo.dll"

// 简单动态库测试
int main(int argc, char * argv[]) {
    void * so = ds_create(_STR_FOO);
    if (NULL == so) {
        fprintf(stderr, "ds_create %s err", _STR_FOO);
        exit(EXIT_FAILURE);
    }

    void * (* foo)(int) = ds_parse(so, "foo");
    if (NULL == foo) {
        fprintf(stderr, "ds_parse err so = %p\n", so);
        exit(EXIT_FAILURE);
    }

    printf("foo() = %p\n", foo(0));

    ds_delete(so);
    return EXIT_SUCCESS;
}

到这里希望读者理解作者的思路. 动态库处理划分为三部分, 装载, 解析, 卸载.

 

这里扯淡一点, 对于动态库设计处理. winds 离不开 LoadLibrary 函数簇. linux 离不开 dlopen 函数簇.(自行科普)

当前测试核心思路就在 dllso.c

#include "dllso.h"

#if defined(_MSC_VER)
#   include <windows.h>

#define RTLD_LAZY               LOAD_WITH_ALTERED_SEARCH_PATH
#define dlopen(filename, flags) LoadLibraryEx(filename, NULL, flags)
#define dlsym(handle, symbol)   GetProcAddress(handle, symbol)
#define dlclose(handle)         FreeLibrary(handle)

#else
#   include <dlfcn.h>
#endif

//
// ds_create - 构造加载动态库文件 
// path     : 动态库文件路径
// return   : 失败返回 NULL
//
inline void * 
ds_create(const char * path) {
    return dlopen(path, RTLD_LAZY);
}

//
// ds_parse - 解析动态库文件, 返回执行函数
// so       : 动态库对象
// name     : 待解析的函数名称
// return   : 返回解析的函数地址, NULL 是失败
//
inline void * 
ds_parse(void * so, const char * name) {
    return dlsym(so, name);
}

//
// ds_delete - 释放卸载动态库文件
// so       : 动态库对象
// return   : void
//
inline void 
ds_delete(void * so) {
    if (so) {
        dlclose(so);
    }
}

核心思路是在 winds 上面采用最小的代价构建了一个 linux dlopen 操作三部曲.

实现的很一般般. 或者说不痛快, 胜在可用, 代价小.  推荐喜欢但基础一般的朋友可以练习练习多写写.

文章到这里也快尾声了, 算 Over ~

 

正文 - 原地踌躇, 也走到了 中年

  人生的修行, 那么让人不可捉摸. 心无旁贷, 好想有机会再能看看大山中柿子树. 甜甜涩涩的 ~

很幸运你读到这里. 不妨带大家看看 libuv 怎么封装 dll 的. 总的思路都一样, winds 向着 *nix 靠拢. 写的很平稳厚实.

先看基础部分 (dl.c, uv.h, internal.h)

/* Platform-specific definitions for uv_dlopen support. */
#define UV_DYNAMIC FAR WINAPI
typedef struct {
  HMODULE handle;
  char* errmsg;
} uv_lib_t;

static void uv__format_fallback_error(uv_lib_t* lib, int errorno){ DWORD_PTR args[1] = { (DWORD_PTR) errorno }; LPSTR fallback_error = "error: %1!d!"; FormatMessageA(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ARGUMENT_ARRAY | FORMAT_MESSAGE_ALLOCATE_BUFFER, fallback_error, 0, 0, (LPSTR) &lib->errmsg, 0, (va_list*) args); }
static int uv__dlerror(uv_lib_t* lib, const char* filename, DWORD errorno) { DWORD_PTR arg; DWORD res; char* msg; if (lib->errmsg) { LocalFree(lib->errmsg); lib->errmsg = NULL; } if (errorno == 0) return 0; res = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorno, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (LPSTR) &lib->errmsg, 0, NULL); if (!res && GetLastError() == ERROR_MUI_FILE_NOT_FOUND) { res = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorno, 0, (LPSTR) &lib->errmsg, 0, NULL); } if (res && errorno == ERROR_BAD_EXE_FORMAT && strstr(lib->errmsg, "%1")) { msg = lib->errmsg; lib->errmsg = NULL; arg = (DWORD_PTR) filename; res = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_ARGUMENT_ARRAY | FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, (LPSTR) &lib->errmsg, 0, (va_list*) &arg); LocalFree(msg); } if (!res) uv__format_fallback_error(lib, errorno); return -1; }

uv__dlerror 是构造 linux dlerror 前戏.
作者设计的意图是围绕 winds FormatMessageA api 错误处理用法包装.

写的挺漂亮的. 其实还有更好更偷懒的实现方式, 参照基础项目 structc 中的

  stderr https://github.com/wangzhione/structc/blob/master/structc/system/stderr.c

winds 实现 strerror 函数. 用于解决 GetLastError 和 FormatMessage 配合的不爽. 

libuv 写的真好, 不知道 niginx 源码是什么水平, 有机会研究一下.一块分享.

 

随后的代码很轻松和标准

int uv_dlopen(const char* filename, uv_lib_t* lib) {
  WCHAR filename_w[32768];

  lib->handle = NULL;
  lib->errmsg = NULL;

  if (!MultiByteToWideChar(CP_UTF8,
                           0,
                           filename,
                           -1,
                           filename_w,
                           ARRAY_SIZE(filename_w))) {
    return uv__dlerror(lib, filename, GetLastError());
  }

  lib->handle = LoadLibraryExW(filename_w, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
  if (lib->handle == NULL) {
    return uv__dlerror(lib, filename, GetLastError());
  }

  return 0;
}


void uv_dlclose(uv_lib_t* lib) {
  if (lib->errmsg) {
    LocalFree((void*)lib->errmsg);
    lib->errmsg = NULL;
  }

  if (lib->handle) {
    /* Ignore errors. No good way to signal them without leaking memory. */
    FreeLibrary(lib->handle);
    lib->handle = NULL;
  }
}


int uv_dlsym(uv_lib_t* lib, const char* name, void** ptr) {
  *ptr = (void*) GetProcAddress(lib->handle, name);
  return uv__dlerror(lib, "", *ptr ? 0 : GetLastError());
}


const char* uv_dlerror(const uv_lib_t* lib) {
  return lib->errmsg ? lib->errmsg : "no error";
}

其中 uv_dlsym 思路很漂亮. 返回 int 错误类型. 可惜也不是线程安全的.

其中用到的几个宏翻译如下

#define CP_UTF7                   65000       // UTF-7 translation
#define CP_UTF8                   65001       // UTF-8 translation

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

其中 libuv uv_dlclose 实现行为和 linux 原生的 dlclose 原生行为基本一致, 没有对 NULL 判断.

让使用的朋友自己维护 NULL这块性能. 总体而言 libuv dl.c 实现的很舒服.

关于动态库话题简单的讲到这里了. 希望总是要有的 ~ ~

 

后记 - 欢迎指正, 感谢阅读, 错误是难免的.

  青春往事 - http://music.163.com/m/song?id=528723987&userid=16529894

      linux动态库编译和使用详细剖析 - 后续 

  (记 龙泉寺 : )