关于c++:C调用Go方法的字符串传递问题及解决方案

31次阅读

共计 3504 个字符,预计需要花费 9 分钟才能阅读完成。

摘要: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 rn", 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 rn", 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 办法的字符串传递问题及解决方案》,原文作者:王芾。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0