补充一些 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