C++调用Go方法的字符串传递问题及解决方案
摘要: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 \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方法的字符串传递问题及解决方案》,原文作者:王芾。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
2019 年 CNCF 中国云原生调查报告
中国 72% 的受访者生产中使用 Kubernetes 在 CNCF,为更好地了解开源和云原生技术的使用,我们定期调查社区。这是第三次中国云原生调查,以中文进行,以便更深入地了解中国云原生技术采用的步伐及如何在庞大且不断发展的社区中赋能开发者并作出变革。本报告基于2018 年 3 月和2018 年 11 月发布的前两份中国报告。 https://www.cncf.io/blog/2018/03/26/cncf-survey-china/ https://www.cncf.io/blog/2018/11/13/cncf-survey-china-november-2018/ 中国云原生调查的重点 49% 的受访者在生产中使用容器,另有 32% 计划这样做。与 2018 年 11 月相比,这是一个显著的增长,当时生产中仅 20% 使用容器。 72% 的受访者在生产中使用 Kubernetes,高于 2018 年 11 月的 40%。 公有云的使用率从 2018 年 11 月的 51% 下降到了 36%,取而代之的是使用 39% 的混合新选项。 CNCF 项目呈指数增长。CNCF 现有四个在...
- 下一篇
基于 Nebula Operator 的 K8s 自动化部署运维
摘要:Nebula Operator 是 Nebula Graph 在 Kubernetes 系统上的自动化部署运维插件。在本文,你将了解到 Nebula Operator 的特性及它的工作原理。 从 Nebula Graph 的架构谈起 Nebula Graph 是一个高性能的分布式开源图数据库,从架构上可以看出,一个完整的 Nebula Graph 集群由三类服务组成,即 Meta Service, Query Service(Computation Layer)和 Storage Service(Storage Layer)。 每类服务都是一个由多副本组件组成的集群,在 Nebula Operator 中,我们分别称这三类组件为: Metad / Graphd / Storaged。 Metad:主要负责提供和存储图数据库的元数据,并承担集群中调度器的角色,指挥存储扩容和数据迁移,leader 变更等运维操作。 Graphd:主要负责处理 Nebula 查询语言语句(nGQL),每个 Graphd 都运行着一个无状态的查询计算引擎,且彼此间无任何通信关系。计算引擎仅从 Metad...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合Redis,开启缓存,提高访问速度