您好,登錄后才能下訂單哦!
本篇內容介紹了“C語言的庫封裝發布技術”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
1. C動態鏈接庫是一種即成標準
2. 用C++制作C的庫
2.1 使用void * 作為句柄
2.2 導出這些方法
3. 使用庫
4. 經典的范例:libuhd
每年實驗課,總有同學問我,如何生成DLL、如何導出類,如何不花很多時間精力,就設計出一個給別人用的爽的功能庫呢?結合這些年的實踐,我們今天就來聊一聊動態鏈接庫的封裝發布。您也可以直接跳到文章最后,去github查看C++/C混合庫的經典案例——Ettus uhd
要讓自己的庫好用,又通用,該怎么辦?重要的事情說前面:
不要導出類、不要導出變量,僅使用C基礎數據類型。
面向對象實現功能真香,實現接口真要命。
用最棒的語言實現功能,遵循C語言標準實現接口。
非密集吞吐的接口,可以使用json整體交互。密集吞吐,用內存。
做到了這幾點,即使用戶從VC2010換成了python,庫都不用改。究其原因,C++的類在二進制結構上是缺乏定義的,一個返回值用了std::string,或者參數用了CTime的方法,從VC2010導出的DLL到了VC2017就不一定能用,更別提其他編譯器和語言了。
C語言是一門古老的語言。從六七十年代開始,在Unix/Linux操作系統上,C語言實現了大量的庫,幾乎涵蓋了當代科學涉及的所有領域。從基礎的XML操作,到復雜的數學算法,都能找到對應的C庫。C語言的動態鏈接庫承載了太多的智力遺產,以至于后來的大部分語言都自覺的加入了享用既有C語言動態鏈接庫的能力。
這種情況使得符合C語言習慣的動態鏈接庫接口1成為了一種即成實事,不同的語言之間,使用C動態鏈接庫的標準交互。盡管這種接口是面向過程的,而可用的參數類型少的可憐,但其簡單、直接,又有大量的歷史資源,使得后來的CORBA、COM也無法取代這種底層的接口方式2。
這里有幾個概念需要明確:
C接口的動態鏈接庫的通用性,一般只和操作系統、運行時(32位還是64位)有關,和具體的編譯器、語言無關。
很多現代編程語言能調用C接口的動態鏈接庫。
部分現代編程語言能生成C接口的動態鏈接庫。
無論你使用什么語言開發功能,只要提供了符合C語言動態鏈接庫結構的接口,許多其他語言就可以使用你的功能。因此,完全可以用C++語言實現一個C接口的庫,在里面盡情使用STL。
用C++做C的庫,關鍵是用好句柄。
什么是“句柄(Handle)”?這是個翻譯問題。你可以理解為“把手”或者“提手”更合適。句柄很多時候是一個整數,用于標記一堆運行時資源,實現操作動態庫功能的目的。
對一個復雜的功能來說,需要很多運行時的參數來支撐。比如FFT,就需要有一個內存區域記錄蝶形運算的單元,以及指向各層單元的索引。對通信中的糾錯譯碼,需要一些內存區域記憶寄存器,以及當前的狀態。所有上述這些狀態,都可以用一個struct 包裹起來,形成一個“箱子”。這個箱子對用戶是透明的,只需要把箱子的把手(Handle)交給用戶手上,用戶在需要的時候,交回箱子并執行任務。
不難想像,可以同時申請多個箱子,交給不同的線程去執行。庫的設計者要確保Handle標記的參數包之間是獨立的、線程安全的。
同時,句柄本身可以復刻面向對象的部分功能。如果把Handle作為this指針看待,則C++類可以直接導出為C的函數。只是首個參數要傳入Handle即可。
舉個例子,假設手頭有一個實現字符串查找的類,需要向外發布功能。但這個類是C++的,類似:
//關鍵詞查找器類 class Findfoo { public: Findfoo(const std::string & task = "foo"); ~Findfoo(); public: void setTask(const std::string & task); const std::string & task() const; //在rawStr里查找關鍵詞 long long Find(const std::string & rawStr); private: //用于匹配的關鍵詞 std::string m_task = "foo"; }; Findfoo::Findfoo(const std::string & task) :m_task(task) {} Findfoo::~Findfoo() {} void Findfoo::setTask(const std::string & task) { m_task = task; } const std::string & Findfoo::task() const { return m_task; } long long Findfoo::Find(const std::string & rawStr) { return rawStr.find(m_task); }
此時,可以設置以下接口,把C++的類變成C的方法。一旦變為C的方法,外部就無需知道該類的存在。
//創建一個查找器,返回句柄。提供的是關鍵詞。 void * ff_init_task(const char * task) { Findfoo * f = new Findfoo(task); return (void *) f; } //重設關鍵詞 void ff_reset_task(void * h, const char * task) { Findfoo * f = (Findfoo *)(h); assert(f); f->setTask(task); } //獲取當前關鍵詞 const char * ff_get_task(void * h) { Findfoo * f = (Findfoo *)(h); assert(f); return f->task().c_str(); } //用關鍵詞查找rawStr long long ff_find(void * h, const char * rawStr) { Findfoo * f = (Findfoo *)(h); assert(f); return f->Find(rawStr); } //刪除當前查找器 void ff_fini_task(void * h) { Findfoo * f = (Findfoo *)(h); if (f) delete f; }
如此操作,用戶可以完全不知道存在Findfoo類,只用一個void *指針作為操作類的指示。
上面的例子僅有1個類作為演示。實際開發中,一個工作可能由好幾個類的實例共同協作完成。可以用一個std::map<long long, XXX>來管理各個實例,也可以把實例全部放在一個struct中。如果用std::map,切記多線程下的mutex一致性保護,防止用戶同時在多個線程init好幾組功能實例,導致std::map崩潰。
從性能角度,建議采用struct來承載所有運行時,而后返回指向該struct的指針。
上述函數,因為是C++函數,編譯器會對其進行改名,把參數也放進去,以便支持多態(同一個函數名,不同參數)。要導出為C的函數,就不允許編譯器改名字。要用“extern ‘C‘”進行包裝,以便導出這些方法時,函數名不變。
同時,在Windows下,函數存在多個參數時,棧內的參數順序也有從左開始還是從右壓棧的區別。要做到最大的適應性,需要指定 stdcall開關。
最后,我們不想為生成庫的工程、用戶的工程準備兩套頭文件,故而需要一些瑣碎的宏定義,以區分當前編譯的是DLL本身,還是使用DLL的用戶工程。
具體:
建立一個頭文件,叫做findfoo_global.h。這個頭文件對Linux和windows平臺定義一些宏,用于聲明函數時,指定導出(構造DLL本身)和導入(使用DLL)
#ifndef FINDFOO_GLOBAL_H #define FINDFOO_GLOBAL_H #if defined(_MSC_VER) || defined(WIN64) || defined(_WIN64) || defined(__WIN64__) || defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) # define Q_DECL_EXPORT __declspec(dllexport) # define Q_DECL_IMPORT __declspec(dllimport) # define FOOCALL __stdcall #else # define Q_DECL_EXPORT __attribute__((visibility("default"))) # define Q_DECL_IMPORT __attribute__((visibility("default"))) # define FOOCALL #endif #if defined(FINDFOO_LIBRARY) # define FINDFOO_EXPORT Q_DECL_EXPORT #else # define FINDFOO_EXPORT Q_DECL_IMPORT #endif //句柄就是void * #define FFHANDLE void * #endif // FINDFOO_GLOBAL_H
在DLL的工程中,要定義FINDFOO_LIBRARY宏,這樣,就開啟了導出開關。
建立頭文件findfoo.h
#ifndef FINDFOO_H #define FINDFOO_H #include "findfoo_global.h" #ifdef __cplusplus extern "C"{ #endif FINDFOO_EXPORT FFHANDLE FOOCALL ff_init_task (const char * task); FINDFOO_EXPORT void FOOCALL ff_reset_task (FFHANDLE h , const char * task); FINDFOO_EXPORT const char * FOOCALL ff_get_task (FFHANDLE h ); FINDFOO_EXPORT long long FOOCALL ff_find (FFHANDLE h , const char * rawStr); FINDFOO_EXPORT void FOOCALL ff_fini_task (FFHANDLE h ); #ifdef __cplusplus } #endif #endif // FINDFOO_H
實現導出方法 findfoo.cpp
#include "findfoo.h" #include <assert.h> #include <string> class Findfoo { public: Findfoo(const std::string & task = "foo"); ~Findfoo(); public: void setTask(const std::string & task); const std::string & task() const; long long Find(const std::string & rawStr); private: std::string m_task = "foo"; }; Findfoo::Findfoo(const std::string & task) :m_task(task){} Findfoo::~Findfoo(){} void Findfoo::setTask(const std::string & task) { m_task = task; } const std::string & Findfoo::task() const { return m_task; } long long Findfoo::Find(const std::string & rawStr) { return rawStr.find(m_task); } //----------- FINDFOO_EXPORT FFHANDLE FOOCALL ff_init_task(const char * task) { Findfoo * f = new Findfoo(task); return (FFHANDLE) f; } FINDFOO_EXPORT void FOOCALL ff_reset_task(FFHANDLE h, const char * task) { Findfoo * f = reinterpret_cast<Findfoo *>(h); assert(f); f->setTask(task); } FINDFOO_EXPORT const char * FOOCALL ff_get_task(FFHANDLE h) { Findfoo * f = reinterpret_cast<Findfoo *>(h); assert(f); return f->task().c_str(); } FINDFOO_EXPORT long long FOOCALL ff_find(FFHANDLE h, const char * rawStr) { Findfoo * f = reinterpret_cast<Findfoo *>(h); assert(f); return f->Find(rawStr); } FINDFOO_EXPORT void FOOCALL ff_fini_task(FFHANDLE h) { Findfoo * f = reinterpret_cast<Findfoo *>(h); if (f) delete f; }
一旦導出了上述方法,即可使用庫。
#include <iostream> #include <cassert> #include "findfoo.h" using namespace std; int main() { FFHANDLE h = ff_init_task("foobar"); assert(h); cout << "Task string:" << ff_get_task(h) << endl; cout << "Input String:"; std::string strRaw; cin >> strRaw; cout << ff_find(h,strRaw.c_str()); //Delete ff_fini_task(h); h = nullptr; return 0; }
上述是最簡單的例子。當需要處理大量動態內存時,需要注意:內存誰申請,誰釋放。這一點特別容易引起錯誤。
USRP軟件無線電平臺對應的開源庫libuhd是用C++ boost開發的。但是,為了兼容更多的語言,其進行了封裝,把各個類都用句柄抽象出來了,且是標準C的接口。
可以去Github的工程頁簽出項目查看,也可以跟蹤代碼查看其原理。
這個項目是把C、C++的聯合運用發揮的非常棒的例子。
1包括函數入口點的定位方式、函數命名方式、參數傳遞規則、參數類型。
2.COM和C接口DLL其實不是一個范疇的東西,這里放在一起,有點粗暴。
“C語言的庫封裝發布技術”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。