欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++ 回調(diào)接口設計和二進制兼容詳細

 更新時間:2021年09月30日 10:05:23   作者:黃兢成  
再開發(fā)視頻編輯 SDK,SDK的回調(diào)接口設計成 C 風格,結(jié)構(gòu)中放著一些函數(shù)指針,既然對外接口是 C++,為什么不直接使用 C++ 的虛函數(shù)?這篇文章便對這一問題做個詳細介紹,需要的朋友可以參考一下

1、疑問

我們在開發(fā)一個視頻編輯 SDK。SDK 的回調(diào)接口設計成 C 風格,結(jié)構(gòu)中放著一些函數(shù)指針

struct SKYMEDIA_API SkyEncodingCallback final {
    // PS: 為達到完全的二進制兼容,這里還應該有個 structSize 的字段。見最后一小節(jié)
    void *userData = nullptr;
    bool (*shouldBeCancelled)(void *userData) = nullptr;
    void (*onProgress)(void *userData, double currentTime, double totalTime) = nullptr;
    void (*onFinish)(void *userData) = nullptr;
    void (*onError)(void *userData, SkyError error) = nullptr;
};

bool exportVideo(const char *filePath, const SkyEncodingParams &params, const SkyEncodingCallback &callback);

有同事乍一看,會有疑問,既然對外接口是 C++,為什么不直接使用 C++ 的虛函數(shù)?

struct SkyEncodingCallback {
    virtual ~SkyEncodingCallback() {}
    virtual bool shouldBeCancelled() = nullptr;
    virtual void onProgress(double currentTime, double totalTime) = nullptr;
    virtual void onFinish() = nullptr;
    virtual void onError(SkyError error) = nullptr;
};

bool exportVideo(const char *filePath, const SkyEncodingParams &params, SkyEncodingCallback *callback);

使用 C 風格的回調(diào)設計,主要考慮兩個原因

  • 更容易做到庫接口的二進制兼容。
  • 更容易跟 C 對應,方便綁定到各種不同的語言實現(xiàn)。(比如 Flutter 的封裝會使用 ffi 直接調(diào)用 C)

這里不討論語言綁定,只討論接口的二進制兼容。

2、二進制兼容

編譯好的 C/C++ 庫,會提供一些頭文件和動態(tài)連接庫(或者靜態(tài)庫)。主程序(或其他庫)使用頭文件調(diào)用接口,之后去鏈接庫(動態(tài)或靜態(tài)鏈接)。

假如主程序在編譯時,看到的頭文件,跟庫代碼不匹配,就會可能產(chǎn)生了兼容問題。為方便描述,我們假設

  • 主程序為 skyeditor.exe
  • 庫為 skymedia.dll
  • 庫的頭文件為 skymedia.h

有些人會奇怪,既然 skymedia.hskymedia.dll 是一起提供的,自然會匹配。怎么可能出現(xiàn)頭文件跟庫不一致呢?

3、編譯環(huán)境

首先注意到,skymedia.dll skyeditor.exe 是分開編譯的。庫的開發(fā)者跟主程序的開發(fā)者有可能會不同,或者編譯時間上會錯開。

于是就可能出現(xiàn),編譯 skymedia.dll skyeditor.exe 所用到的編譯器和編譯選項不一致。

比如 skymedia.dll 用了編譯器 A 預先編譯,而編譯 skyeditor.exe 時用了編譯器 B。同一個標準庫類,比如 std::string,雖然是相同的名字,但編譯器 A 和編譯器 B,自帶 std::string 的實現(xiàn)卻有可能不同。假如 skymedia.h 出現(xiàn)了一些 STL 的類,就算 skymedia.h 源碼完全一樣,但在編譯 skymedia.dll 和 編譯 skyeditor.exe 時,編碼器對頭文件本身的解釋卻會有不同。

于是在編譯 skyeditor.exe 時,看到的頭文件 skymedia.h,就跟 skymedia.dll 不匹配了。

C++ 并沒有規(guī)定一致的二進制標準。對標準庫,以及某些 C++ 語法的支持,不同的編譯器是可以不同的。有時就算是相同名字的編譯器,只是升級了版本,編譯出來的二進制布局有可能不同。C++ 所謂的跨平臺,只是源碼上的跨平臺,并不是二進制級別的跨平臺。

假如幸運的話,不同編譯器編譯出來的鏈接符號不一樣,在鏈接階段能即時發(fā)現(xiàn)問題。但假如鏈接符號一致,但二進制布局不一致,到執(zhí)行階段才會出問題,就難以發(fā)現(xiàn)了。

另外就算是編譯器和標準庫完全一致,因編譯選項不同也有可能引起不匹配。比如

struct Test {
    int a;
    int b;
#ifdef CONFIG_DEBUG
    int64_t debugTimestamp;
#endif
};


假如編譯 skymedia.dll 和編譯 skyeditor.exe 時,對宏 CONFIG_DEBUG 的定義不同。也會引起頭文件和庫不匹配。

將編譯器和編譯選項,統(tǒng)稱編譯環(huán)境。因編譯環(huán)境的不同,就有可能產(chǎn)生二進制兼容問題。

4、動態(tài)鏈接庫

現(xiàn)在假設編譯器和編譯選項,在編譯 skymedia.dll skyeditor.exe 時完全一樣,仍然有可能產(chǎn)生不兼容。

就是 skymedia.dll 動態(tài)升級了。

比如 skyeditor.exe 現(xiàn)在編譯好了,已發(fā)布了出去。skymedia.dll 出現(xiàn)了 bug,或者更新了功能,需要讓用戶單獨下載更新 skymedia.dll。

或者 skyeditor.exe 同時依賴了 skymedia.dll 和 plugin.dll。而 plugin.dll 也依賴了 skymedia.dll。但 skyeditor.exe 和 plugin.dll 所用到的 skymedia.dll 的版本不一致。于是就可能出現(xiàn) plugin.dll 所用的 skymedia.dll 版本,被 skymedia.exe 無意中被覆蓋掉了。

一個程序依賴的組件越多,獨立開發(fā)的團隊就越多,也就越難以協(xié)調(diào)同步每個團隊所用的庫(以及版本)。能預先發(fā)現(xiàn)版本不一致自然最好,但有時明明規(guī)定好開發(fā)準則,但還是可能出現(xiàn)失誤,不一致就偷偷溜進來了。

動態(tài)庫跟靜態(tài)不同,動態(tài)庫并不用強制 skyeditor.exe 重新編譯,也可以單獨更新。于是 skyeditor.exe 在編譯時,看到的 skymedia.h 頭文件,跟新版本的 skymedia.dll 有可能不同。

假設在更新 skymedia.dll 時,修改了 skymedia.h 的結(jié)構(gòu)。就可能引起了二進制兼容問題。

單獨更新了動態(tài)庫,也有可能產(chǎn)生二進制兼容問題。

5、C++ 風格,虛函數(shù)接口例子

現(xiàn)在我們來實際分析一下代碼。假如舊版 skymedia.dll 接口使用虛函數(shù),會產(chǎn)生什么問題。類似這樣子

// old skymedia.h
struct SkyCallback {
    virtual ~SkyCallback() {}
    virtual void callback0() = 0;
};

// old skymedia.dll
void sky_dosomthing(SkyCallback* callback) {
    // 做一些事情
    callback->callback0();
    // 做一些事情
}

skymedia.exe 在編譯時候,所用到的是舊版 skymedia.dll,調(diào)用如下

class MyCallback : public SkyCallback {
    virtual ~MyCallback() {}
    virtual void callback0() {
        // 做一些事情
    }
    
    virtual void onKeyboard() {
        // 做一些事情
    }
};

MyCallback* callback = new MyCallback();
// 做一些事情
void sky_dosomthing(SkyCallback* callback);

現(xiàn)在更新了 skymedia.dll,新版本的 SkyCallback 添加了一個接口

// skymedia.h
struct SkyCallback {
    virtual ~SkyCallback() {}
    virtual void callback0() = 0;
    virtual void callback1() = 0; // 新加
};

// skymedia.dll
void sky_dosomthing(SkyCallback* callback) {
    // 做一些事情
    callback->callback0();
    // 做一些事情
    callback->callback1();
}

注意 skymedia.exe 這時并沒有被重新編譯(因為只單獨更新了 dll),但它動態(tài)鏈接了新的 sky_dosomthing。于是就出現(xiàn)了用舊的 MyCallback 去調(diào)用新版本的 sky_dosomthing。而新版本的 sky_dosomthing 代碼中,又調(diào)用了 MyCallback callback1,但舊版的 MyCallback 是沒有這個 callback1的。C++ 沒有類似 OC 的反射,沒有很好方法去動態(tài)判斷 callback1 是否存在。

于是就出現(xiàn)問題了,調(diào)用之后,就不知執(zhí)行到哪里了。假如這里的代碼只偶然被執(zhí)行,問題就會隱藏得很深。

PS: C++ 常見的虛函數(shù)實現(xiàn),調(diào)用虛函數(shù)會查表。調(diào)用新版本的 callback1,相當于調(diào)用表格第二項(或第三項?)的函數(shù)。對于 skymedia.exe 來說,表格第二項對應于 onKeyboard。于是只是更新了 dll,可能就莫名其妙地觸發(fā)了 onKeyboard了。

在這種虛函數(shù)的設計下,要完全二進制兼容,會比較麻煩。常見的做法是,SkyCallback 每加一個接口,就定義新的名字,保持 SkyCallback 接口完全不變。于是隨著時間推移,要保證二進制兼容,就產(chǎn)生一系列的 SkyCallback、SkyCallback2SkyCallback3。用戶在更新庫版本后,要用新功能,也相應使用新名字的接口類。這種做法,我個人并不喜歡。

PS: 作為對比,在 C 風格的回調(diào),如何做二進制兼容,參考最后一小節(jié)。

6、進一步討論二進制兼容

要完全做到二進制兼容,是一件很麻煩的事情。是否值得花力氣,要看具體場合。假設編譯環(huán)境可控,還能做到一旦庫被修改,強制使用庫的所有程序都重新編譯。有這樣的理想環(huán)境,就不一定要達到二進制兼容。

但我們不能假設有這樣理想的環(huán)境,設想一些情況

多個不同的庫,同時使用了 skymedia.dll。假如 skymedia.dll 能做到二進制兼容,某個庫就可以獨自升級而不用跟其他團隊協(xié)調(diào)。不然難以推動其他團隊一起升級,所用的庫就被鎖死在某個版本。
發(fā)布程序后,主程序不變,讓用戶獨立升級 skymedia.dll,比如 fix bug 或者更新功能。(某些大型程序,會使用 dll 作為插件機制。能獨立升級 dll,也就能獨立升級插件)
用于調(diào)試。比如只在某個測試(更只在某個用戶)的機器上出現(xiàn)問題,但不知道崩潰在那里。這時可以本地編譯一個帶調(diào)試信息的本地 dll,讓測試(或用戶)替換掉原來的 dll。崩潰之后就有出現(xiàn)一些調(diào)試信息。
庫的對外接口,需要仔細考慮。而庫的內(nèi)部實現(xiàn),肯定是一起編譯的,就不需要那樣講究。SkyMedia C++ API 考慮到二進制兼容,做了一些取舍,但還沒有做到完全的二進制兼容(要完全做到,還是有點麻煩的),只是盡量往這目標靠近。

不出現(xiàn)任何 STL 的類。(比如不使用 std::string)。
impl 手法,復雜的類,內(nèi)部只包括一個 void*,隱藏掉內(nèi)部全部實現(xiàn)。
接口不使用任何實現(xiàn)上不標準 C++ 特性,比如虛函數(shù),多重繼承等等。(這里不標準特性,是指不同的編譯器,編譯出來的二進制布局可能不一致)。
有些人可能還是問,既然 C++ 的接口這樣麻煩,為什么還是提供 C++ 的接口,而不是 C 的接口。

確實,有些庫就算內(nèi)部采用 C++ 開發(fā),也是導出純 C 接口。采用 C++ 接口的,主要是考慮到純 C 的接口用起來麻煩。

比如 C++ API,可以類似這樣用

SkyResource res("/helloworld/test.mp4");
SkyVideoTrack *track = timeline->appendVideoTrack();
track->appendClip(res, SkyTimeRange(0, 10));


假如是純 C API, 就類似這樣了

SkyResource *res = SkyResource_create("/helloworld/test.mp4");
SkyVideoTrack *track = SkyTimeline_appendVideoTrack(timeline);
SkyVideoTrack_appendClip(res, SkyTimeRange(0, 10));
SkyResource_release(res);


大量寫這種純 C 代碼,很繁瑣,也容易忘記初始化,和釋放資源。

7、C 風格的回調(diào),如何做二進制兼容

最后,作為補充,我們回到最開始的問題。類似這種 C 風格的結(jié)構(gòu),如何做二進制兼容呢?比如下面結(jié)構(gòu)

struct SkyCallback {
    void *userData = nullptr;
    void (*callback0)(void *userData) = nullptr;
};


這種結(jié)構(gòu),就跟我們最開始的 SkyEncodingCallback 很像了。

要做到完全二進制兼容,最初的 SkyCallback必須稍微改一下的,預埋一個 structSize字段,初始化成結(jié)構(gòu)的大小。

// old skymedia.h
struct SkyCallback {
    int structSize = sizeof(SkyCallback); // 增加這個字段
    void *userData = nullptr;
    void (*callback0)(void *userData) = nullptr;
};

// old skymedia.dll
void sky_dosomthing(SkyCallback callback) {
    if (callback.callback0) {
        callback.callback0(callback.userData);
    }
}

skyeditor.exe 這樣調(diào)用

// skyeditor.exe
void my_callback0(void* userData) {
  // 做一些事情
}

SkyCallback callback;
callback.userData = xxx;
callback.callback0 = callback0;
sky_dosomthing(callback);

現(xiàn)在 skymedia.dll 更新版本,為保證兼容,可以寫成

// new skymedia.h
struct SkyCallback {
    int structSize = sizeof(SkyCallback);
    void *userData = nullptr;
    void (*callback0)(void *userData) = nullptr;
    void (*callback1)(void *userData) = nullptr;
};

// new skymedia.dll
void sky_dosomthing(SkyCallback callback) {
    if (callback.callback0) {
        callback.callback0(callback.userData);
    }
    
    // 做一些事情
  
    // 兼容舊版本
    if (offsetof(SkyCallback, callback1) + sizeof(callback.callback1) <= callback.structSize) {
        if (callback.callback1) {
            callback.callback1(callback.userData);
        }
    }
}

注意 sky_dosomthing 中那個對 callback1 的判斷。

skyeditor.exe 使用舊版本的 skymedia.dll 編譯時,SkyCallback 是沒有 callback1 字段的結(jié)構(gòu),structSize 的值也相應小了。于是舊版的 skyeditor.exe 調(diào)用了新的 sky_dosomthing,那個判斷就不會成立, callback1 的調(diào)用就不會被觸發(fā)。

structSize 放在最前面,而新加的字段 callback1 放在結(jié)構(gòu)的最后。通過 structSize 可以方便地判斷新增的字段是否存在。這樣自然就兼容舊版本,SkyCallback` 的結(jié)構(gòu)名字也不用修改。

目前 SkyEncodingCallback,還沒有添加 structSize 字段。主要是目前我們二進制兼容的需求還不算緊急,但在 API 設計上,已經(jīng)留了條后路,要改起來也很容易,在源碼級別也是完全兼容的。假如一開始就采用 C++ 的虛函數(shù)接口,以后就難以修改了。

類似這種結(jié)構(gòu)當中添加 structSize 字段的設計,在 C 接口中,還是比較常見的。比如 Win32 API,就常見這種用法。

到此這篇關于C++ 回調(diào)接口設計和二進制兼容詳細的文章就介紹到這了,更多相關C++ 回調(diào)接口設計和二進制兼容內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • MFC程序中使用QT開發(fā)界面的實現(xiàn)步驟

    MFC程序中使用QT開發(fā)界面的實現(xiàn)步驟

    本文主要介紹了MFC程序中使用QT開發(fā)界面的實現(xiàn)步驟,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-12-12
  • C語言實現(xiàn)手寫Map(全功能)的示例代碼

    C語言實現(xiàn)手寫Map(全功能)的示例代碼

    這篇文章主要為大家詳細介紹了如何利用C語言實現(xiàn)手寫Map(全功能),文中的示例代碼講解詳細,對我們學習C語言有一定幫助,需要的可以參考一下
    2022-08-08
  • C++實現(xiàn)貪吃蛇游戲

    C++實現(xiàn)貪吃蛇游戲

    這篇文章主要為大家詳細介紹了C++實現(xiàn)貪吃蛇游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-12-12
  • C++實現(xiàn)二叉樹及堆的示例代碼

    C++實現(xiàn)二叉樹及堆的示例代碼

    這篇文章主要介紹了C++實現(xiàn)二叉樹及堆的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-04-04
  • C++11返回類型后置語法的使用示例

    C++11返回類型后置語法的使用示例

    本篇文章主要介紹了C++11返回類型后置語法的使用示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-10-10
  • C++ LeetCode1812判斷國際象棋棋盤格子顏色

    C++ LeetCode1812判斷國際象棋棋盤格子顏色

    這篇文章主要為大家介紹了C++ LeetCode1812判斷國際象棋棋盤格子顏色, 有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • C語言直接插入排序算法

    C語言直接插入排序算法

    大家好,本篇文章主要講的是C語言直接插入排序算法,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽
    2022-01-01
  • C++枚舉類型enum與enum class的使用

    C++枚舉類型enum與enum class的使用

    這篇文章主要介紹了C++枚舉類型enum與enum class的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-08-08
  • C++實現(xiàn)時間轉(zhuǎn)換及格式化

    C++實現(xiàn)時間轉(zhuǎn)換及格式化

    這篇文章主要為大家詳細介紹了C++中實現(xiàn)時間轉(zhuǎn)換及格式化的相關知識,文中的示例代碼講解詳細,具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學習一下
    2023-11-11
  • 簡單的socket編程入門示例

    簡單的socket編程入門示例

    這篇文章主要介紹了簡單的socket編程入門示例,簡單實現(xiàn)client輸入內(nèi)容發(fā)送到server端輸出,需要的朋友可以參考下
    2014-03-03

最新評論