Re: [問題] dynamic shared library設計問題

看板C_and_CPP作者 (躂躂..)時間6年前 (2017/10/08 02:06), 編輯推噓4(4022)
留言26則, 3人參與, 6年前最新討論串3/4 (看更多)
補充一些 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 -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 114.32.204.230 ※ 文章網址: https://www.ptt.cc/bbs/C_and_CPP/M.1507399596.A.A13.html

10/08 02:37, 6年前 , 1F
謝謝補充,需要花時間理解,但中間範例改用dlopen
10/08 02:37, 1F

10/08 02:37, 6年前 , 2F
後 可以在主程式直接delete f and b嗎?不太確定觀
10/08 02:37, 2F

10/08 02:37, 6年前 , 3F
念但印象是要提供release function 給主程式用
10/08 02:37, 3F

10/08 02:42, 6年前 , 4F
如果你可以保證new_object回傳的pointer是new出來的
10/08 02:42, 4F

10/08 02:42, 6年前 , 5F
而且主程式call的new/delete和library的完全符合的 是可以的
10/08 02:42, 5F

10/08 02:43, 6年前 , 6F
保險起見library會自己提供release的函式給主程式使用
10/08 02:43, 6F

10/08 02:43, 6年前 , 7F
因為只有library自己最清楚要如何解構他自己創造出來的物件
10/08 02:43, 7F

10/08 02:45, 6年前 , 8F
是說他們編譯用的crt版本實作要一模一樣嗎?
10/08 02:45, 8F

10/08 02:47, 6年前 , 9F
但我有印象曾經有提到 主程式跟lib 他們new出來的記
10/08 02:47, 9F

10/08 02:47, 6年前 , 10F
憶體是配置在不同heap,所以你不能幫他delete會找不
10/08 02:47, 10F

10/08 02:47, 6年前 , 11F
到之類的,是我記錯嗎還是有條件
10/08 02:47, 11F

10/08 02:49, 6年前 , 12F
如果主程式/library 去重載 operator new/delete 就有可能
10/08 02:49, 12F

10/08 02:49, 6年前 , 13F
不過這個還是回歸到兩邊的new/delete 不 compatible 的問題
10/08 02:49, 13F

10/09 15:02, 6年前 , 14F
dreamboat66, 如你所提, 我這樣的寫法其實比較不好,
10/09 15:02, 14F

10/09 15:03, 6年前 , 15F
API設計上應該是誰 allocate 出來的, 也要題供對應的
10/09 15:03, 15F

10/09 15:04, 6年前 , 16F
deallocate, 或是應該要在 API 規範上講明應如何 delete
10/09 15:04, 16F

10/09 15:05, 6年前 , 17F
若沒有講明的話, 難保new_object會不會改變allocate的方
10/09 15:05, 17F

10/09 15:05, 6年前 , 18F
式. 例如new/malloc/或自帶heap pool.
10/09 15:05, 18F

10/09 15:06, 6年前 , 19F
這個例子主要是demo dlopen的部份, 所以就省delete_obj
10/09 15:06, 19F

10/09 15:06, 6年前 , 20F
省得太多code干擾主要的例子 :)
10/09 15:06, 20F

10/09 15:07, 6年前 , 21F
其實上面PkmX也幫忙解釋了..XD
10/09 15:07, 21F

10/09 15:08, 6年前 , 22F
正常來說libc 或 c++ runtime 不會自帶, 通常是dynamic
10/09 15:08, 22F

10/09 15:09, 6年前 , 23F
link系統環境提供的, 所以 lib/main 的new/delete會相容
10/09 15:09, 23F

10/09 15:09, 6年前 , 24F
反過來說, 如果不是獨立的程式, 其實不建議 static link
10/09 15:09, 24F

10/09 15:10, 6年前 , 25F
C/C++ runtime. 例如 staic link -ldl 會有warning
10/09 15:10, 25F

10/10 10:48, 6年前 , 26F
所以exe跟dll會allicate在不同的heap這講法是錯的嗎
10/10 10:48, 26F
文章代碼(AID): #1PsHUieJ (C_and_CPP)
討論串 (同標題文章)
文章代碼(AID): #1PsHUieJ (C_and_CPP)