Re: [问题] dynamic shared library设计问题

楼主: cole945 (跶跶..)   2017-10-08 02:06:33
补充一些 PkmX 没提到的东西和补一个简单点的例子
※ 引述《dreamboat66 (小嫩)》之铭言:
: 假设我expose某函数void * GetInstance(int version);
: 我可能会回传两种type, Type1 or Type2
: 使用者就要用
: auto inst = reinterpret_cast<Type1* or Type2>(GetInstance(version));
而因为 C/C++ 无法在 runtime 知道 type 的细节 (reflection),
所以一般会约定好一致的接口 (API), 遵循一个 main program 已知的接口
来实作, 例如
class TypeCommon {
public:
virtual do_something();
virtual do_anotherthing();
};
class Type1: public TypeCommon { .. }
class Type2: public TypeCommon { .. }
TypeCommon *GetInstance(int version);
你这里的盲点是, 主程式根本不需要知道 Type1, Type2,
想像一下 Firefox 外挂谁都可以写, 而 Firefox 根本不需要知道那些外挂的存在
而主程式只要知道 TypeCommon 的样子, dynamic load 来的 Type1, Type2 不过都是
当作 TypeCommon 在操作, 说穿了就只是基本的 interface/implemation 概念
: 之后就可以呼叫inst->Func1();
: 说到这边我不了解的事情是
: 使用者并没有.so or .lib
: 我的这class Type1 在header里面是不是要按照某一种规范来实作才能做到

: 不需要.so or .lib就能够编译自己的执行档出来
基本上这是 linking 的事, 没有指名道性要用到, linking 时就不需要
: class Type1{
: public:
: 1. 是不是让Type1整个class都只有pure virtual function即可
: virtual void Func() = 0;
如果你明白了主程式不需要知道 Type1 这件事, 其实 Type1 有没有 pure 不重要.
新的问题是, 桥接 主程式和外挂的 TypeCommon 是不是要 pure?
答案是都可以, 但 link 时会有点差, 主要是 member 会被主程式和外挂都用到,
那应该由谁来提供的问题
: 2. 是不是有了非pure的virtual function, 编译的时候就会需要.so or .lib来做link?
: virtual void Func();
不是. 会不会用到是看程式有没有直接用到 Type1, Type2
: 3. 同上
: void Func();
?
: 4. 如果class内有member的话,是不是也要看这member的型态是不是也满足
: 这边要问的条件?
: };
: 5. 还是说根本不是class 本身的问题而是要透过一些compiler关键字来做到?
: dllexport or __attribute之类的?
: 我自己因为只有微薄的windows开发经验 印象中都需要提供.lib给使用者做link
: 但又看到某些产品是可做到需要用到某功能的时候
: 才去server runtime download动态lib下来执行
: 这样为什么他在编译自己执行档时可以不需要.so or .lib一起做编译呢?
: 也不会遇到unresolved external symbol之类找不到定义的问题呢?
: 谢谢
先举一个不是 dynamic load 的例子, 然后我们再把他转成 dlopen 的用法
// plugin.h 提供共同接口
#ifndef __PLUGIN_H
#define __PLUGIN_H
class plugin {
public:
virtual int getNum() = 0;
int sum();
virtual ~plugin();
};
#endif
// plugin.cc
// 这个 plugin 很简单, sum() 回传 123 + 某个值,
// 而每个实作这个 plugin 的人自行定义 getNum()
#include "plugin.h"
int plugin::sum() {
return 123 + getNum();
}
plugin::~plugin() {}
// foo.cc
// foo plugin 实作 getNum 为 111
#include "plugin.h"
#include <iostream>
class foo: public plugin {
public:
int getNum() override {
return 111;
}
virtual ~foo() {
std::cout << "foo deleted" << std::endl;
}
};
extern "C" plugin* new_foo() {
// 提供一个 new foo 的方法
return new foo();
}
// bar.cc
// 同理你可以实作一个 bar, 实作不同的 getNum, 例如 222
// main.cc
#include "plugin.h"
#include <iostream>
#include <dlfcn.h>
extern "C" plugin* new_bar();
extern "C" plugin* new_foo();
int main () {
// 从 main 的观点, 不需要知道 foo 和 bar
plugin *f = new_foo();
plugin *b = new_bar();
// 只要认得 plugin::getNum 和 plugin::sum 就好了
std::cout << f->getNum() << ", " << f->sum() << std::endl;
// 111 234
std::cout << b->getNum() << ", " << b->sum() << std::endl;
// 222 345
delete f; // foo delete
delete b; // bar delete
g++ -std=c++11 -pedantic \
main.cc plugin.cc foo.cc bar.cc -ldl
- - - - - - -
以上就只是单的 C++ code, 应该大致可以理解?
如果是使用 dlopen 呢?
对主程式而言, 一般不会直接使用 new_foo, new_bar,
若每个 plugin 都有自已的 new function, 主程式还要先知道 new function
的名程, 所以可以定一个同名的 new function. 不同的 plugin (.so) 是不同的
link module, 不会有 multiple define 的问题.
// in foo.cc/bar.cc
extern "C" plugin* new_object() {
// 提供一个 new foo 的方法
return new foo();
}
或是 foo.c 如果不限于在 dlopen 时动态加载, 也可以保留原本的 make_foo
再另外定一个 weak alias new_object 给 dlsym 时使用
extern "C" plugin* new_object ()
__attribute__((weak, alias("new_foo")));
// in main.cc
// 用于 new_object 的 function pointer type
extern "C" typedef plugin* (*new_fp)();
plugin *f, *b;
// 分别开启 libfoo, libbar 的 handle
auto fh = dlopen("./libfoo.so", RTLD_LAZY);
auto bh = dlopen("./libbar.so", RTLD_LAZY);
// 固定使用 new_object 找出两个 plugin 的 new function
auto make_foo_fp = (new_fp) dlsym(fh, "new_object");
auto make_bar_fp = (new_fp) dlsym(fh, "new_object");
// 以下的用法其实就与原本大同小异了
f = make_foo_fp();
b = make_bar_fp();
std::cout << f->getNum() << ", " << f->sum() << std::endl;
std::cout << b->getNum() << ", " << b->sum() << std::endl;
delete f;
delete b;
- - - -
# 若使用我上提供到的 weak alias 的做法,
# foo/bar 可以直接与 main link 起来直接使用,
# 也可以编成 shard object 透过 dlopen/dlsym 使用
CFLAGS="-std=c++11 -pedantic -g"
g++ ${CFLAGS} -fpic -shared foo.cc plugin.cc -o libfoo.so
g++ ${CFLAGS} -fpic -shared bar.cc plugin.cc -o libbar.so
g++ ${CFLAGS} plugin.cc main.cc foo.cc bar.cc -ldl
- -
这边有另一个细节上面没有提到.
因为 plugin class 有部份实作, 或本身的 type_info
这个实作应该由谁提供? 例如 foo class 本如果要乎叫 plugin::sum,
那这份 code 应该是主程式 a.out 还是 libfoo.so 提供?
以我上面的子, 其实 main, foo, bar 都会有一份 plugin class 的实作,
这些会有额外不必要的重复. 而若 plugin class 本身 link 进 foo/bar,
会造成维护上的问题, 例如新版程式的 plugin class 改版.
为了避开这问题大至有两种做法.
一) 改成由 main 主程式提供实作
CFLAGS="-std=c++11 -pedantic -g"
g++ ${CFLAGS} -fpic -shared foo.cc -o libfoo.so
g++ ${CFLAGS} -fpic -shared bar.cc -o libbar.so
g++ ${CFLAGS} -rdynamnic plugin.cc main.cc foo.cc bar.cc -ldl
一般在 link 时, 若主式的 function 没有被其他 shared object 使用到,
就不会 export 到 dynamic symbol 中, 若没有被 export 到 dynamic table,
那这个 symbol 就不会被用来解析 dynamic loading. 例如
// foo.c
void test();
void foo() {
test();
}
// main.c
void test() {...}
void foo();
int main () {
foo ();
}
void bar() {... }
$ gcc foo.c -fpic -shared -o libfoo.so
$ gcc main.c -L. -lfoo
这就与 link static library (.a) 时的状况一样,
有可能 libfoo.so 本身不提供 test(), 而是其他 lib, 甚至 main 本身
提供 test() function. 差别只是 test() 会被 export 到 dynamic symbol table
供加载 libfoo.so 时使用.
但使用 dlopen 时, linker 并不会发生有人要使用 test() function.
所以 -rdynamic 在这的用途是告诉 linker, 有看不到的 user 会使用不知道哪个
sybmol, 把所有 symbol 都 export 出去.
不过这样其实就太过头了, 会有不必要的的 symbol 污染. 而且大型专案 symbol
常常会数以万计.
所以另一个做法其实就只是把 plugin 本身也变成 libray让 main, foo,bar 供用
g++ ${CFLAGS} -fpic -shared plugin.cc -o libplugin.so
g++ ${CFLAGS} -fpic -shared foo.cc -L. -lplugin -o libfoo.so
g++ ${CFLAGS} -fpic -shared bar.cc -L. -lplugin -o libbar.so
g++ ${CFLAGS} main.cc -L. -lplugin -ldl
作者: dreamboat66 (小嫩)   2017-10-08 02:37:00
谢谢补充,需要花时间理解,但中间范例改用dlopen后 可以在主程式直接delete f and b吗?不太确定观念但印象是要提供release function 给主程式用
作者: PkmX (阿猫)   2017-10-08 02:42:00
如果你可以保证new_object回传的pointer是new出来的而且主程式call的new/delete和library的完全符合的 是可以的保险起见library会自己提供release的函式给主程式使用因为只有library自己最清楚要如何解构他自己创造出来的物件
作者: dreamboat66 (小嫩)   2017-10-08 02:45:00
是说他们编译用的crt版本实作要一模一样吗?但我有印象曾经有提到 主程式跟lib 他们new出来的内存是配置在不同heap,所以你不能帮他delete会找不到之类的,是我记错吗还是有条件
作者: PkmX (阿猫)   2017-10-08 02:49:00
如果主程式/library 去重载 operator new/delete 就有可能不过这个还是回归到两边的new/delete 不 compatible 的问题
楼主: cole945 (跶跶..)   2017-10-09 15:02:00
dreamboat66, 如你所提, 我这样的写法其实比较不好,API设计上应该是谁 allocate 出来的, 也要题供对应的deallocate, 或是应该要在 API 规范上讲明应如何 delete若没有讲明的话, 难保new_object会不会改变allocate的方式. 例如new/malloc/或自带heap pool.这个例子主要是demo dlopen的部份, 所以就省delete_obj省得太多code干扰主要的例子 :)其实上面PkmX也帮忙解释了..XD正常来说libc 或 c++ runtime 不会自带, 通常是dynamiclink系统环境提供的, 所以 lib/main 的new/delete会相容反过来说, 如果不是独立的程式, 其实不建议 static linkC/C++ runtime. 例如 staic link -ldl 会有warning
作者: dreamboat66 (小嫩)   2017-10-10 10:48:00
所以exe跟dll会allicate在不同的heap这讲法是错的吗

Links booklink

Contact Us: admin [ a t ] ucptt.com