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