عند تنفيذ آخر مشروع في العمل ، واجهت أنا وزميلي حقيقة أن بعض الأساليب والمُنشئين في System.Draw تسقط من OutOfMemory في أماكن عادية تمامًا ، وعندما لا يزال هناك الكثير من الذاكرة الخالية.
جوهر المشكلة
على سبيل المثال ، خذ رمز C # هذا:
using System.Drawing; using System.Drawing.Drawing2D; namespace TempProject { static class Program { static void Main() { var point1 = new PointF(-3.367667E-16f, 0f); var point2 = new PointF(3.367667E-16f, 100f); var brush = new LinearGradientBrush(point1, point2, Color.White, Color.Black); } } }
عند تنفيذ السطر الأخير ، سيتم طرح OutOfMemoryException ، بغض النظر عن مقدار الذاكرة المتوفرة. علاوة على ذلك ، إذا قمت باستبدال 3.367667E-16f و -3.367667E-16f بـ 0 ، وهو قريب جدًا من الحقيقة ، فسيعمل كل شيء بشكل جيد - سيتم إنشاء التعبئة. في رأيي ، يبدو هذا السلوك غريبًا. دعونا نرى لماذا يحدث هذا وكيفية التعامل معه.
تعرف على أسباب المرض
بادئ ذي بدء ، نتعلم ما يحدث في مُنشئ LinearGradientBrush. للقيام بذلك ، يمكنك إلقاء نظرة على
sourcesource.microsoft.com . سيكون هناك ما يلي:
public LinearGradientBrush(PointF point1, PointF point2, Color color1, Color color2) { IntPtr brush = IntPtr.Zero; int status = SafeNativeMethods.Gdip.GdipCreateLineBrush( new GPPOINTF(point1), new GPPOINTF(point2), color1.ToArgb(), color2.ToArgb(), (int)WrapMode.Tile, out brush ); if (status != SafeNativeMethods.Gdip.Ok) throw SafeNativeMethods.Gdip.StatusException(status); SetNativeBrushInternal(brush); }
من السهل ملاحظة أن أهم شيء هنا هو استدعاء GDI + لطريقة GdipCreateLineBrush. لذا ، أنت بحاجة إلى مشاهدة ما يحدث بداخله. للقيام بذلك ، استخدم IDA + HexRays. تنزيل إلى IDA gdiplus.dll. إذا كنت بحاجة إلى تحديد إصدار المكتبة الذي يجب تصحيحه ، يمكنك استخدام Process Explorer من SysInternals. بالإضافة إلى ذلك ، قد تواجه مشكلات في الأذونات على المجلد حيث يقع gdiplus.dll. يتم حلها عن طريق تغيير مالك هذا المجلد.
لذا ، افتح gdiplus.dll في المؤسسة الدولية للتنمية. دعنا ننتظر حتى تتم معالجة الملف. بعد ذلك ، حدد في القائمة: عرض ← فتح طرق عرض فرعية ← تصدير لفتح جميع الوظائف التي تم تصديرها من هذه المكتبة ، والعثور على GdipCreateLineBrush هناك.
بفضل تحميل الشخصيات وقوة HexRays
والوثائق ، يمكنك بسهولة ترجمة رمز الطريقة من المجمع إلى رمز C ++ قابل للقراءة:
GdipCreateLineBrush GpStatus __userpurge GdipCreateLineBrush@<eax>(int a1@<edi>, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode, GpRectGradient **result) { GpStatus status;
كود هذه الطريقة واضح تماما. يكمن جوهرها في السطور:
if ( result && point1 && point2 && wrapMode != 4 ) { vColor1 = color1; vColor2 = color2; v8 = operator new(a1); status = 0; if ( v8 ) v9 = GpLineGradient::GpLineGradient(v8, point1, point2, &vColor1, &vColor2, wrapMode); else v9 = 0; *result = v9; if ( !CheckValid<GpHatch>(result) ) status = OutOfMemory } else { status = InvalidParameter; }
يتحقق GdiPlus مما إذا كانت معلمات الإدخال صحيحة ، وإذا لم تكن كذلك ، فيتم إرجاع InvalidParameter. خلاف ذلك ، يتم إنشاء GpLineGradient والتحقق من صلاحيتها. إذا فشل التحقق من الصحة ، يتم إرجاع OutOfMemory. على ما يبدو ، هذه هي قضيتنا ، وبالتالي ، نحن بحاجة إلى معرفة ما يحدث داخل مُنشئ GpLineGradient:
GpLineGradient :: GpLineGradient GpRectGradient *__thiscall GpLineGradient::GpLineGradient(GpGradientBrush *this, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode) { GpGradientBrush *v6;
هنا تتم تهيئة المتغيرات ، ثم يتم تعبئتها في LinearGradientRectFromPoints و SetLineGradient. أجرؤ على افتراض أن المستطيل عبارة عن مستطيل تعبئة يعتمد على النقطة 1 والنقطة 2 ، لرؤية هذا ، يمكنك إلقاء نظرة على LinearGradientRectFromPoints:
LinearGradientRectFromPoints GpStatus __fastcall LinearGradientRectFromPoints(GpPointF *p1, GpPointF *p2, GpRectF *result) { double vP1X;
كما هو متوقع ، المستطيل هو مستطيل للنقط point1 و point2.
الآن دعنا نعود إلى مشكلتنا الرئيسية ونرى ما يحدث داخل SetLineGradient:
SetLinegradient GpStatus __thiscall GpLineGradient::SetLineGradient(DpGradientBrush *this, GpPointF *p1, GpPointF *p2, GpRectF *rect, int color1, int color2, float angle, int zero, int wrapMode) { _DWORD *v10;
في SetLineGradient أيضًا ، تحدث تهيئة الحقل فقط. لذا ، نحتاج إلى التعمق:
int __fastcall CalcLinearGradientXform(int zero, GpRectF *rect, float angle, int a4) {
وأخيرًا:
GpStatus __thiscall GpMatrix::InferAffineMatrix(int this, GpPointF *points, GpRectF *rect) {
إن طريقة InferAffineMatrix هي بالضبط ما يهمنا. هنا يتم التحقق من مساحة المستقيم - المستطيل الأصلي للنقاط ، وإذا كان أقل من 0.00000011920929 ، فإن InferAffineMatrix يُرجع InvalidParameter. 0.00000011920929 هو
إبسيلون الجهاز للعوامة (FLT_EPSILON). يمكنك أن ترى مدى اهتمام Microsoft بمنطقة المستطيل:
rectArea = bottom * right - x * y - (y * width + x * height);
من المنطقة إلى أسفل اليمين ، اطرح المنطقة إلى أعلى اليسار ، ثم اطرح المنطقة فوق المستطيل وإلى يسار المستطيل. لماذا يتم ذلك ، لا أفهم. آمل يومًا ما أن أعرف هذه الطريقة السرية.
إذن ما لدينا:
- إرجاع InnerAffineMatrix InvalidParameter؛
- يطرح CalcLinearGradientXForm هذه النتيجة أعلى ؛
- في SetLineGradient ، سيتبع التنفيذ الفرع if ، وستعرض الطريقة أيضًا InvalidParameter ؛
- سيفقد مُنشئ GpLineGradient معلومات حول InvalidParameter ويعيد كائن GpLineGradient الذي لم تتم تهيئته إلى النهاية - هذا أمر سيئ جدًا!
- سيتحقق GdipCreateLineBrush في CheckValid (السطر 26) من أن كائن GpLineGradient صالح مع الحقول التي لم يتم ملؤها بالكامل وسيعود بشكل طبيعي false.
- بعد ذلك ، ستتغير الحالة إلى OutOfMemory ، والتي ستحصل على .NET عند الخروج من طريقة GDI +.
اتضح أن Microsoft لسبب ما تتجاهل حالة الإرجاع لبعض الطرق ، وتضع افتراضات غير صحيحة بسبب ذلك ، وتعقد فهم عمل المكتبة للمبرمجين الآخرين. ولكن كل ما كان عليك فعله هو إعادة توجيه الحالة من مُنشئ GpLineGradient أعلاه ، وفي GdipCreateLineBrush تحقق من القيمة المرتجعة لـ OK ، وإلا قم بإرجاع حالة المُنشئ. بعد ذلك ، بالنسبة لمستخدمي GDI + ، ستبدو رسالة الخطأ التي حدثت داخل المكتبة أكثر منطقية.
خيار استبدال الأرقام الصغيرة جدًا بصفر ، أي باستخدام التعبئة الرأسية ، يتم تشغيله بدون أخطاء بسبب السحر الذي تقوم به Microsoft في طريقة LinearGradientRectFromPoints في الأسطر من 35 إلى 45:
سحر if ( IsCloseReal(p1->X, p2->X) ) { result->X = vLeft - 0.5 * vHeight; result->Width = vHeight; vWidth = vHeight; } if ( IsCloseReal(p1->Y, p2->Y) ) { result->Y = vTop - vWidth * 0.5; result->Height = vWidth; }
كيف تعالج؟
كيفية تجنب هذا التعطل في التعليمات البرمجية .NET؟ الخيار الأبسط والأكثر وضوحًا هو مقارنة مساحة المستطيل من النقطتين 1 و 2 مع FLT_EPSILON وعدم إنشاء تدرج إذا كانت المساحة أصغر. ولكن مع هذا الخيار ، سنفقد معلومات حول التدرج ، وسيتم رسم منطقة غير مطلية ، وهذا ليس جيدًا. أرى خيارًا أكثر قبولًا عند التحقق من زاوية تعبئة التدرج ، وإذا اتضح أن التعبئة قريبة من الأفقي أو الرأسي ، فقم بتعيين المعلمات المقابلة للنقاط نفسها:
الحل في C # static LinearGradientBrush CreateBrushSafely(PointF p1, PointF p2) { if(IsShouldNormalizePoints(p1, p2)) { if(!NormalizePoints(ref p1, ref p2)) return null; } var brush = new LinearGradientBrush(p1, p2, Color.White, Color.Black); return brush; } static bool IsShouldNormalizePoints(PointF p1, PointF p2) { float width = Math.Abs(p1.X - p2.X); float height = Math.Abs(p1.Y - p2.Y); return width * height < FLT_EPSILON && !(IsCloseFloat(p1.X, p2.X) || IsCloseFloat(p1.Y, p2.Y)); } static bool IsCloseFloat(float v1, float v2) { var t = v2 == 0.0f ? 1.0f : v2; return Math.Abs((v1 - v2) / t) < FLT_EPSILON; } static bool NormalizePoints(ref PointF p1, ref PointF p2) { const double twoDegrees = 0.03490658503988659153847381536977d; float width = Math.Abs(p1.X - p2.X); float height = Math.Abs(p1.Y - p2.Y); var angle = Math.Atan2(height, width); if (Math.Abs(angle) < twoDegrees) { p1.Y = p2.Y; return true; } if (Math.Abs(angle - Math.PI / 2) < twoDegrees) { p1.X = p2.X; return true; } return false; }
كيف حال المنافسين؟
دعنا نكتشف ما يحدث في النبيذ. للقيام بذلك ، انظر إلى
شفرة المصدر لـ Wine ، السطر 306:
GdipCreateLineBrush من النبيذ GpStatus WINGDIPAPI GdipCreateLineBrush(GDIPCONST GpPointF* startpoint, GDIPCONST GpPointF* endpoint, ARGB startcolor, ARGB endcolor, GpWrapMode wrap, GpLineGradient **line) { TRACE("(%s, %s, %x, %x, %d, %p)\n", debugstr_pointf(startpoint), debugstr_pointf(endpoint), startcolor, endcolor, wrap, line); if(!line || !startpoint || !endpoint || wrap == WrapModeClamp) return InvalidParameter; if (startpoint->X == endpoint->X && startpoint->Y == endpoint->Y) return OutOfMemory; *line = heap_alloc_zero(sizeof(GpLineGradient)); if(!*line) return OutOfMemory; (*line)->brush.bt = BrushTypeLinearGradient; (*line)->startpoint.X = startpoint->X; (*line)->startpoint.Y = startpoint->Y; (*line)->endpoint.X = endpoint->X; (*line)->endpoint.Y = endpoint->Y; (*line)->startcolor = startcolor; (*line)->endcolor = endcolor; (*line)->wrap = wrap; (*line)->gamma = FALSE; (*line)->rect.X = (startpoint->X < endpoint->X ? startpoint->X: endpoint->X); (*line)->rect.Y = (startpoint->Y < endpoint->Y ? startpoint->Y: endpoint->Y); (*line)->rect.Width = fabs(startpoint->X - endpoint->X); (*line)->rect.Height = fabs(startpoint->Y - endpoint->Y); if ((*line)->rect.Width == 0) { (*line)->rect.X -= (*line)->rect.Height / 2.0f; (*line)->rect.Width = (*line)->rect.Height; } else if ((*line)->rect.Height == 0) { (*line)->rect.Y -= (*line)->rect.Width / 2.0f; (*line)->rect.Height = (*line)->rect.Width; } (*line)->blendcount = 1; (*line)->blendfac = heap_alloc_zero(sizeof(REAL)); (*line)->blendpos = heap_alloc_zero(sizeof(REAL)); if (!(*line)->blendfac || !(*line)->blendpos) { heap_free((*line)->blendfac); heap_free((*line)->blendpos); heap_free(*line); *line = NULL; return OutOfMemory; } (*line)->blendfac[0] = 1.0f; (*line)->blendpos[0] = 1.0f; (*line)->pblendcolor = NULL; (*line)->pblendpos = NULL; (*line)->pblendcount = 0; linegradient_init_transform(*line); TRACE("<-- %p\n", *line); return Ok; }
فيما يلي التحقق الوحيد من المعلمات:
if(!line || !startpoint || !endpoint || wrap == WrapModeClamp) return InvalidParameter;
على الأرجح ، تمت كتابة ما يلي للتوافق مع Windows:
if (startpoint->X == endpoint->X && startpoint->Y == endpoint->Y) return OutOfMemory;
والباقي لا شيء مثير للاهتمام - تخصيص الذاكرة وملء الحقول. من التعليمات البرمجية المصدر ، يصبح من الواضح أنه في النبيذ يجب أن يتم إنشاء تعبئة مشكلة التدرج دون أخطاء. وحقاً - إذا قمت بتشغيل البرنامج التالي على Windows (قمت بتشغيل Windows10x64)
برنامج الاختبار #include <Windows.h> #include "stdafx.h" #include <gdiplus.h> #include <iostream> #pragma comment(lib,"gdiplus.lib") void CreateBrush(float x1, float x2) { Gdiplus::LinearGradientBrush linGrBrush( Gdiplus::PointF(x1, -0.5f), Gdiplus::PointF(x2, 10.5f), Gdiplus::Color(255, 0, 0, 0), Gdiplus::Color(255, 255, 255, 255)); const int status = linGrBrush.GetLastStatus(); const char* result; if (status == 3) { result = "OutOfMemory"; } else { result = "Ok"; } std::cout << result << "\n"; } int main() { Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); Gdiplus::Graphics myGraphics(GetDC(0)); CreateBrush(-3.367667E-16f, 3.367667E-16f); CreateBrush(0, 0); return 0; }
سيكون في وحدة تحكم Windows:
خارج الذاكرة
حسنًا
وفي أوبونتو مع النبيذ:
حسنًا
حسنًا
اتضح أنني إما أفعل شيئًا خاطئًا ، أو يعمل النبيذ في هذه المسألة بشكل منطقي أكثر من Windows.
الخلاصة
آمل حقًا ألا أفهم شيئًا وأن سلوك GDI + منطقي. صحيح أنه ليس من الواضح على الإطلاق سبب قيام Microsoft بذلك. لقد تطرقت كثيرًا إلى منتجاتهم الأخرى ، وهناك أيضًا أشياء من هذا القبيل في المجتمع اللائق لن تجتاز مراجعة الكود.