一个Go和C++多用途工程项目的模型研究

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

本文探讨一个使用Go语言和C++语言实现的多用途工程项目的模型,该工程可适用于一些实际工作环境,且能提高开发效率,降低维护成本。

问题提出

笔者负责的一个工程的测试程序,需要有交互环境,类似于 Linux 命令行那样,比如修改参数A执行一次测试,修改参数B执行一次测试,这类针对测试的使用,用命令行的方式是最快捷的,因为不需要重新初始化。除此外,也会做一些核算验证工作,但不需要交互,单独执行即可,比如执行一次全量数据的核算,需要耗时1小时(即使是32核心/64GB内存服务器亦要如此久),则需在后台运行,而不能用命令行方式,如果网络断开则会前功尽弃。另外还需有动态库以提供其它程序使用,当然,“其它程序”亦由笔者负责。

综上,这个工程最终要产生三种文件:两个可执行程序文件(包括命令行版和单独版)以及一个动态库文件。为能够复用代码,减少维护成本——主要是指笔者的维护成本,做到模块化但又相对独立,需从较高层面考虑整体的架构。经较长一段时间的探索,以目前的技术水平,以目前的实践经验,提出本文所述之模型。

层架图

层架图如下图所示。

image-20220304104419330.png

纵向看,可执行文件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或其它方式校验数据。由于较简单,因此略去不谈。

小结

本文根据笔者需求,提出一种方案,不涉及具体的代码实现。从实际实施效果看,还是不错的,当然,因为所有这些工程均是笔者一人维护,是否经得起考验,那是后来的事了。

猜你喜欢

转载自juejin.im/post/7104437583404335141