简介这份资源面向需要在 C 项目中集成 7-Zip 解压能力的开发者尤其适合使用 Visual Studio 环境、希望直接调用 7z SDK 而非依赖外部命令行工具的中高级程序员。资源包共 488 个文件以 186 个 .h 头文件与 136 个 .cpp 源文件为核心涵盖 Lzma 编解码、7z 归档读写等模块另含 40 个 .c 文件、71 个 .obj 编译产物及 dsp、dsw、sln、vcxproj 等工程配置整体约 2.9MB可直接对照搭建或复用。内容围绕 IInArchive 接口展开涉及库初始化、打开压缩包、遍历文件列表、按索引解压与错误码处理等关键环节并提示了非内存解压的 CPU 占用与速度权衡。已有 1822 人学习下载适合作为理解 7z SDK 调用流程、排查链接与接口使用问题的参考素材。1. 为什么我不再让 C 项目去 fork 一个 7z.exe上周帮一个做桌面端的朋友排查一个解压模块的崩溃问题现象很典型程序在客户机器上偶发卡死日志里没有任何异常最后定位到是CreateProcess调起外部7z.exe时路径里带空格加中文命令行拼接没转义进程创建失败但返回值没检查主线程还在死等管道输出。这类问题在「C 调用 7z SDK 解压」这个场景里几乎每周都能碰到一次。用外部进程解压本质上是把进程管理、路径编码、错误传播三件事全甩给了调用方而这三件事恰恰是 C 里最容易翻车的地方。7z SDK也就是 LZMA SDK提供的是进程内解压能力你不需要额外部署7z.exe不需要处理命令行转义压缩包格式支持 7z、zip、tar、gzip、bzip2、xz 等一大票。它适合谁适合那些要把解压能力嵌进安装器、资源加载器、日志归档工具、游戏补丁模块的 C 工程。代价是你要自己写几十行胶水代码把 SDK 的 C 风格接口包成能用的东西。这篇就按我实际拆包复现的顺序把这条路走一遍。2. 7z SDK 的接口分层从 C 接口到 C 封装到底怎么选2.1 先搞清楚 SDK 里到底有哪几层LZMA SDK 的源码目录结构不复杂但第一次打开容易懵。核心分三块C/目录下是纯 C 实现的压缩解压算法CPP/7zip/下是 7-Zip 自己的 C 封装CPP/Common/是一些基础工具类。很多人一上来就想用CPP/7zip/里的IInArchive接口觉得那是「官方 C 接口」结果被一堆 COM 风格的IUnknown、QueryInterface绕晕。我的建议是分场景选场景推荐接口理由只解压 7z/zip要格式自动识别CPP/7zip/Archive/IArchive.h的IInArchive支持多格式SDK 帮你判断只处理单文件 .xz/.lzmaC/LzmaDec.h的LzmaDecode无依赖代码量最小要嵌入安装器体积敏感C/目录裸接口可以只编译需要的 .c 文件要遍历压缩包内文件列表IInArchiveIArchiveOpenCallback能拿到每个 entry 的元信息大部分业务场景其实是第一种和第四种也就是走IInArchive。这条路的关键在于SDK 不提供现成的「解压到目录」函数它只给你「打开归档 → 遍历条目 → 读取流 → 写文件」这套原语剩下的目录创建、路径拼接、覆盖策略全要自己写。2.2 把 SDK 编进工程源码引入还是静态库我不推荐直接拿预编译的.lib因为 LZMA SDK 的版本和编译器 ABI 绑得很死MSVC 2019 编出来的库拿到 MSVC 2022 工程里链接轻则警告重则崩。常见做法是把C/和CPP/下需要的源文件直接拖进工程编译。最小文件集大概是这样# 以 7z 解压为例需要加入工程的源文件相对 SDK 根目录 C/7zCrc.c C/7zCrcOpt.c C/7zStream.c C/Alloc.c C/Bra.c C/Bra86.c C/BraIA64.c C/CpuArch.c C/Delta.c C/Lzma2Dec.c C/LzmaDec.c C/Ppmd7.c C/Ppmd7Dec.c C/7zBuf.c C/7zBuf2.c C/7zDec.c C/7zIn.c C/7zArcIn.c C/Bcj2.c C/Xz.c C/XzDec.c CPP/7zip/Archive/7z/7zHandler.cpp CPP/7zip/Archive/7z/7zIn.cpp CPP/7zip/Archive/7z/7zOut.cpp CPP/7zip/Archive/7z/7zUpdate.cpp CPP/7zip/Common/StreamUtils.cpp CPP/7zip/Common/InBuffer.cpp CPP/7zip/Common/OutBuffer.cpp CPP/7zip/Common/FileStreams.cpp CPP/7zip/Common/CreateCoder.cpp CPP/7zip/Common/CoderMixer2.cpp CPP/7zip/Common/MethodProps.cpp CPP/7zip/Common/PropId.cpp CPP/Common/MyString.cpp CPP/Common/MyWindows.cpp CPP/Common/StringConvert.cpp CPP/Common/UTFConvert.cpp CPP/Common/IntToString.cpp CPP/Common/NewHandler.cpp CPP/Common/StdInStream.cpp CPP/Common/StdOutStream.cpp这份清单不是绝对的取决于你实际用到的格式。如果只解 7zXz.c、XzDec.c可以去掉如果还要解 zip得再加CPP/7zip/Archive/Zip/下的文件。我一般会先按最小集加编译报缺符号再补比一次性全加进去然后被几百个编译错误淹没要高效。提示SDK 里大量文件用了#include StdAfx.h如果你的工程没开预编译头要么在项目属性里关掉要么给每个文件加一个空的StdAfx.h。这个坑我踩过报错信息完全不指向真实原因。2.3 用 IInArchive 打开归档的最小闭环先看打开归档这段这是所有后续操作的前提#include CPP/7zip/Archive/IArchive.h #include CPP/7zip/IPassword.h #include CPP/Common/MyCom.h #include CPP/7zip/Common/FileStreams.h // 创建 7z 格式的 InArchive 实例 static HRESULT CreateInArchive(const GUID formatId, IInArchive** archive) { // CreateArchiver 是 SDK 提供的工厂函数按 GUID 创建对应格式的处理器 return CreateArchiver(formatId, IID_IInArchive, (void**)archive); } // 打开一个 7z 文件 HRESULT OpenArchive(const std::wstring path, IInArchive** outArchive) { CInFileStream* fileStream new CInFileStream; CMyComPtrIInStream stream(fileStream); if (!fileStream-Open(path.c_str())) { return E_FAIL; // 文件打不开常见于路径含中文但用了窄字符 } CMyComPtrIInArchive archive; // k_7z 是 7z 格式的 GUID定义在 7zHandler.h 里 HRESULT hr CreateInArchive(k_7z, archive); if (hr ! S_OK) return hr; // 第二个参数是最大检查内存传 (UInt64)-1 表示不限制 hr archive-Open(stream, nullptr, nullptr); if (hr ! S_OK) return hr; *outArchive archive.Detach(); return S_OK; }这段代码里几个参数值得说清楚。CInFileStream是 SDK 自带的文件流封装它内部用的是 Windows 的CreateFileW所以路径必须传宽字符传std::string会在中文路径上直接失败而且失败得很安静。archive-Open的第二个参数是IArchiveOpenCallback*传nullptr表示不报告进度也不提供密码如果压缩包有密码这里必须传一个实现了ICryptoGetTextPassword的回调否则Open会返回E_ABORT。第三个参数是IInStream*一般传nullptrSDK 会用第一个参数那个流。3. 遍历条目与流式解压把每个 entry 落到磁盘3.1 拿到条目数量和每个条目的属性归档打开之后第一件事是问它里面有多少个条目UInt32 numItems 0; archive-GetNumberOfItems(numItems); for (UInt32 i 0; i numItems; i) { // 每个属性用 PROPID 枚举查询返回的是 PROPVARIANT CMyComBSTR name; PROPVARIANT prop; PropVariantInit(prop); // kpidPath 是条目在压缩包内的相对路径 archive-GetProperty(i, kpidPath, prop); if (prop.vt VT_BSTR) { name.Attach(prop.bstrVal); prop.vt VT_EMPTY; // 所有权转移避免重复释放 } // kpidIsDir 判断是不是目录 archive-GetProperty(i, kpidIsDir, prop); bool isDir (prop.vt VT_BOOL prop.boolVal ! VARIANT_FALSE); PropVariantClear(prop); // kpidSize 是解压后大小kpidPackSize 是压缩包内占用 // 这两个值可以用来做进度条和磁盘空间预检查 }kpidPath返回的路径分隔符在 7z 里统一是/在 Windows 上落盘前要换成\。这里有个安全细节压缩包里的路径可能包含..\如果直接拼接输出目录就会写到目标目录外面去这是典型的 Zip Slip 问题。我一般会做一次规范化把..段直接拒绝掉。3.2 用 Extract 回调把数据写出去SDK 的Extract方法接受一个IArchiveExtractCallback你需要自己实现这个接口。核心是三个方法SetTotal、SetCompleted、GetStream。GetStream在 SDK 准备解压第 N 个条目时被调用你要在这里返回一个可写的IOutStream。class ExtractCallback : public IArchiveExtractCallback, public ICryptoGetTextPassword, public CMyUnknownImp { public: CMyComPtrIInArchive archive; std::wstring outDir; UInt64 total 0; MY_UNKNOWN_IMP2(IArchiveExtractCallback, ICryptoGetTextPassword) // 返回第 index 个条目的输出流 STDMETHOD(GetStream)(UInt32 index, ISequentialOutStream** outStream, Int32 askExtractMode) { *outStream nullptr; // 先查这个条目是不是目录目录不需要流 PROPVARIANT prop; PropVariantInit(prop); archive-GetProperty(index, kpidIsDir, prop); bool isDir (prop.vt VT_BOOL prop.boolVal ! VARIANT_FALSE); PropVariantClear(prop); if (isDir) return S_OK; // 拼输出路径 CMyComBSTR name; archive-GetProperty(index, kpidPath, name); std::wstring fullPath outDir L\\ (const wchar_t*)name; // 创建目录多级 CreateDirectories(fullPath); COutFileStream* fileStream new COutFileStream; CMyComPtrISequentialOutStream stream(fileStream); if (!fileStream-Create(fullPath.c_str(), false)) { return E_ABORT; // 文件创建失败通常是权限或路径过长 } *outStream stream.Detach(); return S_OK; } STDMETHOD(SetTotal)(UInt64 total) { this-total total; return S_OK; } STDMETHOD(SetCompleted)(const UInt64* completeValue) { return S_OK; } // 密码回调没有密码时返回 S_FALSE STDMETHOD(CryptoGetTextPassword)(BSTR* password) { *password SysAllocString(L); return S_FALSE; } };调用的时候CMyComPtrExtractCallback cb new ExtractCallback; cb-archive archive; cb-outDir LD:\\output; // 第三个参数传 (UInt32)-1 表示解压全部条目 // 如果只想解压某几个传索引数组和数组长度 HRESULT hr archive-Extract(nullptr, (UInt32)-1, false, cb);Extract的第二个参数是UInt32* indices传nullptr配合第三个参数(UInt32)-1就是全解。第四个参数askMode传false表示不询问直接解压。如果你要做「只解压某个文件」的功能就把要解的条目索引放进数组传进去。3.3 进度回调与取消别让用户以为程序死了SetCompleted会被高频调用不要在里面做重活。我一般只更新一个std::atomicUInt64然后 UI 线程定时读。取消功能靠GetStream返回E_ABORT实现SDK 收到这个返回值会中止整个Extract并返回E_ABORT。注意E_ABORT和真正的错误要区分开前者是用户主动取消后者是磁盘满、权限不足这类问题。4. 避坑与排查五个让我加班到凌晨的现场4.1 中文路径解压出来是乱码现象压缩包内文件名是中文解压后文件名变成问号或乱码。原因7z 归档里的文件名以 UTF-16 存储但kpidPath返回的BSTR在某些 SDK 版本里需要经过UTFConvert转换直接当char*用就丢编码。解决统一用std::wstring接收落盘走CreateFileW。如果必须转窄字符用CPP/Common/UTFConvert.cpp里的ConvertUnicodeToUTF8别用WideCharToMultiByte配CP_ACP。4.2 大文件解压到一半内存暴涨现象解压一个 4GB 的 7z进程内存涨到 2GB 以上。原因COutFileStream默认带缓冲但如果你自己实现ISequentialOutStream时把每次Write的数据都攒在std::vector里就会全量驻留。解决Write里直接落盘不要缓存或者用 SDK 自带的COutFileStream它的缓冲是固定大小的。4.3 编译报一堆LNK2005重复符号现象链接阶段报Alloc.c里的g_Alloc重复定义。原因SDK 的C/Alloc.c和CPP/Common/下某些文件都定义了全局分配器同时编进工程就冲突。解决只保留一份通常保留C/Alloc.c把CPP/Common/里重复的实现排除。或者用#define Z7_ST关掉 SDK 的多线程分配器。4.4 密码压缩包 Open 直接返回 E_ABORT现象带密码的 7z 调Open就失败还没到Extract。原因Open阶段 SDK 需要验证密码IArchiveOpenCallback没传或者没实现ICryptoGetTextPassword。解决把密码回调实现在Open用的 callback 上而不是Extract的 callback 上。这两个是独立的接口很多人只实现了后者。4.5 解压完文件时间戳全是当前时间现象解压出来的文件修改时间不对。原因SDK 不会自动设置文件时间kpidMTime属性拿到了但没写回文件系统。解决在GetStream里拿到kpidMTime的FILETIME创建文件后用SetFileTime设置。注意PROPVARIANT里的时间是 UTC写之前要转本地时间。5. 进阶把解压封装成可复用的 C 类与验证清单走到这里基本功能已经能跑了。但要在工程里长期用我习惯把它封成一个类对外只暴露两个方法Open和ExtractAll。内部把IInArchive、回调、路径规范化全藏起来。这样调用方不需要知道PROPVARIANT是什么也不需要 include 任何 SDK 头文件。封装时有个细节值得单独说CMyComPtr是 SDK 自带的智能指针但它不是线程安全的引用计数用的是普通long。如果你的解压类要在多线程里共享得自己加锁或者每个线程一个实例。我一般选后者因为 7z 解压本身是 CPU 密集的多线程共享一个 archive 实例反而会互相拖慢。验证一个解压实现是否靠谱我固定跑这几项验证项构造方式期望结果中文路径压缩包内放「测试/文档.txt」解压后路径和文件名正确空目录压缩包里建一个空文件夹目录被创建不报错大文件单个 2GB 以上文件内存占用稳定不 OOM密码包带密码的 7z传对密码成功传错返回明确错误路径穿越条目名为..\..\evil.txt被拒绝或规范化到输出目录内磁盘满输出到小容量分区返回错误不崩溃不留半截文件最后一项「不留半截文件」需要额外处理GetStream创建文件后如果后续Extract失败已经写了一半的文件不会自动删除。我的做法是在回调里记录所有创建过的文件路径Extract返回非S_OK时统一清理。这个逻辑不复杂但漏掉的话用户会看到一堆损坏的半成品文件然后来问你为什么解压坏了。从那以后我每次接解压相关的需求都强制先把上面那张验证表跑一遍再联调业务代码。这套流程帮我省掉的返工时间远比写封装类花的时间多。希望帮到你。本文还有配套的精品资源点击获取