Ketika menjalankan proyek terakhir di tempat kerja, kolega saya dan saya dihadapkan dengan kenyataan bahwa beberapa metode dan konstruktor dalam System.Drawing jatuh dari OutOfMemory di tempat yang benar-benar biasa, dan ketika masih ada banyak memori bebas.
Inti dari masalah
Misalnya, ambil kode C # ini:
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); } } }
Ketika baris terakhir dieksekusi, OutOfMemoryException dijamin akan dibuang, terlepas dari berapa banyak memori bebas yang tersedia. Selain itu, jika Anda mengganti 3.367667E-16f dan -3.367667E-16f dengan 0, yang sangat dekat dengan kebenaran, semuanya akan berfungsi dengan baik - isinya akan dibuat. Menurut saya, perilaku ini terlihat aneh. Mari kita lihat mengapa ini terjadi dan bagaimana menghadapinya.
Cari tahu penyebab penyakitnya
Untuk memulainya, kita mempelajari apa yang terjadi di konstruktor LinearGradientBrush. Untuk melakukan ini, Anda dapat melihat
Referenceource.microsoft.com . Akan ada yang berikut ini:
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); }
Sangat mudah untuk memperhatikan bahwa hal terpenting di sini adalah panggilan GDI + dari metode GdipCreateLineBrush. Jadi, Anda perlu melihat apa yang terjadi di dalamnya. Untuk melakukan ini, gunakan IDA + HexRays. Unduh ke IDA gdiplus.dll. Jika Anda perlu menentukan versi perpustakaan mana yang akan di-debug, Anda dapat menggunakan Process Explorer dari SysInternals. Selain itu, Anda mungkin mengalami masalah dengan izin di folder tempat gdiplus.dll berada. Mereka dipecahkan dengan mengubah pemilik folder ini.
Jadi, buka gdiplus.dll di IDA. Mari kita tunggu file diproses. Setelah itu, pilih di menu: Lihat → Buka Subviews → Ekspor untuk membuka semua fungsi yang diekspor dari perpustakaan ini, dan temukan GdipCreateLineBrush di sana.
Berkat pemuatan karakter, kekuatan HexRays dan
dokumentasi , Anda dapat dengan mudah menerjemahkan kode metode dari assembler menjadi kode C ++ yang dapat dibaca:
GdipCreateLineBrush GpStatus __userpurge GdipCreateLineBrush@<eax>(int a1@<edi>, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode, GpRectGradient **result) { GpStatus status;
Kode untuk metode ini benar-benar jelas. Esensinya terletak pada garis:
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 memeriksa apakah parameter input sudah benar, dan jika tidak, maka mengembalikan InvalidParameter. Kalau tidak, GpLineGradient dibuat dan diperiksa validitasnya. Jika validasi gagal, OutOfMemory dikembalikan. Rupanya, ini adalah kasus kami, dan oleh karena itu, kami perlu mencari tahu apa yang terjadi di dalam konstruktor GpLineGradient:
GpLineGradient :: GpLineGradient GpRectGradient *__thiscall GpLineGradient::GpLineGradient(GpGradientBrush *this, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode) { GpGradientBrush *v6;
Di sini variabel diinisialisasi, yang kemudian diisi dengan LinearGradientRectFromPoints dan SetLineGradient. Saya berani berasumsi bahwa rect adalah rectangle isian berdasarkan point1 dan point2, untuk melihat ini, Anda dapat melihat LinearGradientRectFromPoints:
LinearGradientRectFromPoints GpStatus __fastcall LinearGradientRectFromPoints(GpPointF *p1, GpPointF *p2, GpRectF *result) { double vP1X;
Seperti yang diharapkan, rect adalah persegi panjang poin point1 dan point2.
Sekarang mari kita kembali ke masalah utama kita dan melihat apa yang terjadi di dalam 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;
Di SetLineGradient, juga, hanya inisialisasi bidang terjadi. Jadi, kita perlu masuk lebih dalam:
int __fastcall CalcLinearGradientXform(int zero, GpRectF *rect, float angle, int a4) {
Dan akhirnya:
GpStatus __thiscall GpMatrix::InferAffineMatrix(int this, GpPointF *points, GpRectF *rect) {
Metode InferAffineMatrix adalah apa yang menarik minat kita. Di sini area rect diperiksa - persegi panjang poin asli, dan jika kurang dari 0,00000011920929, maka InferAffineMatrix mengembalikan InvalidParameter. 0,00000011920929 adalah
mesin epsilon untuk float (FLT_EPSILON). Anda dapat melihat betapa menariknya Microsoft mempertimbangkan bidang persegi panjang:
rectArea = bottom * right - x * y - (y * width + x * height);
Dari area ke kanan bawah, kurangi area ke kiri atas, lalu kurangi area di atas persegi panjang dan ke kiri persegi panjang. Mengapa ini dilakukan, saya tidak mengerti; Saya berharap suatu hari nanti saya akan mengetahui metode rahasia ini.
Jadi apa yang kita miliki:
- InnerAffineMatrix mengembalikan InvalidParameter;
- CalcLinearGradientXForm melempar hasil ini lebih tinggi;
- Di SetLineGradient, eksekusi akan mengikuti cabang if, dan metode ini juga akan mengembalikan InvalidParameter;
- Konstruktor GpLineGradient akan kehilangan informasi tentang InvalidParameter dan mengembalikan objek GpLineGradient yang tidak diinisialisasi hingga akhir - ini sangat buruk!
- GdipCreateLineBrush akan memeriksa di CheckValid (baris 26) bahwa objek GpLineGradient valid dengan bidang yang tidak sepenuhnya diisi dan secara alami akan kembali salah.
- Setelah itu, status akan berubah menjadi OutOfMemory, yang akan mendapatkan .NET saat keluar dari metode GDI +.
Ternyata Microsoft karena alasan tertentu mengabaikan status pengembalian beberapa metode, membuat asumsi yang salah karena hal ini, dan memperumit pemahaman pekerjaan perpustakaan untuk programmer lain. Tetapi yang harus Anda lakukan adalah melempar status lebih tinggi dari konstruktor GpLineGradient, dan memeriksa nilai kembali dalam OK di GdipCreateLineBrush dan sebaliknya mengembalikan status konstruktor. Kemudian, untuk pengguna GDI +, pesan kesalahan yang terjadi di dalam pustaka akan terlihat lebih logis.
Opsi dengan mengganti angka yang sangat kecil dengan nol, mis. dengan isi vertikal, ini berjalan tanpa kesalahan karena keajaiban yang dilakukan Microsoft dalam metode LinearGradientRectFromPoints di baris 35 hingga 45:
Ajaib 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; }
Bagaimana cara mengobati?
Bagaimana cara menghindari crash ini dalam kode .NET? Opsi paling sederhana dan paling jelas adalah membandingkan luas persegi panjang dari points1 dan point2 dengan FLT_EPSILON dan tidak membuat gradien jika area lebih kecil. Tetapi dengan opsi ini, kita akan kehilangan informasi tentang gradien, dan area yang tidak dicat akan ditarik, yang tidak bagus. Saya melihat opsi yang lebih dapat diterima ketika memeriksa sudut pengisian gradien, dan jika ternyata isinya dekat dengan horizontal atau vertikal, maka atur parameter yang sesuai untuk titik-titik yang sama:
Solusi saya di 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; }
Bagaimana kabar pesaing?
Mari cari tahu apa yang terjadi di Wine. Untuk melakukan ini, lihat
kode sumber untuk Wine , baris 306:
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; }
Inilah satu-satunya validasi parameter:
if(!line || !startpoint || !endpoint || wrap == WrapModeClamp) return InvalidParameter;
Kemungkinan besar, yang berikut ini telah ditulis untuk kompatibilitas dengan Windows:
if (startpoint->X == endpoint->X && startpoint->Y == endpoint->Y) return OutOfMemory;
Dan sisanya tidak ada yang menarik - alokasi memori dan mengisi bidang. Dari kode sumber, menjadi jelas bahwa dalam Wine pembuatan gradien isian masalah harus dilakukan tanpa kesalahan. Dan sungguh - jika Anda menjalankan program berikut pada Windows (saya berlari pada Windows10x64)
Program uji #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; }
Yang ada di konsol Windows adalah:
Kenangan
Ok
dan di Ubuntu dengan Wine:
Ok
Ok
Ternyata saya melakukan sesuatu yang salah, atau Wine dalam hal ini bekerja lebih logis daripada Windows.
Kesimpulan
Saya sangat berharap bahwa saya tidak mengerti sesuatu dan perilaku GDI + masuk akal. Benar, sama sekali tidak jelas mengapa Microsoft melakukan hal itu. Saya menggali banyak ke dalam produk-produk mereka yang lain, dan ada juga hal-hal seperti itu di masyarakat yang baik tidak akan lolos dari Ulasan Kode.