资讯详情

资讯详情

C++集成7z SDK解压实战:从命令行到原生接口的工程化方案

简介这份资源面向需要在 C 项目中集成 7-Zip 解压能力的开发者尤其适合使用 Visual Studio 2010 环境、希望直接调用 7z SDK 而非依赖外部命令行工具的中高级程序员。资源包共 488 个文件以 186 个 .h 头文件与 136 个 .cpp 源文件为主体涵盖 IInArchive 接口、LzmaEnc/LzmaDec 编解码、7zIn/7zDec 归档读写等核心模块另含 40 个 .c 文件、71 个 .obj 编译产物及 dsp、dsw、sln、vcxproj 等工程配置整体约 2.9MB可直接在 VS 中打开编译调试。已有 1822 人学习下载。通过该 SDK 源码读者能掌握初始化库、打开压缩包、遍历文件列表、按索引解压及错误码处理等完整流程并理解非内存解压的按需读取机制与 CPU 占用特点为多线程解压、进度显示等扩展需求提供可复用的底层参考。1. C 调用 7z SDK 解压为什么我不推荐你直接上命令行如果你在 Windows 上写过安装器、打包工具或者自动化部署脚本大概率绕不开一个需求在 C 程序里解压.7z文件。很多人的第一反应是system(7z x archive.7z)或者ShellExecute调一下7z.exe能跑就行。但真到了产品环境问题就来了——目标机器上不一定装了 7z命令行窗口会闪一下路径里有空格或中文就翻车返回值还拿不到具体错误码。这时候把 7z 的 SDK 直接编进你的 C 工程才是更稳的路子。7z SDK 本质上是 LZMA SDK 加上 7z 格式的封装官方提供了一套 C 接口核心是IInArchive、IOutArchive和ISequentialInStream这几个 COM 风格的抽象类。你不需要理解 LZMA 的区间编码细节只要按它的协议把文件流喂进去它就能把压缩包里的目录结构和数据吐出来。这套东西适合谁适合做桌面软件安装包、游戏资源热更新、嵌入式设备固件升级以及任何需要在无外部依赖环境下处理 7z 的场景。接下来我会按“先跑通最小解压、再处理回调与内存流、最后避坑”的顺序把这条链路拆开讲。2. 把 7z SDK 编进工程从源码目录到第一个可执行文件2.1 先搞清楚 SDK 里哪些文件必须留从官方渠道拿到的 7z SDK 压缩包解压后目录通常长这样C/是纯 C 实现的 LZMA 编解码CPP/是 C 封装CS/是 C# 版本。我们只关心CPP/7zip/下的内容。但整个CPP/7zip/目录很大全加进工程会拖慢编译还容易因为平台相关代码报错。我的做法是只保留下面这几块目录/文件作用是否必须CPP/7zip/Archive/7z/7z 格式的处理器必须CPP/7zip/Common/流、文件、进度等公共实现必须CPP/7zip/Compress/LZMA/LZMA2 等编解码器必须CPP/Common/字符串、文件 IO 等基础工具必须CPP/Windows/Windows 平台适配层Windows 下必须C/底层 C 编解码必须注意CPP/7zip/UI/和CPP/7zip/Bundles/是给独立 exe 用的SDK 集成时不要加否则会引入WinMain冲突。另外CPP/7zip/Archive/下其他格式如 zip、rar的目录也一并去掉只留7z能省掉大量无关符号。2.2 用 CMake 组织一个最小可编译工程手工往 Visual Studio 里拖文件很容易漏我一般用 CMake 把源码路径和宏定义固定下来。下面是一个能跑通的最小CMakeLists.txtcmake_minimum_required(VERSION 3.15) project(SevenZipDemo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 7z SDK 根目录按你实际放置位置改 set(SEVENZIP_ROOT ${CMAKE_SOURCE_DIR}/third_party/7zSDK) # 只收集必要的源文件避免把 UI 和 Bundles 编进来 file(GLOB_RECURSE SEVENZIP_SOURCES ${SEVENZIP_ROOT}/C/*.c ${SEVENZIP_ROOT}/CPP/7zip/Archive/7z/*.cpp ${SEVENZIP_ROOT}/CPP/7zip/Common/*.cpp ${SEVENZIP_ROOT}/CPP/7zip/Compress/*.cpp ${SEVENZIP_ROOT}/CPP/Common/*.cpp ${SEVENZIP_ROOT}/CPP/Windows/*.cpp ) add_executable(SevenZipDemo main.cpp ${SEVENZIP_SOURCES}) target_include_directories(SevenZipDemo PRIVATE ${SEVENZIP_ROOT} ${SEVENZIP_ROOT}/CPP ${SEVENZIP_ROOT}/C ) # 关键宏告诉 SDK 我们用的是 Windows 下的动态库方式 target_compile_definitions(SevenZipDemo PRIVATE UNICODE _UNICODE WIN32 _WINDOWS ) # 链接系统库 target_link_libraries(SevenZipDemo PRIVATE ole32 oleaut32 user32 advapi32 shell32 )这段 CMake 的逻辑说明file(GLOB_RECURSE ...)把四类源文件收进来其中C/*.c是底层算法必须用 C 编译器处理CMake 会自动按扩展名分派。target_compile_definitions里的UNICODE和_UNICODE是必须的因为 7z SDK 内部大量使用UString基于wchar_t不定义这两个宏会导致字符串类型不匹配编译期就报错。链接ole32和oleaut32是因为 SDK 里用到了 COM 的IUnknown和PROPVARIANT相关设施。参数怎么改如果你的 SDK 版本里CPP/Windows/目录不存在有些精简版把它合并到了CPP/Common/把那一行删掉即可。如果编译时报CreateObject未定义检查是否漏了CPP/7zip/Common/下的CreateCoder.cpp。2.3 写一个只做解压的 main.cpp工程能编译只是第一步真正要跑通解压需要按 7z SDK 的协议实现三个东西打开归档、遍历条目、把数据写到磁盘。下面是最小实现#include windows.h #include stdio.h #include CPP/7zip/Archive/IArchive.h #include CPP/7zip/IPassword.h #include CPP/Windows/PropVariant.h #include CPP/Common/MyCom.h #include CPP/7zip/Common/FileStreams.h // 用 COM 智能指针管理 IInArchive 生命周期 class InArchiveGuard { public: CMyComPtrIInArchive archive; ~InArchiveGuard() { if (archive) archive-Close(); } }; // 创建 7z 处理器通过 CLSID 拿到 IInArchive 接口 HRESULT Create7zArchive(IInArchive** out) { // CLSID 来自 7z 格式的注册信息SDK 里固定为这个 GUID const CLSID CLSID_CFormat7z { 0x23170F69, 0x40C1, 0x278A, {0x10, 0x00, 0x00, 0x01, 0x10, 0x07, 0x00, 0x00} }; return CoCreateInstance(CLSID_CFormat7z, NULL, CLSCTX_INPROC_SERVER, IID_IInArchive, (void**)out); } int wmain(int argc, wchar_t* argv[]) { if (argc 3) { wprintf(L用法: SevenZipDemo archive.7z outdir\n); return 1; } CoInitialize(NULL); InArchiveGuard guard; HRESULT hr Create7zArchive(guard.archive); if (hr ! S_OK) { wprintf(L创建 7z 处理器失败: 0x%08X\n, hr); return 2; } // 用文件流打开压缩包 CInFileStream* fileStream new CInFileStream; CMyComPtrIInStream inStream fileStream; if (!fileStream-Open(argv[1])) { wprintf(L无法打开压缩包: %s\n, argv[1]); return 3; } // 打开归档第二个参数是最大检查大小传 NULL 表示不限制 hr guard.archive-Open(inStream, NULL, NULL); if (hr ! S_OK) { wprintf(L打开归档失败: 0x%08X\n, hr); return 4; } UInt32 numItems 0; guard.archive-GetNumberOfItems(numItems); wprintf(L压缩包内条目数: %u\n, numItems); // 这里先只打印条目名验证链路通了 for (UInt32 i 0; i numItems; i) { CMyComBSTR name; NWindows::NCOM::CPropVariant prop; guard.archive-GetProperty(i, kpidPath, prop); if (prop.vt VT_BSTR) { wprintf(L [%u] %s\n, i, prop.bstrVal); } } guard.archive-Close(); CoUninitialize(); return 0; }逻辑说明CoCreateInstance那一步是 7z SDK 的入口它内部会根据 CLSID 找到CArchive的工厂函数并返回IInArchive。CInFileStream是 SDK 自带的文件流实现直接new出来用CMyComPtr接管引用计数。Open的第二个参数是IArchiveOpenCallback*最小场景传NULL即可但如果压缩包有密码或需要进度反馈就必须传一个自定义回调。GetProperty配合kpidPath拿到的是BSTR在UNICODE宏下就是宽字符串。参数说明kpidPath是属性 IDSDK 里还有kpidSize、kpidIsDir、kpidAttrib等后面提取文件时会用到。GetNumberOfItems返回的是归档内所有条目包括目录项所以遍历时要用kpidIsDir判断否则会把目录当文件处理。3. 真正把文件写出来回调、内存流与进度控制3.1 实现 IArchiveExtractCallback 才能拿到数据上面那段代码只打印了文件名没有解压。7z SDK 的设计是你调用Extract它反过来调用你提供的回调在回调里把数据写到目标位置。所以必须实现IArchiveExtractCallback接口。这个接口有五个方法其中GetStream和SetOperationResult是核心。class ExtractCallback : public IArchiveExtractCallback, public ICryptoGetTextPassword, public CMyUnknownImp { public: MY_UNKNOWN_IMP2(IArchiveExtractCallback, ICryptoGetTextPassword) // 关键返回一个 ISequentialOutStreamSDK 会把解压数据写进去 STDMETHOD(GetStream)(UInt32 index, ISequentialOutStream** outStream, Int32 askExtractMode) { *outStream nullptr; _currentIndex index; // 先拿到条目路径和是否为目录 NWindows::NCOM::CPropVariant prop; _archive-GetProperty(index, kpidPath, prop); if (prop.vt ! VT_BSTR) return S_OK; // 没有路径的条目跳过 UString fullPath _outDir L\\ prop.bstrVal; // 把路径里的 / 换成 \\7z 内部用正斜杠 fullPath.Replace(L/, L\\); NWindows::NCOM::CPropVariant isDirProp; _archive-GetProperty(index, kpidIsDir, isDirProp); bool isDir (isDirProp.vt VT_BOOL isDirProp.boolVal ! VARIANT_FALSE); if (isDir) { // 目录直接创建不返回流 CreateDirectoryW(fullPath, NULL); return S_OK; } // 确保父目录存在 int pos fullPath.ReverseFind(L\\); if (pos 0) { UString parent fullPath.Left(pos); CreateDirectoryW(parent, NULL); } // 创建输出文件流 COutFileStream* outFile new COutFileStream; if (!outFile-Create(fullPath, false)) { delete outFile; return E_FAIL; } _outFileStream outFile; *outStream outFile; return S_OK; } STDMETHOD(SetOperationResult)(Int32 opRes) { // 每个条目处理完后释放流 _outFileStream.Release(); return S_OK; } // 密码回调无密码时返回 S_FALSE STDMETHOD(CryptoGetTextPassword)(BSTR* password) { *password nullptr; return S_FALSE; } void Init(IInArchive* archive, const UString outDir) { _archive archive; _outDir outDir; } private: CMyComPtrIInArchive _archive; CMyComPtrISequentialOutStream _outFileStream; UString _outDir; UInt32 _currentIndex 0; };逻辑说明MY_UNKNOWN_IMP2是 SDK 提供的宏用来实现IUnknown的QueryInterface、AddRef、Release两个参数对应它实现的两个接口。GetStream里先判断目录目录直接CreateDirectoryW然后返回S_OK且不设置*outStreamSDK 收到空流就知道这是目录。文件则创建COutFileStream并赋给*outStreamSDK 后续会把解压数据写进这个流。SetOperationResult在每个条目结束后被调用这里释放流避免文件句柄泄漏。参数说明askExtractMode通常是NArchive::NExtract::NAskMode::kExtract表示正常解压如果是kTest则只测试不写盘。COutFileStream::Create的第二个参数false表示不创建临时文件直接写目标路径。3.2 调用 Extract 并处理返回值有了回调主流程里加一行Extract就能真正解压ExtractCallback* callback new ExtractCallback; CMyComPtrIArchiveExtractCallback extractCallback callback; callback-Init(guard.archive, argv[2]); // 第三个参数是索引数组NULL 表示全部条目 // 第四个参数 false 表示不测试第五个参数是回调 hr guard.archive-Extract(NULL, (UInt32)(Int32)-1, false, extractCallback); if (hr ! S_OK) { wprintf(L解压失败: 0x%08X\n, hr); return 5; } wprintf(L解压完成\n);这里(UInt32)(Int32)-1是 SDK 约定的“全部条目”写法等价于0xFFFFFFFF。Extract的第二个参数是条目数量传这个值表示处理所有。如果只想解压特定文件可以传一个UInt32数组和实际数量。3.3 用内存流替代文件流做二次处理有些场景不想落盘比如解压后直接加载到内存做校验。这时候把COutFileStream换成自定义的ISequentialOutStream实现即可class MemoryOutStream : public ISequentialOutStream, public CMyUnknownImp { public: MY_UNKNOWN_IMP1(ISequentialOutStream) std::vectorBYTE data; STDMETHOD(Write)(const void* buf, UInt32 size, UInt32* processed) { if (processed) *processed size; const BYTE* p (const BYTE*)buf; data.insert(data.end(), p, p size); return S_OK; } };在GetStream里把COutFileStream替换成MemoryOutStream解压完直接读data就行。注意内存流没有大小上限遇到超大文件要自己做分片或落盘否则容易触发 OOM。4. 避坑记录那些让我加班到凌晨的细节4.1 现象编译报LNK2019 unresolved external symbol _WinMain原因把CPP/7zip/UI/或Bundles/下的文件加进了工程这些文件里包含WinMain入口和你的main/wmain冲突。解决只保留Archive/7z、Common、Compress、Windows四个目录UI 和 Bundles 一律不加。4.2 现象解压出来的中文文件名变成乱码原因7z SDK 内部用UStringwchar_t但如果你在GetStream里用std::string拼接路径宽窄字符转换时丢了编码。解决全程用UString和CreateFileW不要中途转std::string。如果必须转用WideCharToMultiByte指定CP_UTF8不要用CP_ACP。4.3 现象Extract返回S_OK但文件是空的原因GetStream里创建了COutFileStream但没有正确设置*outStream或者SetOperationResult里提前Release了流。解决检查*outStream outFile;这行是否在return S_OK之前执行并且_outFileStream的Release只在SetOperationResult里做。4.4 现象带密码的压缩包解压时卡死原因没有实现ICryptoGetTextPasswordSDK 在需要密码时反复调用CryptoGetTextPassword如果返回S_FALSE且没有密码它会一直重试。解决实现该接口有密码时返回S_OK并填充BSTR无密码时返回S_FALSE并确保*password nullptr。另外在Open时传入IArchiveOpenCallback的密码回调否则打开阶段就会失败。4.5 现象Release 编译下解压大文件崩溃原因GetStream里用了new创建流对象但没有用CMyComPtr管理SetOperationResult里Release后 SDK 可能还持有引用。解决所有流对象都用CMyComPtr包装GetStream里赋给*outStream后不要手动delete让引用计数归零时自动释放。5. 进阶把解压封装成可复用的类与验证清单走到这里最小链路已经通了。但要在真实项目里用我一般会把它封装成一个SevenZipExtractor类对外只暴露Extract(const std::wstring archive, const std::wstring outDir)一个方法内部把CoInitialize、CInFileStream、ExtractCallback的生命周期都管起来。这样调用方不需要知道IInArchive和kpidPath这些细节。封装时有一个容易忽略的点CoInitialize和CoUninitialize必须成对出现而且如果调用方线程已经初始化过 COM重复调用会返回S_FALSE这时候不应该再调CoUninitialize。我的做法是在类构造时记录CoInitialize的返回值只有返回S_OK时才在析构里调CoUninitialize。验证解压结果是否完整我习惯用三个指标条目数量是否和GetNumberOfItems一致、每个文件的kpidSize和实际写盘大小是否相等、目录结构是否和压缩包内kpidPath一致。下面这个检查函数可以直接嵌到测试里bool VerifyExtract(IInArchive* archive, const UString outDir) { UInt32 numItems 0; archive-GetNumberOfItems(numItems); for (UInt32 i 0; i numItems; i) { NWindows::NCOM::CPropVariant pathProp, sizeProp, isDirProp; archive-GetProperty(i, kpidPath, pathProp); archive-GetProperty(i, kpidSize, sizeProp); archive-GetProperty(i, kpidIsDir, isDirProp); if (pathProp.vt ! VT_BSTR) continue; if (isDirProp.vt VT_BOOL isDirProp.boolVal ! VARIANT_FALSE) continue; UString fullPath outDir L\\ pathProp.bstrVal; fullPath.Replace(L/, L\\); HANDLE h CreateFileW(fullPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (h INVALID_HANDLE_VALUE) return false; LARGE_INTEGER fileSize; GetFileSizeEx(h, fileSize); CloseHandle(h); if (sizeProp.vt VT_UI8 fileSize.QuadPart ! (LONGLONG)sizeProp.uhVal.QuadPart) return false; } return true; }这个函数遍历归档内所有非目录条目逐个检查磁盘上是否存在、大小是否匹配。kpidSize返回的是VT_UI8类型用uhVal.QuadPart取值。如果压缩包里有零字节文件sizeProp.vt可能是VT_EMPTY这时候跳过大小比较即可。最后说一个我踩过的坑7z SDK 的Extract在遇到损坏条目时默认行为是继续处理后续条目并最终返回S_FALSE而不是直接失败。如果你在自动化流程里只判断hr ! S_OK会把部分失败当成完全成功。我的习惯是在SetOperationResult里记录每个条目的opRes最后统一检查有没有非S_OK的结果。这个习惯帮我拦下过好几次“看起来解压完了、实际少了几个文件”的问题。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →