1. 程式人生 > >C++呼叫Go方法的字串傳遞問題及解決方案

C++呼叫Go方法的字串傳遞問題及解決方案

摘要:C++呼叫Go方法時,字串引數的記憶體管理需要由Go側進行深度值拷貝。

現象

在一個APP技術專案中,子程序按請求載入Go的ServiceModule,將需要拉起的ServiceModule資訊傳遞給Go的Loader,存在C++呼叫Go方法,傳遞字串的場景。

方案驗證時,發現有奇怪的將std::string物件的內容傳遞給Go方法後,在Go方法協程中取到的值與預期不一致。

經過一段時間的分析和驗證,終於理解問題產生的原因並給出解決方案,現分享如下。

背景知識

  1. Go有自己的記憶體回收GC機制,通過make等申請的記憶體不需要手動釋放。
  2. C++中為std::string變數賦值新字串後,.c_str()和.size()的結果會聯動變化,尤其是.c_str()指向的地址也有可能變化。
  3. go build -buildmode=c-shared .生成的.h標頭檔案中定義了C++中Go的變數型別的定義對映關係,比如GoString、GoInt等。其中GoString實際是一個結構體,包含一個字元指標和一個字元長度。

原理及解釋

通過程式碼示例方式解釋具體現象及原因,詳見註釋

C++側程式碼:

//
    // Created by w00526151 on 2020/11/5.
    //
     
    #include <string>
    #include <iostream>
    #include <unistd.h>
    #include "libgoloader.h"
     
    /**
     * 構造GoString結構體物件
     * @param p
     * @param n
     * @return
     */
    GoString buildGoString(const char* p, size_t n){
        //typedef struct { const char *p; ptrdiff_t n; } _GoString_;
        //typedef _GoString_ GoString;
        return {p, static_cast<ptrdiff_t>(n)};
    }
     
    int main(){
        std::cout<<"test send string to go in C++"<<std::endl;
     
        std::string tmpStr = "/tmp/udsgateway-netconftemplateservice";
        printf("in C++ tmpStr: %p, tmpStr: %s, tmpStr.size:%lu \r\n", tmpStr.c_str(), tmpStr.c_str(), tmpStr.size());
        {
            //通過new新申請一段記憶體做字串拷貝
            char *newStrPtr = NULL;
            int newStrSize = tmpStr.size();
            newStrPtr = new char[newStrSize];
            tmpStr.copy(newStrPtr, newStrSize, 0);
     
            //呼叫Go方法,第一個引數直接傳std::string的c_str指標和大小,第二個引數傳在C++中單獨申請的記憶體並拷貝的字串指標,第三個引數和第一個一樣,但是在go程式碼中做記憶體拷貝儲存。
            //呼叫Go方法後,通過賦值修改std::string的值內容,等待Go中新起的執行緒10s後再將三個引數值打印出來。
            LoadModule(buildGoString(tmpStr.c_str(), tmpStr.size()), buildGoString(newStrPtr, newStrSize), buildGoString(tmpStr.c_str(),tmpStr.size()));
            //修改tmpStr的值,tmpStr.c_str()得到的指標指向內容會變化,tmpStr.size()的值也會變化,Go中第一個引數也會受到影響,前幾位會變成新字串內容。
            //由於在Go中int是值拷貝,所以在Go中,第一個引數的長度沒有變化,因此實際在Go中已經出現記憶體越界訪問,可能產生Coredump。
            tmpStr = "new string";
            printf("in C++ change tmpStr and delete newStrPtr, new tmpStr: %p, tmpStr: %s, tmpStr.size:%lu \r\n", tmpStr.c_str(), tmpStr.c_str(), tmpStr.size());
            //釋放新申請的newStrPtr指標,Go中對應第二個string變數記憶體也會受到影響,產生亂碼。
            // 實際在Go中,已經在訪問一段在C++中已經釋放的記憶體,屬於野指標訪問,可能產生Coredump。
            delete newStrPtr;
        }
        pause();
    }

Go側程式碼:

package main
     
    import "C"
    import (
        "fmt"
        "time"
    )
     
    func printInGo(p0 string, p1 string, p2 string){
        time.Sleep(10 * time.Second)
        fmt.Printf("in go function, p0:%s size %d, p1:%s size %d, p2:%s size %d", p0, len(p0), p1, len(p1), p2, len(p2))
    }
     
    //export LoadModule
    func LoadModule(name string, version string, location string) int {
        //通過make的方式,新構建一段記憶體來存放從C++處傳入的字串,深度拷貝防止C++中修改影響Go
        tmp3rdParam := make([]byte, len(location))
        copy(tmp3rdParam, location)
        new3rdParam := string(tmp3rdParam)
        fmt.Println("in go loadModule,first param is",name,"second param is",version, "third param is", new3rdParam)
        go printInGo(name, version, new3rdParam);
        return 0
    }

Go側程式碼通過-buildmode=c-shared的方式生成libgoloader.so及libgoloader.h供C++編譯執行使用

    go build -o libgoloader.so -buildmode=c-shared .

程式執行結果:

test send string to go in C++
    in C++ tmpStr: 0x7fffe1fb93f0, tmpStr: /tmp/udsgateway-netconftemplateservice, tmpStr.size:38 
    # 將C++的指標傳給Go,一開始列印都是OK的
    in go loadModule,first param is /tmp/udsgateway-netconftemplateservice second param is /tmp/udsgateway-netconftemplateservice third param is /tmp/udsgateway-netconftemplateservice
    # 在C++中,將指標指向的內容修改,或者刪掉指標
    in C++ change tmpStr and delete newStrPtr, new tmpStr: 0x7fffe1fb93f0, tmpStr: new string, tmpStr.size:10 
    # 在Go中,引數1、引數2對應的Go string變數都受到了影響,引數3由於做了深度拷貝,沒有受到影響。
    in go function, p0:new string eway-netconftemplateservice size 38, p1:        p���  netconftemplateservice size 38, p2:/tmp/udsgateway-netconftemplateservice size 38

結論

  • 結論:C++呼叫Go方法時,字串引數的記憶體管理需要由Go側進行深度值拷貝。即引數三的處理方式
  • 原因:傳入的字串GoString,實際是一個結構體,第一個成員p是一個char*指標,第二個成員n是一個int長度。

在C++程式碼中,任何對成員p的char*指標的操作,都將直接影響到Go中的string物件的值。

只有通過單獨的記憶體空間開闢,進行獨立記憶體管理,才可以避免C++中的指標操作對Go的影響。

ps:不在C++中進行記憶體申請釋放的原因是C++無法感知Go中何時才能真的已經沒有物件引用,無法找到合適的時間點進行記憶體釋放。

本文分享自華為雲社群《C++呼叫Go方法的字串傳遞問題及解決方案》,原文作者:王芾。

 

點選關注,第一時間瞭解華為雲新鮮技術~