最近有在作 RocketMQ 社區的 Node.js SDK,是基於 RocketMQ 的 C SDK 封裝的 Addon,而 C 的 SDK 則是基於 C++ SDK 進行的封裝。html
然而,卻出現了一個詭異的問題,就是當我在消費信息的時候,發如今 macOS 下獲得的消息竟然是亂碼,也就是說 Linux 下竟然是正常的。c++
首先咱們要知道一個函數是const char* GetMessageTopic(CMessageExt* msg)
,用於從一個msg
指針中獲取它的 Topic 信息。
亂碼的代碼能夠有好幾個版本,是我在排查的時候作的各類改變:數組
// 往 JavaScript 的 `object` 對象中插入鍵名爲 `topic` 的值爲 `GetMessageTopic` // 第一種寫法:亂碼 Nan::Set( object, // v8 中的 JavaScript 層對象 Nan::New("topic").ToLocalChecked(), Nan::New(GetMessageTopic(msg)).ToLocalChecked() ); // 另外一種寫法:亂碼 const char* temp = GetMessageTopic(msg); Nan::Set( object, // v8 中的 JavaScript 層對象 Nan::New("topic").ToLocalChecked(), Nan::New(temp).ToLocalChecked() ); // 第三種寫法:亂碼 string GetMessageColumn(CMessageExt* msg, char* name) { // ... const char* orig = GetMessageTopic(msg); int len = strlen(orig); char temp[len + 1]; memcpy(temp, orig, sizeof(char) * (len + 1)); return temp; } const char* temp = GetMessageColumn(msg, "topic"); Nan::Set( object, // v8 中的 JavaScript 層對象 Nan::New("topic").ToLocalChecked(), Nan::New(temp).ToLocalChecked() );
而且很詭異的是,當我在調試第三種寫法的時候,我發如今 const char* orig = GetMessageTopic(msg);
這一部的時候 orig
的值是正確的。而一步步單步運行下去,一直到 memcpy
執行結束的時候,orig
內存塊裏面的字符串竟然被莫名其妙修改爲亂碼了。瀏覽器
參考以下:函數
這就不能忍了。ui
當我持之以恆的時候,發現當我改爲這樣以後,返回的值就對了:spa
string GetMessageColumn(CMessageExt* msg, char* name) { // ... const char* orig = GetMessageTopic(msg); int len = strlen(orig); int i; char temp[len + 1]; for(i = 0; i < len + 1; i++) { temp[i] = orig[i]; } // 作一些其它操做 return temp; } const char* temp = GetMessageColumn(msg, "topic"); Nan::Set( object, // v8 中的 JavaScript 層對象 Nan::New("topic").ToLocalChecked(), Nan::New(temp).ToLocalChecked() );
但問題在於,在「其它操做」中,orig
仍是會變成一堆亂碼。當前返回能正確的緣由是由於我在它變成亂碼以前,用能夠「不觸發」變成亂碼的操做先把 orig
的字符串給賦值到另外一個字符數組中,最後返回那個新的數組。指針
問題看似解決了,可是這種詭異、危險的行爲始終是我心中的一顆喪門釘,不處理總之是慌的。調試
在排查的過程當中,我去看了 RocketMQ 的 C++ 和 C SDK 的實現,我把重要的內容摘出來:code
class MQMessage { public: string::string getTopic() const { return m_topic; } ... private: string m_topic; ... } // MQMessageExt 是繼承自 MQMessage const char* GetMessageTopic(CMessageExt *msg) { ... return ((MQMessageExt *) msg)->getTopic().c_str(); }
咱們閱讀一下這段代碼,在 GetMessageTopic
中,先獲得了一個 getTopic
的 STL 字符串,而後調用它的 c_str()
返回 const char*
。一切看起來是那麼美好,沒有問題。
但我後來在屢次調試的時候發現,對於同一個 msg
進行調用 GetMessageTopic
獲得的指針竟然不同!我是否是發現了什麼新大陸?
誠然,msg->getTopic()
返回了一個字符串對象,而且是經過拷貝構造從 m_topic
那邊來的。依稀記得大學時候看的 STL 源碼解析,根據 STL 字符串的 Copy-On-Write 來講,我沒作任何改變的狀況下,它們不該該是同源的嗎?
事實證實,我當時的這個「想固然」就差點讓我查不出問題來了。
在我捉雞了很久以後一直毫無頭緒以後,在參考資料 1 中得到了靈感,我開始打開腦洞(請原諒我這個坑還找了好久,畢竟我主手武器仍是 Node.js),會不會如今的 String 都不是 Copy-On-Write 了?可是 Linux 下又是正常的哇。
後來我在網上找是否是有人跟我遇到同樣的問題,最後仍是找到了端倪。
不一樣的 stl 標準庫實現不一樣, 好比 CentOS 6.5 默認的 stl::string 實現就是 『Copy-On-Write』, 而 macOS(10.10.5)實現就是『Eager-Copy』。
說得白話一點就是,不一樣庫實現不同。Linux 用的是 libstdc++,而 macOS 則是 libc++。而 libc++ 的 String 實現中,是不寫時拷貝的,一開始賦值就採用深拷貝。也就是說就算是兩個同樣的字符串,在不一樣的兩個 String 對象中也不會是同源。
其實深挖的話內容還有不少的,例如《Effective STL》中的第 15 條也有說起 String 實現有多樣性;以及大多數的現代編譯器中 String 也都有了 Short String Optimization 的特性;等等。
獲得了上面的結論以後,這個 Bug 的緣由就知道了。
((MQMessageExt *) msg)->getTopic()
獲得了一個函數中的棧內存字符串變量。
c_str()
仍是源字符串指向的指針,因此函數聲明週期結束,這個棧內存中的字符串被釋放,c_str()
指向的內存還堅挺着;c_str()
的生命週期是跟着字符串自己來的,一旦函數調用結束,該字符串就被釋放了,相應地 c_str()
對應內存中的內容也被釋放。綜上所述,在 macOS 下,我經過 GetMessageTopic()
獲得的內容實際上是一個已經被釋放內存的地址。雖然經過 for
能夠趁它的內存塊被複制以前趕忙搶救出來,可是這種操做一塊已經被釋放的內存行爲總歸是危險的,由於它的內存塊隨時可能被覆蓋,這也就是以前亂碼的本質了。
對於 STL 在這兩個平臺上不一樣的行爲,我也抽出了一個最小化的 Demo,各位看官能夠在本身的電腦上試試看:
#include <stdio.h> #include <string> using namespace std; string a = "123"; string func1() { return a; } int main() { printf("0x%.8X 0x%.8X\n", a.c_str(), func1().c_str()); return 0; }
上面的代碼在 Linux 下(如 Ubuntu 14.04)運行會輸出兩個同樣的指針地址,而在 macOS 下執行則輸出的是兩個不同的指針。
在語言、庫的使用中,咱們不能去使用一個沒有明確在文檔中定義的行爲的「特性」。例如文檔中沒跟你說它用的是 Copy-On-Write 技術,也就說明它可能在將來任什麼時候候不通知你就去改掉,而你也不容易去發現它。你就去用已經定義好的行爲便可,就是說 c_str()
返回的是字符串的一個真實內容,咱們就要認爲它是跟隨着 String 的生命週期,哪怕它其中有黑科技。
畢竟,下面這個纔是 C++ reference 中提到的定義,咱們不能臆想人家必定是 COW 行爲:
Returns a pointer to a null-terminated character array with data equivalent to those stored in the string.The pointer is such that the range
[c_str(); c_str() + size()]
is valid and the values in it correspond to the values stored in the string with an additional null character after the last position.
這同樣能夠引伸到 JavaScript 上來,例如較早的 ECMAScript 262 第三版對於一個對象的定義中,鍵名在對象中的順序也是未定義的,當時就不能討巧地看哪一個瀏覽器是怎麼樣一個順序來進行輸出,畢竟對於未定義的行爲,瀏覽器隨時改了你也不能聲討它什麼。
很久沒寫文了,碼字能力變弱了。
以上。