国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - C/C++ - C++ 回調接口設計和二進制兼容詳細

C++ 回調接口設計和二進制兼容詳細

2022-01-19 12:49黃兢成 C/C++

再開發視頻編輯 SDK,SDK的回調接口設計成 C 風格,結構中放著一些函數指針,既然對外接口是 C++,為什么不直接使用 C++ 的虛函數?這篇文章便對這一問題做個詳細介紹,需要的朋友可以參考一下

1、疑問

我們在開發一個視頻編輯 SDK。SDK 的回調接口設計成 C 風格,結構中放著一些函數指針

?
1
2
3
4
5
6
7
8
9
10
struct SKYMEDIA_API SkyEncodingCallback final {
    // PS: 為達到完全的二進制兼容,這里還應該有個 structSize 的字段。見最后一小節
    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++ 的虛函數?

?
1
2
3
4
5
6
7
8
9
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 風格的回調設計,主要考慮兩個原因

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

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

2、二進制兼容

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

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

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

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

3、編譯環境

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

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

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

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

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

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

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

?
1
2
3
4
5
6
7
struct Test {
    int a;
    int b;
#ifdef CONFIG_DEBUG
    int64_t debugTimestamp;
#endif
};

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

將編譯器和編譯選項,統稱編譯環境。因編譯環境的不同,就有可能產生二進制兼容問題。

4、動態鏈接庫

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

就是 skymedia.dll 動態升級了。

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

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

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

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

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

單獨更新了動態庫,也有可能產生二進制兼容問題。

5、C++ 風格,虛函數接口例子

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

?
1
2
3
4
5
6
7
8
9
10
11
12
// 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,調用如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyCallback : public SkyCallback {
    virtual ~MyCallback() {}
    virtual void callback0() {
        // 做一些事情
    }
    
    virtual void onKeyboard() {
        // 做一些事情
    }
};
 
MyCallback* callback = new MyCallback();
// 做一些事情
void sky_dosomthing(SkyCallback* callback);

現在更新了 skymedia.dll,新版本的 SkyCallback 添加了一個接口

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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),但它動態鏈接了新的 sky_dosomthing。于是就出現了用舊的 MyCallback 去調用新版本的 sky_dosomthing。而新版本的 sky_dosomthing 代碼中,又調用了 MyCallback callback1,但舊版的 MyCallback 是沒有這個 callback1的。C++ 沒有類似 OC 的反射,沒有很好方法去動態判斷 callback1 是否存在。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7、C 風格的回調,如何做二進制兼容

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

?
1
2
3
4
struct SkyCallback {
    void *userData = nullptr;
    void (*callback0)(void *userData) = nullptr;
};

這種結構,就跟我們最開始的 SkyEncodingCallback 很像了。

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 這樣調用

?
1
2
3
4
5
6
7
8
9
// skyeditor.exe
void my_callback0(void* userData) {
  // 做一些事情
}
 
SkyCallback callback;
callback.userData = xxx;
callback.callback0 = callback0;
sky_dosomthing(callback);

現在 skymedia.dll 更新版本,為保證兼容,可以寫成

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 字段的結構,structSize 的值也相應小了。于是舊版的 skyeditor.exe 調用了新的 sky_dosomthing,那個判斷就不會成立, callback1 的調用就不會被觸發。

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

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

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

到此這篇關于C++ 回調接口設計和二進制兼容詳細的文章就介紹到這了,更多相關C++ 回調接口設計和二進制兼容內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://zhuanlan.zhihu.com/p/380510399

延伸 · 閱讀

精彩推薦
  • C/C++詳解c語言中的 strcpy和strncpy字符串函數使用

    詳解c語言中的 strcpy和strncpy字符串函數使用

    strcpy 和strcnpy函數是字符串復制函數。接下來通過本文給大家介紹c語言中的strcpy和strncpy字符串函數使用,感興趣的朋友跟隨小編要求看看吧...

    spring-go5642021-07-02
  • C/C++學習C++編程的必備軟件

    學習C++編程的必備軟件

    本文給大家分享的是作者在學習使用C++進行編程的時候所用到的一些常用的軟件,這里推薦給大家...

    謝恩銘10102021-05-08
  • C/C++C語言中炫酷的文件操作實例詳解

    C語言中炫酷的文件操作實例詳解

    內存中的數據都是暫時的,當程序結束時,它們都將丟失,為了永久性的保存大量的數據,C語言提供了對文件的操作,這篇文章主要給大家介紹了關于C語言中文件...

    針眼_6702022-01-24
  • C/C++C語言實現電腦關機程序

    C語言實現電腦關機程序

    這篇文章主要為大家詳細介紹了C語言實現電腦關機程序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下...

    xiaocaidayong8482021-08-20
  • C/C++C++之重載 重定義與重寫用法詳解

    C++之重載 重定義與重寫用法詳解

    這篇文章主要介紹了C++之重載 重定義與重寫用法詳解,本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下...

    青山的青6062022-01-04
  • C/C++c++ 單線程實現同時監聽多個端口

    c++ 單線程實現同時監聽多個端口

    這篇文章主要介紹了c++ 單線程實現同時監聽多個端口的方法,幫助大家更好的理解和學習使用c++,感興趣的朋友可以了解下...

    源之緣11542021-10-27
  • C/C++C/C++經典實例之模擬計算器示例代碼

    C/C++經典實例之模擬計算器示例代碼

    最近在看到的一個需求,本以為比較簡單,但花了不少時間,所以下面這篇文章主要給大家介紹了關于C/C++經典實例之模擬計算器的相關資料,文中通過示...

    jia150610152021-06-07
  • C/C++深入理解goto語句的替代實現方式分析

    深入理解goto語句的替代實現方式分析

    本篇文章是對goto語句的替代實現方式進行了詳細的分析介紹,需要的朋友參考下...

    C語言教程網7342020-12-03
主站蜘蛛池模板: 亚洲一区二区久久 | 亚洲成人高清 | 国产精品视频免费 | 日本中文字幕在线免费观看 | 一区二区国产精品 | 夜夜嗨aⅴ免费视频 | 国产精品久久久久国产a级 成人a在线视频 | 国产精品一区二区三区四区五区 | 国产精品成人3p一区二区三区 | 久久在线视频 | 成人免费一区二区三区视频网站 | 国产一级一级国产 | 超碰在线9 | 91视频大全 | 国产一区二区三区在线观看免费 | 午夜影院久久 | 国产色网 | 精品久久久久一区二区国产 | 极品女神高潮呻吟av久久 | 欧美日韩国产在线观看 | 激情综合欧美 | 亚洲理论电影在线观看 | 69久久 | 免费岛国片| 国内精品视频 | 91在线网站| 嫩草视频免费在线观看 | 亚洲一区二区三区四区的 | 黄色小视频在线免费观看 | 伦理午夜电影免费观看 | 精品一区二区三区在线观看 | 国产精品a久久久久 | 在线观看日韩 | 一级黄色片aaa | 9色av | 久草视频网 | 国产精品久久久久白丝呻吟 | 国产第一区在线 | av免费网站 | 色视频免费在线观看 | 一呦二呦三呦国产精品 |