0. 简介

参数分离对于绝大多数算法开发来说收益是非常大的,因为我们都知道,随着平台的更替,很多时候如果说数据流和算法交叠在一起(即接口与实现合在一起)。这将有可能会导致在迁移平台时候会导致代码难以维护,并有可能会造成莫名的Bug。为了使代码更易于维护和扩展,在修改通信接口时不需要修改相关的算法代码,本文的下面将会介绍几种常用的方法。

1. 通过动态链接库完成链接

动态链接库(Dynamic Link Library,简称DLL)是一种在操作系统中用于共享代码和数据的机制。它是一种可执行的二进制文件,可以被多个程序同时使用,以提高代码的重用性和模块化程度。在C++算法分离中,动态链接库可以用于将算法代码从应用程序中分离出来,使得算法可以独立开发、测试和优化,同时也方便应用程序的调用和升级。

使用动态链接库可以使得算法代码和应用程序代码分别编译和链接,从而实现分离。这样一来,算法代码的修改和升级不会对应用程序造成影响,而应用程序也不必重新编译和链接。动态链接库还可以提高代码的重用性和可维护性,因为同一份动态链接库可以被多个应用程序使用,而且如果需要更新动态链接库,只需替换原文件即可。

对于动态链接库来说,主要分成两步。首先,需要在算法代码中明确定义导出函数(Export Function),以供应用程序调用。

比如说我们会创建一个函数my_pack_test.cpp

#include <stdio.h>
void foobar(int i)
{
  printf("Printing from my_pack_test.so %d\n", i);
}

CMakeList.txt

# include_directories( 这个括号里添加此项目依赖的头文件路径 )
 include_directories(../include)
add_library(my_pack_test 
# SHARED 字段指定testJni为动态链接库
SHARED 
# my_pack_test,为testJni库的cpp文件(生成testJni所需要的cpp都写到此处)
my_pack_test.cpp
)
# libgadl.so 为该testJni库需要链接的so库
target_link_libraries(testJni /home/lib/libgdal.so)
install(TARGETS my_pack_test LIBRARY DESTINATION lib)

这样我们就可以生成一个.so文件了。然后我们就需要在应用程序中使用动态链接库的导入函数(Import Function),以便在程序运行时动态加载动态链接库,并调用其中的算法函数。最后,需要将动态链接库文件放置在应用程序能够搜索到的路径下,以便程序能够找到它。

#include <iostream>
#include "mytool.h" // include the lib header

// include shared lib load
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h> # in linux, should also link to dl system library when build
#endif

// define shared lib load handler
typedef MyTool *(*CreateMyToolFunc)();

#ifdef _WIN32
HINSTANCE gDllHandler = nullptr;
const char *gDefaultSharedLibPath = "mytool.dll"; // here put it the same path
#else
void *gSoHandler = nullptr;
// 为上面生成的.so文件的绝对路径
const char *gDefaultSharedLibPath = "libmytool.so"; // here put it the same path, some linux must use ./libmytool.so
#endif

int main()
{

#ifdef _WIN32
    // load shared lib
    gDllHandler = LoadLibrary(gDefaultSharedLibPath);
    if (!gDllHandler)
        std::cout << "load shared lib failed" << std::endl;

    CreateMyToolFunc create_mytool = (CreateMyToolFunc)(GetProcAddress(gDllHandler, "CreateMyTool"));
    MyTool *my_tool = create_mytool(); // get the derived class instance from shared lib

    my_tool->fun1();
    int z = my_tool->fun2(2, 3);
    printf("z: %d", z);

    // when all done, unload shared lib
    FreeLibrary(gDllHandler);
    gDllHandler = nullptr;
#else
    // 加载指定的 .so 文件
    gSoHandler = dlopen(gDefaultSharedLibPath, RTLD_LAZY);
    if (!gSoHandler)
        std::cout << "load shared lib failed" << std::endl;

    CreateMyToolFunc create_mytool = (CreateMyToolFunc)(dlsym(gSoHandler, "CreateMyTool"));
    // 查找函数create_mytool,并返回函数指针
    MyTool *my_tool = create_mytool(); // get the derived class instance from shared lib
    // 调用对应的foobar函数打印输出
    my_tool->fun1();
    int z = my_tool->fun2(2, 3);
    printf("z: %d", z);

    // when all done, unload shared lib
    dlclose(gSoHandler);
    gSoHandler = nullptr;
#endif

    return 0;
}

dlopen:该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。这种机制使得在系统中添加或者删除一个模块时,都不需要重新进行编译。
dlsym:在打开的动态库中查找符号的值。
dlclose:关闭动态库。
dlerror:返回一个描述最后一次调用dlopen、dlsym,或dlclose的错误信息的字符串。

2. 通过进程完成链接

2.1 fork()完成子进程和父进程的操作

pid_t fork(void);

0: 子进程
子进程PID(大于0的整数):父进程
-1: 出错

使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:
进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
子进程与父进程的区别在于:
1、父进程设置的锁,子进程不继承(因为如果是排它锁,被继承的话,矛盾了)
2、各自的进程ID和父进程ID不同
3、子进程的未决告警被清除;
4、子进程的未决信号集设置为空集

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int glob = 6;
int main()
{
    int local;
    int pid;

    local = 88; 

    printf("parent[pid = %d]: before fork, glob(&glob) = %d(%p), local(&local) = %d(%p)\n",
        getpid(), glob, &glob, local, &local );

    if((pid = fork()) < 0) {
        perror("fail to fork");
        return -1; 
    }   

    if(pid == 0) { /* child process */
        printf("child[pid = %d]: after fork, glob(&glob) = %d(%p), local(&local) = %d(%p)\n",
            getpid(), glob, &glob, local, &local );
        glob++;
        local++;    
        printf("child[pid = %d]: changed data after fork, glob(&glob) = %d(%p), local(&local) = %d(%p)\n",
            getpid(), glob, &glob, local, &local );
    }else { /* parent process */
        sleep(2);
        printf("parent[pid = %d]: after fork, glob(&glob) = %d(%p), local(&local) = %d(%p)\n",
            getpid(), glob, &glob, local, &local );
    }   
    /* return euqal to exit(0), but exit may cause a compile warning
     * due to main() is declared to return with an integter 
     */

    return 0;  
}

2.2 exec函数族

exec函数族提供了一种在进程中启动另一个程序执行的方法。
它可以根据指定的文件名或目录名找到可执行文件, 并用它来取代原调用进程的数据段、 代码段和堆栈段。
在执行完之后, 原调用进程的内容除了进程号外, 其他全部都被替换了。
可执行文件既可以是二进制文件, 也可以是任何Linux下可执行的脚本文件。

这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用,调用关系如下图所示:

/* exec函数族的语法 */
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>


int main(int argc, char *argv[])
{
    /* 字符串指针数组传递参数,使用包含v的exec函数参数 */
    char *arg[] = {"ls","-a",NULL};
    char *arg1[] = {"env",NULL};        //只用于execve函数

    char *envp[] = {"NAME=amoscykl","EMAIL=xxxx@xx.com","PATH=/tmp",NULL};
    char **ptr;                //指向环境表

    // 打印出环境表
    printf("自定义环境表\n");

    for (ptr = envp; *ptr != 0; ptr++)
        printf("%s \n",*ptr);

    printf("\n");

    sleep(2);

    /* 子进程调用execl函数 */
    if (fork() == 0)
    {
        //child1
        printf("1-----execl-----\n");
        if (execl("/bin/ls","ls","-a",NULL) == -1)
        {
            perror("execl error!");
            exit(1);
        }
    }

    sleep(2);

    /* 子进程调用execv函数 */
    if (fork() == 0)
    {
        //child2
        printf("2-----execv-----\n");
        if (execv("/bin/ls",arg) == -1)
        {
            perror("execv error!");
            exit(1);
        }
    }

    sleep(2);

    /* 子进程调用execlp函数 */
    if (fork() == 0)
    {
        //child3
        printf("3-----execlp-----\n");
        if (execlp("ls","ls","-a",NULL) == -1)
        {
            perror("execlp error!");
            exit(1);
        }
    }

    sleep(2);

    /* 子进程调用execvp函数 */
    if (fork() == 0)
    {
        //child4
        printf("4-----execvp-----\n");
        if (execvp("ls",arg) == -1)
        {
            perror("execvp error!");
            exit(1);
        }
    }

    sleep(2);

    /* 子进程调用execle函数 */
    if (fork() == 0)
    {
        //child5
        printf("5-----execle-----\n");
        if (execle("/usr/bin/env","env",NULL,envp) == -1)        //使用自定义的环境表,并打印出自定义环境变量
        {
            perror("execle error!");
            exit(1);
        }
    }

    sleep(2);

    /* 子进程调用execve函数 */
    if (fork() == 0)
    {
        //child6
        printf("6-----execve-----\n");
        if (execve("/usr/bin/env",arg1,envp) == -1)                //使用自定义的环境表,并打印出自定义环境变量
        {
            perror("execve error!");
            exit(1);
        }
    }

    sleep(2);

    printf("over!\n");
    return 0;
}

3. 参考链接

https://hermit.blog.csdn.net/article/details/107897498

https://blog.csdn.net/weixin_52849254/article/details/129468743

https://img-blog.csdnimg.cn/b1676431811242a0a03e17d7847f14fb.png