一个Go和C++多用途工程项目的模型研究
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
本文探讨一个使用Go语言和C++语言实现的多用途工程项目的模型,该工程可适用于一些实际工作环境,且能提高开发效率,降低维护成本。
问题提出
笔者负责的一个工程的测试程序,需要有交互环境,类似于 Linux 命令行那样,比如修改参数A执行一次测试,修改参数B执行一次测试,这类针对测试的使用,用命令行的方式是最快捷的,因为不需要重新初始化。除此外,也会做一些核算验证工作,但不需要交互,单独执行即可,比如执行一次全量数据的核算,需要耗时1小时(即使是32核心/64GB内存服务器亦要如此久),则需在后台运行,而不能用命令行方式,如果网络断开则会前功尽弃。另外还需有动态库以提供其它程序使用,当然,“其它程序”亦由笔者负责。
综上,这个工程最终要产生三种文件:两个可执行程序文件(包括命令行版和单独版)以及一个动态库文件。为能够复用代码,减少维护成本——主要是指笔者的维护成本,做到模块化但又相对独立,需从较高层面考虑整体的架构。经较长一段时间的探索,以目前的技术水平,以目前的实践经验,提出本文所述之模型。
层架图
层架图如下图所示。
纵向看,可执行文件foobar和foobar_allone分别有不同的执行流程,而foobar_allone可以认为是动态库libfoobar.so的整合调用。
横向看,虽然都有初始化、运行、退出等主要流程,但或多或少有差异。最终执行到的函数,则在全局命令结构体中,在该结构体中,定义了不同的命令名称及对应的执行函数。
从结果上看,不管哪一种可执行文件形式,真正执行操作的是命令结构体中的函数,只是过程稍有不同而已,最终殊途同归。
开发相关
上述层架图涉及的内容,均在同一个工程中,使用不同的适当数量的目录,不同前缀的文件名称,这样做方便项目管理。编码风格上,使用不同风格的函数以示区别。对外提供的函数,使用大小写形式,如FoobarInit
,而内部函数,一般使用小写及下划线形式,比如实际命令的执行函数一般为do_xx
。
工程概述
- 命令行版本适用于交互场合;单独版本适用于单独运行场合。
- 不同形式程序,最终本质还是调用不同命令对应的函数。
- 网页版本由go语言加载so库,再调用FoobarXX函数。
整个工程使用 Makefile 编译,对外输出文件为foobar、foobar_allone、libfoobar.so,其中前两者内容完全一致,通过文件名称实现不同的程序功能。该功能实现不复杂,即在 main 函数中根据运行程序的名称,从而调用不同的模块入口函数,以往文章有涉及,有兴趣可自行查阅。
命令交互的实现,沿用笔者之前实现的模块代码,也有相应的文章。命令结构体定义示例如下:
cmd_tbl_t my_cmd_table[] =
{
{"help", CONFIG_SYS_MAXARGS, do_help_default, "print help info."},
{"?", CONFIG_SYS_MAXARGS, do_help_default, "print help info."},
{"exit", 1, do_exit, "quit program"},
{"quit", 1, do_exit, "quit program"},
{"show", 1, do_show, "show param"},
};
其中,命令名称与实现函数一一对应,如 exit 命令,对应实现函数为do_exit
。
设计说明
由于功能的实现体现在命令中,因为能够在一定程度上解耦,要新加功能,直接添加命令即可。
设置全局参数,gConfig结构体,参数可以在命令行中修改。
设置日志打印标志 showlog,根据等级不同打印不同日志。
单独版本支持多命令输入,与命令行版本相似。以下2种示例,效果完全一样。
``` 命令版本: ./foobar
set showlog=1 test 单独版本: ./foobar_allone "set showlog=1; test" ```
动态库:
```
ifdef __cplusplus
extern "C" {
endif
typedef struct InParam_s{ char logstr; } InParam; typedef struct OutParam_s{ char logstr; } OutParam; int CalFeeRun(InParam inparam, OutParam outparam);
ifdef __cplusplus
}
endif
```
Go 和 C++ 交互
交互方式
笔者除了在终端执行外,还需要用网页做可视化,这样方便给领导展示,也可给其它同事使用。Go 实现 web 服务比较方便,有很多现成的库,笔者实际使用 gin 框架。但底层用到的库还是C++,这样就涉及两种语言的交互了。
前面已经提供了动态库so文件,动态库是C++语言编写,但 Go 只支持 C 语言,因此要解决在 Go 中如何使用 C 封装动态库的调用。
在数据传输上,设计成只有字符串形式而不是结构体。实现简单,能自由定制内容。
具体看,使用C.CString(instr)
将 Go 的字符串转换成 C 语言字符串,再调用。调用结束后,将返回字符串用outstr = C.GoString(outParam.logstr)
转换成 Go 字符串。
// 结构体指针,传入传出
int CalFeeRun(InParam* inparam, OutParam* outparam)
{
typedef int (*ptr)(InParam*, OutParam*);
ptr fptr = (ptr)dlsym(g_sohandle, "CalFeeRun");
return (*fptr)(inparam, outparam);
}
调用示例:
var inParam C.InParam
var outParam C.OutParam
inParam.logstr = C.CString(instr)
ret := C.CalFeeRun(&inParam, &outParam)
if ret < 0 {
klog.Printf("run cal cmd failed\n")
return outstr
}
outstr = C.GoString(outParam.logstr)
// 传出参数为静态缓冲区,不在这里释放
defer C.free(unsafe.Pointer(inParam.logstr))
数据传递
有时数据量较大,可能会溢出或者不完整,前者,可限制固定大小的缓冲区,后者,则可使用crc32或其它方式校验数据。由于较简单,因此略去不谈。
小结
本文根据笔者需求,提出一种方案,不涉及具体的代码实现。从实际实施效果看,还是不错的,当然,因为所有这些工程均是笔者一人维护,是否经得起考验,那是后来的事了。