[TSG开发日志4]算法组件、个人编写的库文件如何封装成DLL,如何更好地对接软件开发?

发布时间 2023-07-15 17:17:17作者: 轩先生。

写在前面

这个内容确实是我有点疏忽了,我以为做算法的同事应该多少对这方面会有点了解的。但是我想了一下我刚毕业的时候,确实对这方面的理解不深,查了很多资料才勉强搞懂什么意思,也是后来随着工程学习的愈加深入,才渐渐了解了在C++开发中动态链接库的重要性及如何编写。

一般在说一个标准时,我喜欢从两个角度出发:为什么,怎么做。

一、为什么

这里问的是为什么要把自己的算法组件打包成C++动态链接库。
事实上,我不仅需要你作为一名开发,写出来的代码需要打包成一个DLL的动态链接库,而不是直接提供工程文件,或者直接提供 .h文件和.cpp文件?其实主要从以下几个角度出发:

  1. 代码保护:通过提供动态链接库,您可以隐藏算法的源代码和实现细节,从而更好地保护您的知识产权。其他人只能使用动态链接库提供的接口来调用算法,而无法查看或修改算法的实现。

    这个对于大伙来说其实不那么重要,因为作为软件开发我不够关心算法是怎么写的,但是这个世界不好说是不是小人多,也许今天你把算法交给他了,明天他就拿到github上开源说是自己写的了。

  2. 代码封装:动态链接库允许您将算法封装为一个单独的实体,从而提供一个清晰的接口和功能集。其他开发人员可以直接使用库提供的函数,而无需关心算法的内部实现。

    这么说吧,前面的开发并不关心你算法代码是怎么实现的,你这样将一坨代码粗暴的插入到软件中是非常不合适的,因为这会导致测试上的拖沓,试想一下,当程序崩溃时,测试员/程序员如果追到你的代码里面去,这时候你说你的代码在本地实现很好啊,这时候你和软件开发双方该如何面对彼此?是不是非常尴尬?因为错误确实是在你的代码中发生的,但是确实是在他的程序里才会发生这种问题,你自己本地运行就不会发生这种问题,这下你们两个人就扯不清了!会严重拖慢测试进度和开发进度,而这样的问题会在你短暂的职业生涯中疯狂地折磨你的精神,直到永远,因为和算法对接的那个软件开发大概率也不会关心算法是怎么写的,他在乎他自己(悲。

  3. 版本控制:通过提供动态链接库,您可以更轻松地进行版本控制。如果您对算法进行了更新或修复,只需提供更新的动态链接库即可,而不需要重新编译依赖于该算法的其他代码。

    试想一下,你现在手上有一个算法Calculation1,你交到软件A的时候是v1.0.1版本,交的是.h和.cpp文件,你交过去之后软件开发要对你的代码做好一番修改(这几乎是必然的)之后才能顺利插入到软件中去。

    时过境迁,多年后,你的算法要被另一款软件使用了,而这个软件B的时候可能你的算法已经提供了非常完备的计算方法,计算的能力也获得了巨大进步,已经是v2.14.10版本了,这个时候你再同样的将.h和.cpp文件插入到软件中。

    突然,维护A软件的同志惊奇地发现,原来Calculation1算法从一开始就有巨大的问题,是一直依靠内存溢出或者什么莫名其妙的bug才能正常运行的。你非常惊讶,但想了一下自己应届的水平,确实,写出这种代码是人之常情。于是你潇洒的一批把v2.14.10交给了软件开发。

    也许是作为一名算法开发完全不需要遵守开闭原则或者其他设计模式(虽然我建议每一位算法都去看一下,这坨v2.14.10完全用不了,那边需求又来得很急,最终你和开发争不过只能重写Calculation1,你越看这个代码越生气,自己当年怎么就写出坨这么个东西,看了半天,决定重写。但是甲方的催促已经驾到你的脖子上了,你只能在丧钟一般的微信报警中熬夜通宵重写完这个算法,装上软件一编译,满屏幕的依赖错误,输出的结果不符合预期,输入经常会报错。你扶额看着这一摊究极烂摊子。天亮的时候必须给甲方一个答复,而你的答复是...

  4. 跨语言兼容性:动态链接库可以被多种编程语言调用和使用。通过提供动态链接库,您可以使算法在不同的编程语言和平台上可用和可调用。

    事实上,我们希望所有的算法都提供统一C接口的动态链接库。因为我们都知道算法是确定的,而语言是不确定的。什么意思呢?就是这个项目部会经历很多东西,也许他会从C#到Qt,再从Qt到C#,再到GO,再到VB,再到Rust,再到Ruby,甚至到前端,到什么乱七八糟的C++ 114514版本,规范和当年的完全不同了————

    但是呢?哪怕是核弹轰炸了东京,你的动态链接库从点云中解算出净空的算法仍然是正确的。如果我们说TSD这个软件是一坨屎,那么点云解算算法就是这坨屎圣代上的神圣樱桃,值得永远传承下去,而不是疯狂地重写迭代。

它就永远存在 懂我的意思吗,就算是公司炸了,公司重建了,公司上市了,公司退市了,世界崩塌了,世界重启了,我死了,我出生了,我怀孕了,我堕胎了,我又出生了,如果你写的是C接口的动态链接库,那么它就永远存在.

所以为了大家的心里更健康一点,请写类C接口,哪怕是COM组件呢?

看完以上四点,想必你已经明白为什么要把代码封装成动态链接库了吧?

这既是为了你好,也是为了那个软件开发好。当然了如果你从入职开始就考虑好什么时候跑路,请联系我?,我会给你看一份世上最烂的代码,手把手教你怎么搞垮一个项目部,当前了前提是我也准备跑路了。

二、怎么做

ok,到了我最喜欢的怎么做环节。实际上当你想清楚你为什么要把代码打包成动态链接库的时候你就应该已经懂了自己该怎么做了。

我们现在来写一个动态链接库的模板。

结果是什么样的?

  1. 我们希望给出来的算法组件是有文档的。这个文档里应当包含这个库的依赖、接口说明、调用示例,如果可以的话请给出必要的说明。

  2. 最起码的,算法组件应当提供一个.h文件和.lib文件和.dll文件,其中.h文件代表的是这个库的声明文件,而.lib则是提供了一些接口转义的功能,.dll则是动态链接库本尊,三者几乎缺一不可。另外如果你用的是C++新特性import和module,则另说,如果你是这样开发算法组件的,请你将其详细说明在文档中。

    另外,我们希望算法组件应当提供类C接口,这样算法组件才可以一直传承下去,直到宇宙爆炸。这样的话如果后续开发更换了语言,就不需要像现在TSD那样把原来的程序改造成一个统一的EXE文件,然后通过传值的方式调用过去的包袱。

  3. 请给出一个DEMO程序,这将成为你日后和软件开发最强的武器,可以避免扯皮扯不清。就比如我之前那个问题,如果dll在你的demo上能跑通,那在实际环境中跑不通,就说明是软件开发的问题,反之则是你的问题。总不能等到问题出现之后再写一个测试程序来测试,对吧?

从零开始,开法一个算法组件

接下来我将演示如何从零开始,开发一个C++算法组件,这个算法组件的功能会比较简单,但是会包含很多你想要了解的知识,以及提到一些你可能会产生的疑惑。

1.建立工程。

我们现在先假设已经写好了一个算法组件,内容比较简单,只能做加减法,如下:

Test.h

#pragma once
#include <iostream>

class Test
{
	Test();
	~Test();

	int Add(int a, int b);
	int Minus(int a, int b);
};

Test.cpp

#include "Test.h"
Test::Test() {
	printf("Test Inialitzed");
}
Test::~Test() {
	printf("Test Uninialitzed");
}

int Test::Add(int a, int b) {
	printf("Add was created");
	return a + b;
}

int Test::Minus(int a, int b) {
	printf("Minus was Created");
	return a - b;
}

ok,这样一个简单的算法组件就写好了,它包含了两个接口,一个是Add,一个 是Minus,现在我们右键一下项目,可以看一下项目的属性

image

我们需要输出的是动态链接库,所以需要将其配置类型改为动态链接库(.dll)才对。这里需要注意,你编写的代码是debug版还是release版,一般情况下我们发布的代码版本需要保持是release版本,所以需要将两个配置都修改一下。

image

这里你可能会问,那为什么不打包成静态库?有这个想法是好的,如果你很好奇这方面的内容,我建议你去查一下。总之一般情况下我们都将其打包成动态链接库。

ok,那么现在就可以编译一下尝试一下了:

image

欧克,这样代码就算生成成功了,但是这个代码就可以用了吗?是不可以的,实际上在编译过程中,这样形成的库是没有办法正常使用的,还需要一个宏来声明其接口时需要公布的。

2.导出接口

所以我们可以将头文件中的代码改成这样:

#pragma once
#include <iostream>
#ifdef DLL_TEST_EXPORTS
#define DLL_TEST_API __declspec(dllexport)
#else
#define DLL_TEST_API __declspec(dllimport)
#endif

class DLL_TEST_API Test
{
	Test();
	~Test();

	int Add(int a, int b);
	int Minus(int a, int b);
};

这个宏是什么意思呢?大概意思就是:如果我在当前代码中声明了DLL_TEST_EXPORTS的宏,则所有的DLL_TEST_API字段都将变成__declspec(dllexport),而如果我没有声明这个宏,则所有DLL_TEST_API字段都将变成__declspec(dllimport)

一般情况下,我们会在编写动态链接库中默认地带上这么个宏(却不写在需要公布的.h文件中),为的就是让代码在编译期间做到:

在算法的电脑上,这个头文件中DLL_TEST_API字段代表的是输出(__declspec(dllexport)),而在别人电脑,比如开发的电脑上,这个头文件中的DLL_TEST_API字段代表的就是引入(__declspec(dllimport))

image
也就是说,这个宏DLL_TEST_EXPORTS只应该在你的算法工程中存在,别人是不需要调用这个export的。


这两个字段是什么意思呢?实际上__declspec(dllexport) 是 Microsoft Visual C++ 编译器的一个扩展,用于指定将函数、变量或对象导出到动态链接库(DLL)中.

  1. __declspec(dllexport) 是一个修饰符,可以放在函数、变量或对象的声明前,用于显式地指定将其导出到 DLL 中。使用 __declspec(dllexport) 修饰的函数、变量或对象将可以从其他模块或程序中访问和调用。

    具体而言,__declspec(dllexport) 用于告诉编译器将修饰的函数、变量或对象添加到导出表中。导出表是动态链接库中所包含的一个表格,记录了可以从其他模块加载和使用的函数、变量或对象的地址和信息。

    使用 __declspec(dllexport) 修饰符可以使您的函数、变量或对象可在 DLL 中使用,并提供给其他模块或程序进行调用。这对于将代码封装为动态链接库并与其他代码进行交互非常有用。

  2. __declspec(dllimport) 是微软的标记,用于在 C/C++ 编译器中指示某个函数或变量是从动态链接库(DLL)中导入的。它用于告诉编译器在链接时,要在指定的 DLL 中查找该函数或变量的定义,而不是在当前代码中进行定义。

    该标记的作用是实现动态链接库的函数和变量共享。通过使用 __declspec(dllimport) 标记,我们可以从其他编译单元(如另一个 DLL 或可执行文件)中使用其导出的函数或变量。

    使用该标记有助于提高代码的可维护性和可重用性。当我们在不同的模块或项目中使用相同的函数或变量时,可以将它们定义为动态链接库,并使用 __declspec(dllimport) 标记来引用它们,从而减少重复代码的编写。这样更易于修改和更新相应的功能,也减少了项目的二进制文件的大小。


ok,你已经懂得开发接口库大部分精髓了,当你完成了这个接口导出,你就已经可以不用管剩下的事情,只管编写这个算法库即可。

3.完成编译

我们右键一下我们的项目,编译之后,会生成一堆文件如下:
image

你需要做的就是把.lib文件和.dll,以及这个Test.h文件打个包,发送给软件开发即可。

当然了,你得写个文档,然后在SVN上把这个算法打个TAG包,就算你的工作阶段性完成了。

4.进阶 - 文档

不是,写文档也要我教啊?

5.进阶 - 写Demo

写Demo这个就比较简单了,就拿现在这个东西,我们先打个包出来看看

image

我们写一个Qt的程序来试试,Qt的程序比较简单,当然了你也可以直接写一个控制台程序(不推荐) 最好是可以有界面的,Qt的界面学起来也很简单,这个不懂我就也不多说了。

然后就是外部引用一些库,这个基本上和算法引用第三方库比如opencv或者pcl库差不多,只需要引用.lib文件,然后在代码中引用.h文件,即可正常调用这个开发的Project1.dll库

这里不过多阐述,之后有必要再来更新这部分。

6.进阶 - 类C接口

以后问起来再更新吧。