Bei der Ausführung des letzten Projekts bei der Arbeit waren mein Kollege und ich mit der Tatsache konfrontiert, dass einige Methoden und Konstruktoren in System.Drawing an ganz normalen Orten aus OutOfMemory stammen und wenn noch viel freier Speicher vorhanden ist.
Das Wesentliche des Problems
Nehmen Sie zum Beispiel diesen C # -Code:
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); } } }
Wenn die letzte Zeile ausgeführt wird, wird garantiert eine OutOfMemoryException ausgelöst, unabhängig davon, wie viel freier Speicher verfügbar ist. Wenn Sie außerdem 3.367667E-16f und -3.367667E-16f durch 0 ersetzen, was der Wahrheit sehr nahe kommt, funktioniert alles einwandfrei - die Füllung wird erstellt. Meiner Meinung nach sieht dieses Verhalten seltsam aus. Mal sehen, warum das passiert und wie man damit umgeht.
Finden Sie die Ursachen der Krankheit heraus
Zunächst erfahren wir, was im LinearGradientBrush-Konstruktor passiert. Zu diesem Zweck können Sie auf
referenceource.microsoft.com zugreifen . Es wird Folgendes geben:
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); }
Es ist leicht zu bemerken, dass das Wichtigste hier der GDI + -Aufruf der GdipCreateLineBrush-Methode ist. Sie müssen also beobachten, was darin passiert. Verwenden Sie dazu IDA + HexRays. Auf IDA herunterladen gdiplus.dll. Wenn Sie bestimmen müssen, welche Version der Bibliothek debuggt werden soll, können Sie den Process Explorer von SysInternals verwenden. Darüber hinaus können Probleme mit Berechtigungen für den Ordner auftreten, in dem sich gdiplus.dll befindet. Sie werden gelöst, indem Sie den Besitzer dieses Ordners ändern.
Öffnen Sie also gdiplus.dll in der IDA. Warten wir, bis die Datei verarbeitet ist. Wählen Sie danach im Menü: Ansicht → Unteransichten öffnen → Exportieren, um alle Funktionen zu öffnen, die aus dieser Bibliothek exportiert werden, und suchen Sie dort GdipCreateLineBrush.
Dank des Ladens von Zeichen, der Leistungsfähigkeit von HexRays und der
Dokumentation können Sie den Methodencode vom Assembler problemlos in lesbaren C ++ - Code übersetzen:
GdipCreateLineBrush GpStatus __userpurge GdipCreateLineBrush@<eax>(int a1@<edi>, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode, GpRectGradient **result) { GpStatus status;
Der Code für diese Methode ist völlig klar. Sein Wesen liegt in den Zeilen:
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 prüft, ob die Eingabeparameter korrekt sind. Wenn dies nicht der Fall ist, wird ein InvalidParameter zurückgegeben. Andernfalls wird ein GpLineGradient erstellt und auf Gültigkeit überprüft. Wenn die Validierung fehlschlägt, wird OutOfMemory zurückgegeben. Anscheinend ist dies unser Fall, und daher müssen wir herausfinden, was im GpLineGradient-Konstruktor passiert:
GpLineGradient :: GpLineGradient GpRectGradient *__thiscall GpLineGradient::GpLineGradient(GpGradientBrush *this, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode) { GpGradientBrush *v6;
Hier werden die Variablen initialisiert, die dann in LinearGradientRectFromPoints und SetLineGradient eingetragen werden. Ich wage anzunehmen, dass rect ein Füllrechteck ist, das auf Punkt1 und Punkt2 basiert. Um dies zu sehen, können Sie sich LinearGradientRectFromPoints ansehen:
LinearGradientRectFromPoints GpStatus __fastcall LinearGradientRectFromPoints(GpPointF *p1, GpPointF *p2, GpRectF *result) { double vP1X;
Wie erwartet ist rect ein Rechteck aus den Punkten point1 und point2.
Kommen wir nun zu unserem Hauptproblem zurück und sehen, was in SetLineGradient passiert:
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;
Auch in SetLineGradient erfolgt nur die Feldinitialisierung. Wir müssen also tiefer gehen:
int __fastcall CalcLinearGradientXform(int zero, GpRectF *rect, float angle, int a4) {
Und endlich:
GpStatus __thiscall GpMatrix::InferAffineMatrix(int this, GpPointF *points, GpRectF *rect) {
Die InferAffineMatrix-Methode ist genau das, was uns interessiert. Hier wird der Bereich des Rect überprüft - das ursprüngliche Rechteck aus Punkten. Wenn er kleiner als 0,00000011920929 ist, gibt InferAffineMatrix InvalidParameter zurück. 0.00000011920929 ist das
Maschinen-Epsilon für Float (FLT_EPSILON). Sie können sehen, wie interessant Microsoft den Bereich des Rechtecks betrachtet:
rectArea = bottom * right - x * y - (y * width + x * height);
Subtrahieren Sie vom Bereich unten rechts den Bereich oben links und dann den Bereich über dem Rechteck und links vom Rechteck. Warum das so ist, verstehe ich nicht; Ich hoffe, dass ich eines Tages diese geheime Methode kennen werde.
Also was wir haben:
- InnerAffineMatrix gibt InvalidParameter zurück.
- CalcLinearGradientXForm wirft dieses Ergebnis höher;
- In SetLineGradient folgt die Ausführung dem if-Zweig, und die Methode gibt auch InvalidParameter zurück.
- Der GpLineGradient-Konstruktor verliert Informationen über den InvalidParameter und gibt ein GpLineGradient-Objekt zurück, das nicht bis zum Ende initialisiert wurde - das ist sehr schlecht!
- GdipCreateLineBrush überprüft in CheckValid (Zeile 26), ob das GpLineGradient-Objekt für Felder gültig ist, die nicht vollständig ausgefüllt sind, und gibt natürlich false zurück.
- Danach ändert sich der Status in OutOfMemory, das beim Beenden der GDI + -Methode .NET erhält.
Es stellt sich heraus, dass Microsoft aus irgendeinem Grund den Rückgabestatus einiger Methoden ignoriert, aus diesem Grund falsche Annahmen trifft und das Verständnis der Arbeit der Bibliothek für andere Programmierer erschwert. Sie mussten jedoch nur den Status des GpLineGradient-Konstruktors höher werfen, den Rückgabewert in GdipCreateLineBrush auf OK überprüfen und andernfalls den Konstruktorstatus zurückgeben. Für GDI + -Benutzer würde die in der Bibliothek aufgetretene Fehlermeldung dann logischer aussehen.
Option mit Ersetzen sehr kleiner Zahlen durch Null, d.h. Bei vertikaler Füllung wird es aufgrund der Magie, die Microsoft in der LinearGradientRectFromPoints-Methode in den Zeilen 35 bis 45 ausführt, fehlerfrei ausgeführt:
Magie 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; }
Wie zu behandeln?
Wie vermeide ich diesen Absturz in .NET-Code? Die einfachste und naheliegendste Option besteht darin, die Fläche des Rechtecks von Punkt1 und Punkt2 mit FLT_EPSILON zu vergleichen und keinen Farbverlauf zu erstellen, wenn die Fläche kleiner ist. Mit dieser Option verlieren wir jedoch Informationen über den Farbverlauf und es wird ein unbemalter Bereich gezeichnet, was nicht gut ist. Ich sehe eine akzeptablere Option, wenn ich den Winkel der Verlaufsfüllung überprüfe. Wenn sich herausstellt, dass die Füllung nahe an der Horizontalen oder Vertikalen liegt, stellen Sie die entsprechenden Parameter für die Punkte gleich ein:
Meine Lösung in 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; }
Wie geht es den Wettbewerbern?
Lassen Sie uns herausfinden, was in Wine los ist. Schauen Sie sich dazu den
Quellcode für Wine in Zeile 306 an:
GdipCreateLineBrush of Wine 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; }
Hier ist die einzige Validierung der Parameter:
if(!line || !startpoint || !endpoint || wrap == WrapModeClamp) return InvalidParameter;
Aus Gründen der Kompatibilität mit Windows wurde höchstwahrscheinlich Folgendes geschrieben:
if (startpoint->X == endpoint->X && startpoint->Y == endpoint->Y) return OutOfMemory;
Und der Rest ist nichts Interessantes - Speicherzuweisung und Ausfüllen der Felder. Aus dem Quellcode geht hervor, dass in Wine die Erstellung einer Problemgradientenfüllung fehlerfrei durchgeführt werden sollte. Und wirklich - wenn Sie das folgende Programm unter Windows ausführen (ich lief unter Windows 10x64)
Testprogramm #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; }
Das in der Windows-Konsole wird sein:
Outofmemory
Ok
und in Ubuntu mit Wein:
Ok
Ok
Es stellt sich heraus, dass entweder ich etwas falsch mache oder Wine in dieser Angelegenheit logischer funktioniert als Windows.
Fazit
Ich hoffe wirklich, dass ich etwas nicht verstanden habe und das Verhalten von GDI + logisch ist. Es ist wahr, es ist überhaupt nicht klar, warum Microsoft genau das getan hat. Ich habe mich viel mit ihren anderen Produkten beschäftigt, und es gibt auch solche Dinge, die in einer anständigen Gesellschaft die Code Review nicht bestanden hätten.