摘要:C++调用Go办法时,字符串参数的内存治理须要由Go侧进行深度值拷贝。
景象
在一个APP技术我的项目中,子过程按申请加载Go的ServiceModule,将须要拉起的ServiceModule信息传递给Go的Loader,存在C++调用Go办法,传递字符串的场景。
计划验证时,发现有奇怪的将std::string对象的内容传递给Go办法后,在Go办法协程中取到的值与预期不统一。
通过一段时间的剖析和验证,终于了解问题产生的起因并给出解决方案,现分享如下。
背景常识
- Go有本人的内存回收GC机制,通过make等申请的内存不须要手动开释。
- C++中为std::string变量赋值新字符串后,.c_str()和.size()的后果会联动变动,尤其是.c_str()指向的地址也有可能变动。
- 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 LoadModulefunc 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办法的字符串传递问题及解决方案》,原文作者:王芾。
点击关注,第一工夫理解华为云陈腐技术~