65.9K
CodeProject 正在变化。 阅读更多。
Home

Managed C++ ZLib 封装

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (13投票s)

2004年7月6日

CPOL

9分钟阅读

viewsIcon

159526

downloadIcon

5780

ZLib的.NET包装器,用MC++编写

zlibwrapper.gif

引言

在本文中,我将介绍一个托管类型命名空间,该命名空间提供了对ZLib导出的一些标准功能的包装器。ZLib是一个众所周知的免费、通用的无损数据压缩库,可用于任何操作系统(1)。

背景

Visual C++允许您将托管代码和非托管代码合并到同一个程序集中。下面的示例演示了混合使用非托管(原生)代码和托管代码的用法。这种技术在构建包装器围绕非托管类型的托管类型时非常有用,它允许您迁移代码并仍然保持良好的效率。混合代码的另一个好处是,就像在本例中一样,对原生代码层(尤其是由其他供应商提供的代码)的错误修复或改进非常容易处理,在大多数情况下只需要重新编译项目即可。付出的代价是VC++ .NET代码无法进行可验证的类型安全(可以尝试使用peverify.exe工具)。混合代码的另一个怪癖来自于标准库的静态结构初始化,这些库通常与CRT、ATL和MFC等原生模块链接。这个问题的解决方案来自一篇MSDN文章(2),但它几乎是曲折的。我听说这个问题将在下一版本的.NET Framework中得到解决。在等待下一版本.NET Framework的同时,最好不要使用这些库中的静态结构,甚至更好的是根本不使用这些库(后者更难)。

调用非托管代码

关于从托管代码调用非托管代码的说明。ZStream类使用内部(托管)字节缓冲区来重现流行为。为了使用托管缓冲区与ZLib库函数,我们必须向函数提供一个指向托管堆的固定指针。这可以防止托管缓冲区被垃圾回收器移动。固定托管对象的某个部分会固定整个对象。因此,如果数组的任何元素被固定,那么整个数组也会被固定。这促使我们编写了以下代码

BYTE __pin * pBuffer = &buffer[0];
BYTE __pin * pSource = &source[0];

int nRes = compress2(pBuffer, & length, pSource, sourceLen, level);

一旦固定指针超出范围或指针设置为零,托管对象就会被取消固定。

使用代码

DevelopDotnet.Compression命名空间包含许多专用于压缩任务的类型。要使用其中一种类型,只需在项目中引用该组件,并在源文件开头插入以下声明

//
// Compression types
//
using DevelopDotNet.Compression;

ZStream封装了压缩流的功能。它继承自System.Stream .NET Framework类,可用于压缩流以及解压缩压缩流。类构造函数接受要管理的基流,以及可选的其他参数,以确定ZStream是否可以读取(解压缩)数据,或写入(压缩)数据。请注意,ZStream是顺序流,因此不支持Seek。

// Read only Stream (it can only decompress)
ZStream(Stream stream);


// Read or Write Stream depending on the boolean parameter write
// If write = true this stream can only Write 
ZStream(Stream stream, bool write);


// Write only Stream  (it can only compress)
ZStream(Stream stream, CompressionLevel level);
ZStream(Stream stream, CompressionLevel level, CompressionStrategy strategy);

以下代码行代表将二进制数据压缩到文件中的标准方法。

//
// Serializing dataset object
//

FibonacciDataSet ds = (FibonacciDataSet) GenerateFibonacciData();

fs = new FileStream(sFileName, FileMode.Create);

ZStream compressor = new ZStream(fs, true);

BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(compressor, ds);

要从压缩流中重新生成数据集对象,请打开压缩数据文件,然后将其附加到ZStream对象,并使用BinaryFormatter类的Deserialize方法。

//
// Deserializing data
//
fs = new FileStream(sFileName, FileMode.Open);


ZStream decompressor = new ZStream(fs);


BinaryFormatter bf = new BinaryFormatter();
FibonacciDataSet ds = (FibonacciDataSet) bf.Deserialize(decompressor);

dataGrid1.DataSource = ds;

ZCompressor公开了两个静态方法:Compress和Uncompress,用于快速压缩和解压缩字节缓冲区。请注意,Compress需要与被压缩数据占用的内存量相同的内存,而Uncompress可能会需要非常大的内存分配,具体取决于压缩数据的来源。这是因为解压缩算法在解压缩过程中动态分配缓冲区来容纳解压缩后的数据。

string sData = txtData.Text;

try
{
    Encoding encoder = Encoding.UTF7;
    byte [] compressedData = ZCompressor.Compress(encoder.GetBytes(sData), 
        CompressionLevel.BestCompression);

    txtData.Text = encoder.GetString(compressedData);
}
catch(ZException err)
{
    txtData.Text = err.Message;
}

DevelopDotnet.Compression命名空间还公开了适用于校验和的类型。它们是Adler32CRC32,以及一个接口IChecksumAdler32CRC32托管类型都实现了IChecksum接口。编写实现IChecksum接口的不同具体类型将实现多态性。也就是说,Adler32CRC32都可以对数据缓冲区执行校验和,但校验和执行方式仅取决于具体类型的实现,只要它们都通过相同的接口进行控制。

public __gc __interface IChecksum
{
    __property unsigned long get_Checksum();

    unsigned long Update(unsigned char buffer __gc[]);
    unsigned long Update(unsigned char buffer __gc[], int offset, int count);
};

这使我们能够轻松地通过编写基于IChecksum接口的通用访问代码来配置应用程序使用校验和功能,而忽略校验和的执行方式。

Adler32 crc = new Adler32();
DoChecksum(txtFile.Text, crc);
lblAdler.Text = crc.Checksum.ToString("X");

CRC32 crc = new CRC32();
DoChecksum(txtFile.Text, crc);
lblCrc.Text = crc.Checksum.ToString("X");

// ...

private long DoChecksum(string sFile, IChecksum chk)
{
    FileStream fs = null;
    long checksum = 0;

    try
    {
        fs = new FileStream(sFile, FileMode.Open);

        Byte [] data = new Byte[16384];

        while(fs.Read(data, 0, 16384) > 0)
        {
            checksum = chk.Update(data);
        }
    }
    catch(Exception err)
    {
        MessageBox.Show(err.Message, "Error", MessageBoxButtons.OK, 
                                MessageBoxIcon.Error);
    }
    finally
    {
        if(null != fs)
            fs.Close();
    }

    return checksum;
}

ZException扩展了System.Exception类,为Compression命名空间提供了一些机制,以便将ZLib错误翻译到托管世界。Compression命名空间中直接使用ZLib库的每个导出的类型都应该抛出ZException,以便让应用程序知道库内部发生了“崩溃”。

最后,我非常自豪地向您介绍最后一个类型,女士们、先生们……ZLib。这个小类型只有两个公共静态属性。第一个是Version,它返回嵌入托管代码中的ZLib库的版本字符串,最后一个是CompiledFlags。CompiledFlags是一个CompileOptions类型的枚举,它具有Flags属性,代表ZLib库源代码在编译时选择的选项。

Zip文件支持

从版本1.1.0.0开始,该库支持Zip归档文件,根据PKWARE标准,如zip文件格式规范文档(3)所述。由于ZLib目前仅支持deflated压缩方法,但这种方法是zip归档最常用的方法。Compression库公开了以下与zip归档相关的托管类型:ZipCompressionMethodZipFileEncryptionZipArchiveZipEntryZipEntryCollectionZipExceptionZipBadCrc32ExceptionZipFailEventHandlerZipFailEventArgsZipProgressEventHandlerZipProgressEventArgsZipRecoveryActionZipStoreFilePath。ZipArchive类可以通过ZipFail事件引发归档操作期间发生的错误的通知。注册此事件的客户端可以最终决定是停止操作,还是给它第二次机会,甚至对用户隐藏错误。请参阅ZTest应用程序以了解Fail事件可以做什么。Progress事件在正常操作流程中通知用户。此事件携带有关当前操作状态和完成百分比的信息,以及用于中止操作的标志。

以下是一些有趣的片段代码,展示了该库在zip归档上执行的一些任务的标准用法

将文件添加到Zip归档

string archiveFile = "test.zip";
using(ZipArchive archive = new ZipArchive(archiveFile, FileMode.OpenOrCreate, 
                                          FileAccess.ReadWrite))
{
    // The simplest method to add file in archive. 
    archive.Add("file.txt");

    
    bool recursive = true;
    // Adding all the files contained in a folder.
    archive.Add(@"C:\Temp", recursive);
    

    // Another method to add a files.
    ZipEntry entry = new ZipEntry(@"C:\Temp");
    archive.Entries.Add(entry, recursive);

}

从Zip归档中删除文件

string archiveFile = "test.zip";
using(ZipArchive archive = new ZipArchive(archiveFile, FileMode.Open, 
                                          FileAccess.ReadWrite))
{
    // Remove from the zip archive all the entries contained in the given folder
    archive.Remove("Temp", true);
}

做一些有用的测试…

string archiveFile = "test.zip";
using(ZipArchive archive = new ZipArchive(archiveFile, FileMode.Open, 
                                          FileAccess.Read))
{
    archive.Fail += new ZipFailEventHandler(OnZipFail);
    archive.Progress += new ZipProgressEventHandler(OnZipProgress);
    if(archive.Test())
    {
        Console.WriteLine("{0} test successfully completed.", archiveFile);
    }
}

提取归档内容

string archiveFile = "test.zip";
using(ZipArchive archive = new ZipArchive(archiveFile, FileMode.Open, 
                                          FileAccess.Read))
{
    // Set some flag to perform extraction
    archive.OverwriteExisting = true;
    archive.BuildTree = true;
    // Get error notification.
    archive.Fail += new ZipFailEventHandler(OnZipFail);
    // Go with extract
    archive.ExtractTo(@"C:\Temp");
}

ZipArchive类使用.NET可查找流来访问Zip归档。这使我们能够即时创建内存中的zip归档,并在需要顺序执行多个操作时加快速度。

MemoryStream archiveStream = new MemoryStream();

ZipArchive archive = new ZipArchive(archiveStream);

try
{
    archive.Add("file1.txt");
    archive.Add("file2.pdf");
    archive.Comment = "Memory Archive Test";
}
catch(ZipException)
{
    MessageBox.Show("Something goes wrong while adding files to the archive.");
}
//
// Copy memory stream to file
//

using(FileStream file = new FileStream("memory.zip", FileMode.Create, 
                                       FileAccess.Write))
{
    StreamUtilities.StreamCopy(archiveStream, 0, file);
}

改进

总有时间做到最好,那么何不让库管理SFX归档、密码和其他东西呢?该库是免费软件,并附带源代码。目前正在DevelopDotNet http://www.developdotnet.com%20(4/4)开发。在那里,您将找到关于该库的论坛以及报告错误或请求更改的可能性。

关注点

好的,那时,当我拥有ZStream的能力时,我希望有一些东西可以压缩来进行一些测试。向上向下搜索,我偶然发现一本老式书,从中我找到了获得很多数字的方法:斐波那契数列。我希望您不会因为下面关于斐波那契数列的小故事而感到厌烦。1202年,Leonardo Pisano,也称为Leonardo Fibonacci,出版了一本伟大的数学书:《Liber Abaci》。在这本书中,Fibonacci讨论了著名的兔子繁殖问题,这是Sigler教授(5)对Fibonacci原始陈述的翻译。

一对兔子一年能产生多少对。

某人将一对兔子放在一个封闭的地方,并想知道在一年内它们能产生多少对,因为它们的本性是,在一个月内它们会再生一对,而在第二个月,新生的兔子也会繁殖。因为上面提到的这对兔子在一个月内繁殖了,所以你将它翻倍;一个月内将有两对。其中一对,即第一对,在第二个月繁殖,因此在第二个月有3对;其中一对在第一个月繁殖,在第二个月生育了另一对,因此在第二个月有3对;在这3对中,有2对怀孕,并在第三个月生育了2对兔子,因此有5对;在这3对中,有2对怀孕,并在第三个月生育了2对兔子,因此有5对;在这5对中,有3对怀孕,并在第四个月生育了3对兔子,因此有8对;在这5对中,有3对怀孕,并在第四个月生育了3对兔子,因此有8对;其中5对兔子再生了5对;这些兔子加上8对兔子,构成第五个月的13对;在这5个月中出生的这5对兔子在本月不交配,但另有8对兔子怀孕,因此在第六个月有21对兔子;这13对兔子在第七个月出生,并添加到其中;本月将有34对兔子;这21对兔子在第八个月出生,并添加到其中;本月将有55对兔子;这34对兔子在第九个月出生,并添加到其中;本月将有89对兔子;在第十个月出生的55对兔子再次添加到其中;本月将有144对兔子;在第十一个月份出生的89对兔子再次添加到其中;本月将有233对兔子。

在最后一个月出生的144对兔子仍然添加到其中;届时将有377对兔子,这便是上述那对兔子在一年结束时在该地点产生的数量。

你可以在旁边看到我们是如何操作的,即我们将第一个数字加到第二个数字,即1加到2,第二个加到第三个,第三个加到第四个,第四个加到第五个,依此类推,直到我们将第十个数字加到第十一个数字,即144加到233,我们得到了上面写的兔子总数,即377,这样你就可以按顺序找到无数个月的兔子数量。

这个问题的解决方案来自于一个通项公式为

F(n) = F(n-1) + F(n-2),初始条件为:F(0) = 0 且 F(1) = 1。

1, 1, 2, 3, 5, 8, 11, ...,

很有趣,不是吗?这个数列的通用项可以通过递归计算,但这在计算时间上很昂贵,所以我将问题简化为一条直线,即简单地保存数列的最后两项,然后将它们相加得到最后一项。

参考文献

(1) ZLib主页 http://www.gzip.org/zlib/
(2) Converting Managed Extensions for C++ Projects from Pure Intermediate Language to Mixed Mode - Managed Extensions for C++ Reference
(3) PKWARE白皮书 ZIP文件格式规范
(4) 在 http://www.developdotnet.com/ 下载DevelopDotNet最新的源代码。
(5) L. E. (Laurence E.) Sigler:“Fibonacci's Liber Abaci: A Translation into Modern English of Leonardo Pisano's Book of Calculation”,Springer。

历史

  • 2005年2月28日:版本1.1.0.0(添加了Zip文件支持和StreamUtilities。字符串在资源中。)
  • 2004年6月21日:版本1.0.0.0
© . All rights reserved.