分享一次与SharpDX坑爹Bug刚正面的过程

和SharpDX坑爹的Variant刚正面

几个月前我写了这篇文章《.NET中生成动态验证码》文章,其实里面藏着一个大坑。运行里面的代码,会发现运行的 gif图片并没有循环播放: 

细心的网友也注意到了这个问题:

……但后来他备注说“已解决”,我当时也不知道该怎么解决的,所以我追问了一下,但他一直没有回复。但思路肯定是有的,再不济,也可以将保存为字节数组的数据,用其它的库进行重新解析,然后指定循环次数即可,当然这个方法肯定很搓,想象中较好的办法应该是调用 SharpDX内置的 API来完成。

踩坑之路

就此我开始了 SharpDX的踩坑之路,我找到了许多资料,找到不少示例代码,最后不断实验,最终成功。

C++示例

首先我在网上找到了 SharpDX生成循环 gif文件的 C++开源代码示例,代码源自于https://github.com/GarethRichards/GifSaver/blob/master/GifSaver.cpp:

  1. PROPVARIANT propValue; PropVariantInit(&propValue);

  2. propValue.vt = VT_UI1 | VT_VECTOR;

  3. propValue.caub.cElems = 11;

  4. DX::ThrowIfFailed(m_imagingFactory->CreateEncoder(GUID_ContainerFormatGif, &GUID_VendorMicrosoft, &m_wicBitmapEncoder));

  5. DX::ThrowIfFailed(m_wicBitmapEncoder->Initialize(m_stream.Get(), WICBitmapEncoderNoCache));

  6. ComPtr<IWICMetadataQueryWriter> pEncoderMetadataQueryWriter;

  7. DX::ThrowIfFailed(m_wicBitmapEncoder->GetMetadataQueryWriter(&pEncoderMetadataQueryWriter));

  8. string elms = "NETSCAPE2.0";

  9. propValue.caub.pElems = const_cast<UCHAR *>(reinterpret_cast<const UCHAR *>(elms.c_str()));

  10. DX::ThrowIfFailed(pEncoderMetadataQueryWriter->SetMetadataByName(L"/appext/Application", &propValue));

  11. // Set animated GIF format

  12. propValue.vt = VT_UI1 | VT_VECTOR;

  13. propValue.caub.cElems = 5;

  14. UCHAR buf[5];

  15. propValue.caub.pElems = &buf[0];

  16. *(propValue.caub.pElems) = 3; // must be > 1,

  17. *(propValue.caub.pElems + 1) = 1; // defines animated GIF

  18. *(propValue.caub.pElems + 2) = 0; // LSB 0 = infinite loop.

  19. *(propValue.caub.pElems + 3) = 0; // MSB of iteration count value

  20. *(propValue.caub.pElems + 4) = 0; // NULL == end of data

  21. DX::ThrowIfFailed(pEncoderMetadataQueryWriter->SetMetadataByName(L"/appext/Data", &propValue));

  22. // ...

注意其中 /appext/Data实际本质是一个字节数组,其内容为 31000,代表无限循环 gif(注释中说得很清楚),这就应该是循环 gif的关键所在。

可见,首先需要创建一个 PROPVARIANT对象,然后调用 IWICBitmapEncoder中的 GetMetadataQueryWriter方法,获取 IWICMetadataQueryWriter,然后通过该方法中的 SetMetadataByName,将 /appext/Application以及 /appext/Data按照指定的格式传入即可,还能有问题什么呢?

问题可大咯!

坑人的Variant

刚好 SharpDX对这些 C++/COM接口有看似正确的移植,首先 GifBitmapEncoder提供了 MetadataQueryWriter属性(而不是 Get函数),非常贴心,该类中也包含了名字相同的设置函数方法,其签名如下:

  1. public unsafe void SetMetadataByName(string name, object value) { /* ... */ }

嗯,非常合理,一个键值对而已,能有什么问题?我便用了起来,我自以为代码可能也许大概应该长这个样子:

  1. encoder.MetadataQueryWriter.SetMetadataByName("/appext/Application", "NETSCAPE2.0");

  2. encoder.MetadataQueryWriter.SetMetadataByName("/appext/Data", new byte[] { 3, 1, 0, 0, 0 });

然而运行报错了,错误信息为:

  1. SharpDX.SharpDXException: HRESULT: [0x80070057], Module: [General], ApiCode: [E_INVALIDARG/Invalid Arguments], Message: 参数错误。

  2. at SharpDX.Result.CheckError() in C:\projects\sharpdx\Source\SharpDX\Result.cs:line 197

  3. at SharpDX.WIC.MetadataQueryWriter.SetMetadataByName(String name, IntPtr varValueRef) in C:\projects\sharpdx\Source\SharpDX.Direct2D1\Generated\REFERENCE\WIC\Interfaces.cs:line 4923

  4. at SharpDX.WIC.MetadataQueryWriter.SetMetadataByName(String name, Object value) in C:\projects\sharpdx\Source\SharpDX.Direct2D1\WIC\MetadataQueryWriter.cs:line 91

  5. at UserQuery.SaveD2DBitmap(Int32 width, Int32 height, String text) in C:\Users\sdfly\AppData\Local\Temp\LINQPad6\_uciwedks\kncljn\LINQPadQuery:line 79

  6. at UserQuery.Main() in C:\Users\sdfly\AppData\Local\Temp\LINQPad6\_uciwedks\kncljn\LINQPadQuery:line 5

反编译它这个 SetMetadataByName一看,其代码如下:

  1. public unsafe void SetMetadataByName(string name, object value)

  2. {

  3. byte* variant = stackalloc byte[512];

  4. Variant* variantStruct = (Variant*)variant;

  5. variantStruct->Value = value;

  6. SetMetadataByName(name, (IntPtr)(void*)variant);

  7. }

  8. internal unsafe void SetMetadataByName(string name, IntPtr varValueRef) { /* ... */ }

原来 objectvalue的本质是它内部骚操作创建了一个 Variant,该 Variant就对应了 C++中的 PROPVARIANT,然后后续调用的正确性取决于 Variant.Value属性的设置方法,该属性的源代码如下:

  1. // SharpDX.Win32.Variant

  2. using SharpDX.Mathematics.Interop;

  3. using System;

  4. using System.Globalization;

  5. using System.Reflection;

  6. using System.Runtime.InteropServices;

  7. public unsafe object Value

  8. {

  9. get { /* ... */ }

  10. set

  11. {

  12. if (value == null)

  13. {

  14. Type = VariantType.Default;

  15. ElementType = VariantElementType.Null;

  16. return;

  17. }

  18. Type type = value.GetType();

  19. Type = VariantType.Default;

  20. if (type.GetTypeInfo().get_IsPrimitive())

  21. {

  22. if ((object)type == typeof(byte)) // ...

  23. if ((object)type == typeof(sbyte)) // ...

  24. if ((object)type == typeof(int)) // ...

  25. if ((object)type == typeof(uint)) // ...

  26. if ((object)type == typeof(long)) // ...

  27. if ((object)type == typeof(ulong)) // ...

  28. if ((object)type == typeof(short)) // ...

  29. if ((object)type == typeof(ushort)) // ...

  30. if ((object)type == typeof(float)) // ...

  31. if ((object)type == typeof(double)) // ...

  32. }

  33. else

  34. {

  35. if (value is ComObject) // ...

  36. if (value is DateTime) // ...

  37. if (value is string) // ...

  38. }

  39. throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Type [{0}] is not handled", new object[1]

  40. {

  41. type.get_Name()

  42. }));

  43. }

  44. }

在原代码,该属性全部代码长达约 300行,可见作者是花了些心思的,判断了那个 object的“各种”情况,根据上文中的 C++代码,我需要的是 VT_UI1|VT_VECTOR 然而这么多情况,就是没找到我需要的那一种????。

最重要的是,直接传 IntPtr的另一个重载它居然是 internal的,导致我几乎没什么操作空间了。

逢山开路,遇水搭桥

我花了很多时间怎么在 SharpDX已有提供的 Variant对象上做手脚,但感觉代码会比较复杂,所以我走向了另一条路,从那个 internal,传 IntPtr的接口出发解决。

调用私有方法

传 IntPtr的方法是私有的,不能直接调用,但可以使用反射来创建一个指向该私有方法的委托,然后通过该委托调用这个方法:

  1. var setMetadataMethod = encoder.MetadataQueryWriter

  2. .GetType()

  3. .GetMethod(nameof(WIC.MetadataQueryWriter.SetMetadataByName), BindingFlags.NonPublic | BindingFlags.Instance);

  4. var setMetadata = (Action<string, IntPtr>)setMetadataMethod

  5. .CreateDelegate(typeof(Action<string, IntPtr>), encoder.MetadataQueryWriter);

这样一来即可通过 setMetadata委托来调用。

不一样的Variant

SharpDX提供了一个 Variant,不代表必须要用他的 Variant,他想通过高达 300行代码的 if/else搞定一切,导致代码超级复杂。而通过查询 C++接口,我能将 Variant简化为如下一个 struct

  1. [StructLayout(LayoutKind.Explicit)]

  2. struct PV

  3. {

  4. [FieldOffset(0)] short VT;

  5. [FieldOffset(8)] int Length;

  6. [FieldOffset(8)] IntPtr StringBuffer;

  7. [FieldOffset(8)] ushort UShortValue;

  8. [FieldOffset(16)] IntPtr Buffer;

  9. public static PV CreateUByteVector(int length, IntPtr buffer) => new PV

  10. {

  11. VT = (short)VariantType.Vector + (short)VariantElementType.UByte,

  12. Length = length,

  13. Buffer = buffer

  14. };

  15. public static PV CreateString(IntPtr buffer) => new PV

  16. {

  17. VT = (short)VariantElementType.StringPointer,

  18. StringBuffer = buffer,

  19. };

  20. public static PV CreateUI2(ushort val) => new PV

  21. {

  22. VT = (short)VariantElementType.UShort,

  23. UShortValue = val,

  24. };

  25. }

指定/appext/Application

这是一个指向字符串的指针,正常可能要用 fixed关键字或者 Marshal.AllocHGlobal方法,但这里有更简单的办法,我可以使用 stackalloc,因为此处我具备该内存的所有权,且能精准控制它的生命周期,而且它性能也更好:

  1. // /appext/Application: NETSCAPE2.0

  2. byte* bytes = stackalloc byte[11] { 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48 };

  3. PV pv = PV.CreateUByteVector(11, (IntPtr)bytes);

  4. setMetadata("/appext/Application", (IntPtr)(void*)&pv);

指定/appext/Data

同样的道理,将 /appext/data的 31000分配在栈上代码最简单,性能也最好:

  1. // /appext/Data: 3, 1, [0, 0], 0

  2. byte* bytes2 = stackalloc byte[5] { 3, 1, 0, 0, 0, };

  3. PV pv2 = PV.CreateUByteVector(5, (IntPtr)bytes2);

  4. setMetadata("/appext/Data", (IntPtr)(void*)&pv2);

注意,不是所有内存都能分配到栈上,它有两大限制,首先栈内存是有限的,其次,该内存必须立即被使用,否则栈销毁后这些内存指针也会立即失效。

优化

其实有了这个,还能做更多的操作。默认图片的运行速度比较慢,可以将图片的速度调快一点,这也是通过设置 /grctlext/Delay实现的,只是该 metadata是存在于帧上(而不是图片容器上):

  1. // frame delay by 50ms.

  2. var setMetadata = (Action<string, IntPtr>)setMetadataMethod

  3. .CreateDelegate(typeof(Action<string, IntPtr>), frame.MetadataQueryWriter);

  4. var pv = PV.CreateUI2(5);

  5. setMetadata("/grctlext/Delay", (IntPtr)(void*)&pv);

我设置的是 5,代表 50ms,该值的意思就是乘以 10ms,如 10就代表 100ms,每一帧都可以不一样。如果不设置,那就由呈现器决定 gif播放的速度,大部分播放器都是 100ms。如果设置为 1或者 0,则会被呈现器忽略,如果想要在浏览器上运行,我亲测最低可以设置的值为 2

下图为默认效果: 

下图为 50ms效果: 

下图为 20ms效果: 

还能如指定 Gif的 Comment字段,可以给自己做一个签名:

  1. // "/commentext/TextEntry": "Created by Flysha.Zhou\0"

  2. var commentsBytes = Encoding.UTF8.GetBytes("Created by Flysha.Zhou\0");

  3. fixed (byte* p = commentsBytes)

  4. {

  5. var pv3 = PV.CreateString((IntPtr)p);

  6. setMetadata("/commentext/TextEntry", (IntPtr)(void*)&pv3);

  7. }

结语

上述代码都已经上传到了我的博客 Github中: https://github.com/sdcb/blog-data/tree/master/2019/20191127-the-variant-bug-in-sharpdx

由于 SharpDX官方已经停止维护,但迟迟又没有其它包顶上,官方是不可能修这些东西????,这些坑只能自己默默踩了(这波“福报”只能默默领好了)。有时我觉得要是脾气来了,我甚至可以 SharpDX拿过来自己维护一个发行版,因为 SharpDX在平时的生活工作中实在是太常用、太常见了。

微信不能评论,有想法的朋友可以点击阅读原文,或者到这个地址(博客园)进行点赞/评论:https://www.cnblogs.com/sdflysha/p/20191127-the-variant-bug-in-sharpdx.html

喜欢的朋友 请关注我的微信公众号:【DotNet骚操作】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312827.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

EF Core 3.0查询

随着.NET Core 3.0的发布&#xff0c;EF Core 3.0也随之正式发布&#xff0c;关于这一块最近一段时间也没太多去关注&#xff0c;陆续会去对比之前版本有什么变化没有&#xff0c;本节我们来看下两个查询。分组我们知道在EF Core 3.0版本之前&#xff0c;对于分组查询是在客户端…

经典排序算法(1)——冒泡排序算法详解

冒泡排序&#xff08;Bubble Sort&#xff09;是一种典型的交换排序算法&#xff0c;通过交换数据元素的位置进行排序。 一、算法基本思想 &#xff08;1&#xff09;基本思想 冒泡排序的基本思想就是&#xff1a;从无序序列头部开始&#xff0c;进行两两比较&#xff0c;根据…

C++模版和C#泛型求同存异录(一)sizeof(T)

sizeof(T)从C的模板代码往C#代码移植的时候发现了一个小问题。在C模板代码中 sizeof(T)是一种有效的写法&#xff0c;最终在会编译器展开成sizeof(int),sizeof(float)或者sizeof(myclass),然后在运行时这个代码是有效的&#xff0c;能够执行的。于是我们看上去就可以计算在运行…

经典排序算法(2)——快速排序算法详解

快速排序&#xff08;Quick Sort&#xff09;也是一种典型的交换排序算法&#xff0c;通过交换数据元素的位置进行排序。 一、算法基本思想 &#xff08;1&#xff09;基本思想 快速排序的基本思想就是&#xff1a;通过一趟排序将要排序的数据分割成独立的两部分&#xff0c;其…

经典排序算法(3)——直接插入排序算法详解

直接插入排序&#xff08;Insertion Sort&#xff09;是一种插入排序算法&#xff0c;通过不断地将数据元素插入到合适的位置进行排序。 一、算法基本思想 &#xff08;1&#xff09;基本思想 直接插入排序的基本思想是&#xff1a;顺序地把待排序的序列中的各个元素按其关键字…

[ASP.NET Core 3框架揭秘] 异步线程无法使用IServiceProvider?

标题反映的是上周五一个同事咨询我的问题&#xff0c;我觉得这是一个很好的问题。这个问题有助于我们深入理解依赖注入框架在ASP.NET Core中的应用&#xff0c;以及服务实例的生命周期。一、问题重现我们通过一个简单的实例来模拟该同事遇到的问题。我们采用极简的方式创建了如…

经典排序算法(4)——折半插入排序算法详解

折半插入排序&#xff08;Binary Insertion Sort&#xff09;是一种插入排序算法&#xff0c;通过不断地将数据元素插入到合适的位置进行排序&#xff0c;在寻找插入点时采用了折半查找。 一、算法基本思想 &#xff08;1&#xff09;基本思想 折半插入排序的基本思想是&#x…

经典排序算法(5)——希尔排序算法详解

希尔排序&#xff08;Shell Sort&#xff09;是一种典型的插入排序算法&#xff0c;通过对原始序列进行分组进行排序。 一、算法基本思想 &#xff08;1&#xff09;基本思想 希尔排序是基于插入排序的以下两点性质而提出改进方法的&#xff1a; 插入排序在对几乎已经排好序的…

程序员修神之路--容器技术为什么会这么流行(记得去抽奖)

菜菜哥&#xff0c;你上次讲的kubernetes我研究了一下&#xff0c;你再给我讲讲docker呗docker可很流行呀kubernetes是容器编排技术&#xff0c;容器不就是指的docker吗&#xff1f;docker可不等于容器哦&#xff0c;docker只算是容器的一种吧&#xff0c;算了容器的典型代表容…

经典排序算法(6)——直接选择排序算法详解

直接选择排序&#xff08;Straight Select Sort&#xff09;是一种典型的选择排序算法&#xff0c;通过不断选择序列中最大&#xff08;小&#xff09;的元素。 一、算法基本思想 &#xff08;1&#xff09;基本思想 直接选择排序的基本思想就是&#xff1a;不断从未排序队列中…

一篇文章看懂Git是什么以及如何简单的上手Git

本文来自DotNET技术圈作者&#xff1a;显杰1.Git是什么Git是目前世界上最先进的分布式版本控制系统什么是版本控制系统&#xff1f;好比设计师从开始设计第一个版本的设计稿开始&#xff1a;Demo > Demo1 > Demo2 > ... >Demo1001 > Demo最终版本 > Demo最终…

[翻译] 使用 Serverless 和 .NET Core 构建飞速发展的架构

作者&#xff1a;Samuele RescaServerless 技术为开发人员提供了一种快速而独立的方式将实现投入生产。这种技术在企业的技术栈中日益流行&#xff0c;自 2017 年以来&#xff0c;它一直是 ThoughtWorks 技术雷达的实验级别的技术[译注&#xff1a;技术雷达是 ThoughtWorks 每半…

经典排序算法(7)——堆排序算法详解

堆排序&#xff08;Heap sort&#xff09;是指利用堆&#xff08;最大堆、最小堆&#xff09;这种数据结构所设计的一种排序算法。堆是一个完全二叉树的结构&#xff0c;并同时满足如下性质&#xff1a;即子结点的键值或索引总是小于&#xff08;或者大于&#xff09;它的父节点…

经典排序算法(8)——归并排序算法详解

归并排序&#xff08;Merge sort&#xff09;&#xff0c;是创建在归并操作上的一种有效的排序算法&#xff0c;效率为O(nlog n)。该算法是采用分治法&#xff08;Divide and Conquer&#xff09;的一个非常典型的应用&#xff0c;且各层分治递归可以同时进行。 一、算法基本思…

祝贺王远当选为中国区第二位 Teams MVP

今天一上班就传来喜讯&#xff0c;Microsoft Teams 大中华区技术社区专家委员会成员之一的王远成功当选了2020-2021年度微软最有价值专家&#xff08;MVP)&#xff0c;这是对他在基于Office 365的音视频会议系统&#xff08;尤其是在Microsoft Teams&#xff09;方面的深入研究…

经典排序算法(9)——桶排序算法详解

桶排序&#xff08;Bucket sort&#xff09;或所谓的箱排序&#xff0c;并不是比较排序&#xff0c;它不受到 O(nlogn) 下限的影响。 一、算法基本思想 &#xff08;1&#xff09;基本思想 桶排序工作的原理是将数组分到有限数量的桶子里&#xff0c;每个桶子再个别排序&#x…

[原]排错实战——使用process explorer替换任务管理器

前言 一般&#xff0c;我们会使用任务管理器查看系统中有哪些进程在运行&#xff0c;强制杀掉某个进程。可是系统自带的任务管理器功能有限&#xff0c;process explorer是一个功能更强大的工具。它可以让我们查看更多更详细的信息&#xff08; 比如查看某个进程的父进程&#…

ABP vNext中使用开源日志面板 LogDashboard

ABP vNext 使用 logdashboard本文示例源码&#xff1a;https://github.com/liangshiw/LogDashboard/tree/master/samples/abpvnextABPABP是aspnetcore3.0的开源web应用程序框架&#xff0c;非常适合现代web应用程序。有关ABP的更多内容可以查看官方文档Logdashboard可以直接在基…

经典排序算法(10)——基数排序算法详解

基数排序&#xff08;Radix sort&#xff09;是一种非比较型整数排序算法&#xff0c;其原理是将整数按位数切割成不同的数字&#xff0c;然后按每个位数分别比较。 一、算法基本思想 &#xff08;1&#xff09;基本思想 基数排序是基于桶排序来实现。通过键值的部分信息&#…

经典排序算法(11)——计数排序算法详解

计数排序&#xff08;Counting sort&#xff09;是一个非基于比较的排序算法&#xff0c;该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时&#xff0c;它的复杂度为Ο(nk)&#xff08;其中k是整数的范围&#xff09;&#xff0c;快于任何比较…