我平时有在做一些开源软件。

有些时候,软件做好打包出来并不是单个EXE文件,而是一个EXE文件跟着好多个依赖文件,比如资源文件,DLL文件。如果遇到手欠的用户误删一下,还来问你为啥不能运行,那真的是马上就能治好你的低血压。

要是能把exe和依赖文件什么的,都一起打包成单个exe文件就好了,这样分发起来就会方便很多,看起来也会整洁很多。


在GitHub上找了一些开源的项目,但都不能满足我的需要,就自己做了一个。

原理其实也比较简单,把我的软件文件夹打成压缩包,然后把压缩包的二进制数据追加到这个EXE文件的末尾。

在用户双击运行的时候,读取文件末尾的压缩包数据,把文件解压到临时目录,然后从临时目录运行我的软件主程序。这样就做到了把一个目录打包成一个exe文件。


这是我第一次拿C++ 做完整的项目,过程中其实还是碰到了许多坑的,也是第一次相对完整地了解到C++ 的开发流程。不得不说C++的确是一门极其复杂的语言,有各种繁琐概念,机制,语法。而且开发过程中,我有相当一部分时间是在做内存管理,确保这个地方内存正确回收,确保那个对象的生命周期不会过短之类的问题。

其中有些有意思的地方,可以单独拿出来讲讲,避免后面的同学踩坑。

为C++增加调用堆栈回溯的支持

首先。C++不像Java,Python,C#之类的高级语言是有原生调用堆栈回溯的。

是的,连调用堆栈也看不到。也就是说,如果你代码出了BUG,你连Bug出在代码哪一行都没法知道,只能不断地根据问题现象,不断地注释代码,不断地的排除,真的很折磨人。

但好在Windows为我们提供了调用堆栈回溯的API,我们只需要调一调API就能知道当前代码执行到哪里了,是从哪里执行过来的。

具体的原理是VC++编译器在编译的时候,除了输出PE文件以外,还会输出一个同名PDB文件,PDB全程是ProgramDatabase,这是一个映射文件,或者说是一个字典,当程序在执行的时候,可以通过当前指令的地址,用查表法,找到对应的源代码文件,函数名以及代码行数。

C++11的移动构造函数

再然后就是C++11加入的移动构造函数,移动赋值函数。

比如下面的代码,在C++11以前,在函数a里面被创建的my_obejct临时对象在返回时会被赋值给obj变量,这时候my_obejct拷贝构造函数会被调用,一个新的my_obejct对象会被创建出来,然后用临时对象作为参数去调用拷贝构造函数。这样做其实是效率很低下的,遇到构造和析构开销的大的Class性能下降会非常严重。

void a()
{
    return my_object();
}

my_object obj = a();

其实只需要把临时对象直接赋值给obj对象就好了,中间不需要经过一个创建又销毁的过程。(创建对象本身性能开销不大,而性能开销大的构造函数,因为可能要分配内存,做各种初始化操作)

移动构造函数就是做这个事情的,在移动构造函数中,需要把一个对象的数据转移到另一个对象里,就不用走普通构造->析构函数了,这样会快得多

class my_object
{
    int* p;
    
    my_object(my_object&& other)
    {
        p = other.p;
        other.p = nullptr;
    }
}

不得不说C++对细节的控制能力是真的强。就连传递参数,lambda捕获外部变量,你都可以手动选择是按引用捕获,还是按值捕获,当然用的不好也会出现各种野指针读写的问题。

单实例机制

为什么要做单实例机制,因为有的应用程序同一时间只允许有一个进程,如果有多个进程存在,那么对同一个文件进行写入时,会发生一个进程写入成功,一个失败的问题。这时候到底是成功了呢?还是失败了呢。

这时候就需要加入单实例锁的机制,当App运行起来之后,会先检查一下有没有其它正在运行的进程,如果有,那么自己就是后启动的进程,就会直接退出运行,然后把窗口焦点给到那个正在运行的进程窗口上。如果没有,那么说明自己就是第一个进程,接下来正常运行App的主逻辑就好了。

具体实现主要是靠WindowsAPI提供的互斥锁,可以用CreateMutexA()函数来申请一个互斥锁,参数是一个char*类型的,第一次申请会申请成功,第二次申请会申请失败,由此可以判断当前进程是第一个进程,还是第二个进程,如果是第二个进程,那么就直接退出运行。

当第一个进程结束的时候,操作系统会自动回收第一个进程的互斥锁,确保后面的新进程能够正常申请,这样就可以保证App永远只有一个进程在运行了

小结

其实做这个软件主要还是为了方便自己以后做一些新的软件,毕竟一个文件夹打包成单个EXE之后移动复制什么的会方便很多。

这个过程中也学到了一些Windows的开发知识,简单了解了下PE文件的结构。其实PE文件就是我们常见的EXE文件,只不过它的专业名词叫Portable Executable(PE)。

也知道了PE文件里不仅能包含程序,代码,也能包含资源。像PE文件的图标,版本号,版权声明等信息就是存储在PE资源里的。当然你也可以存一些自定义的二进制数据。

如果有感兴趣的朋友,可以到GitHub仓库找到软件的源代码,也可以在Release中直接下载打包好的EXE文件,在README中也增加了常用参数说明。

在此还要额外感谢以下开源项目:

  • zlib:压缩算法支持库
  • wingetopt:CLI参数解析库
  • cjson:JSON解析和生成库
  • md5:MD5散列算法支持
  • StackWalker:CPP的调用堆栈支持库