Делая последний проект на работе, мы с коллегой столкнулись с тем, что некоторые методы и конструкторы в System.Drawing падали с OutOfMemory в совершенно обычных местах, причем когда свободной памяти было еще много.
Суть проблемы
Например, возьмем этот код на C#:При выполнении последней строки гарантированно будет выброшено исключение OutOfMemoryException, независимо от того, сколько свободной памяти доступно.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); } } }
Более того, если заменить 3.367667E-16f и -3.367667E-16f на 0, что очень близко к истине, все будет работать нормально — заливка создастся.
На мой взгляд, такое поведение выглядит странно.
Давайте разберемся, почему это происходит и как с этим бороться.
Выясняем причины заболевания
Начнем с изучения того, что происходит в конструкторе LinearGradientBrush. Для этого вы можете посмотреть referencesource.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. Давайте загрузим gdiplus.dll в IDA. Если вам нужно определить, какую версию библиотеки нужно отлаживать, вы можете использовать Process Explorer из SysInternals. Кроме того, могут возникнуть проблемы с правами на папку, где находится gdiplus.dll. Решаются они сменой владельца этой папки.
Итак, откроем gdiplus.dll в IDA. Подождем обработки файла.
После этого выберите в меню: View → Open Subviews → Exports, чтобы открыть все функции, которые экспортируются из этой библиотеки, и найдите там 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; // esi MAPDST
GpGradientBrush *v8; // eax
GpRectGradient *v9; // eax
int v12; // [esp+4h] [ebp-Ch]
int vColor1; // [esp+8h] [ebp-8h]
int vColor2; // [esp+Ch] [ebp-4h]
FPUStateSaver::FPUStateSaver(&v12, 1);
EnterCriticalSection(&GdiplusStartupCriticalSection::critSec);
if ( Globals::LibraryInitRefCount > 0 )
{
LeaveCriticalSection(&GdiplusStartupCriticalSection::critSec);
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;
}
}
else
{
LeaveCriticalSection(&GdiplusStartupCriticalSection::critSec);
status = GdiplusNotInitialized;
}
__asm { fclex }
return 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; // esi
float height; // ST2C_4
double v8; // st7
float width; // ST2C_4
float angle; // ST2C_4
GpRectF rect; // [esp+1Ch] [ebp-10h]
v6 = this;
GpGradientBrush::GpGradientBrush(this);
GpRectGradient::DefaultBrush(v6);
rect.Height = 0.0;
rect.Width = 0.0;
rect.Y = 0.0;
rect.X = 0.0;
*v6 = &GpLineGradient::`vftable;
if ( LinearGradientRectFromPoints(point1, point2, &rect) )
{
*(v6 + 1) = 1279869254;
}
else
{
height = point2->Y - point1->Y;
v8 = height;
width = point2->X - point1->X;
angle = atan2(v8, width) * 180.0 / 3.141592653589793;
GpLineGradient::SetLineGradient(v6, point1, point2, &rect, color1, color2, angle, 0, wrapMode);
}
return v6;
}
Здесь инициализируются переменные, которые затем заполняются в LinearGradientRectFromPoints и SetLineGradient. Осмелюсь сказать, что rect — это прямоугольник с заливкой, основанный на точках point1 и point2. Чтобы убедиться в этом, вы можете посмотреть LinearGradientRectFromPoints: ЛинейныйГрадиентРектФромПойнтс GpStatus __fastcall LinearGradientRectFromPoints(GpPointF *p1, GpPointF *p2, GpRectF *result)
{
double vP1X; // st7
float vLeft; // ST1C_4 MAPDST
double vP1Y; // st7
float vTop; // ST1C_4 MAPDST
float vWidth; // ST18_4 MAPDST
double vWidth3; // st7
float vHeight; // ST18_4 MAPDST
float vP2X; // [esp+18h] [ebp-8h]
float vP2Y; // [esp+1Ch] [ebp-4h]
if ( IsClosePointF(p1, p2) )
return InvalidParameter;
vP2X = p2->X;
vP1X = p1->X;
if ( vP2X <= vP1X )
vP1X = vP2X;
vLeft = vP1X;
result->X = vLeft;
vP2Y = p2->Y;
vP1Y = p1->Y;
if ( vP2Y <= vP1Y )
vP1Y = vP2Y;
vTop = vP1Y;
result->Y = vTop;
vWidth = p1->X - p2->X;
vWidth = fabs(vWidth);
vWidth3 = vWidth;
result->Width = vWidth;
vHeight = p1->Y - p2->Y;
vHeight = fabs(vHeight);
result->Height = vHeight;
vWidth = vWidth3;
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;
}
return 0;
}
Как и ожидалось, rect — это прямоугольник, состоящий из точек 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; // edi
float *v11; // edi
GpStatus v12; // esi
_DWORD *v14; // edi
this->wrapMode = wrapMode;
v10 = &this->dword40;
this->Color1 = *color1;
this->Color2 = *color2;
this->Color11 = *color1;
this->Color21 = *color2;
this->dwordB0 = 0;
this->float98 = 1.0;
this->dwordA4 = 1;
this->dwordA0 = 1;
this->float94 = 1.0;
this->dwordAC = 0;
if ( CalcLinearGradientXform(zero, rect, angle, &this->gap4[16]) )
{
*this->gap4 = 1279869254;
*v10 = 0;
v14 = v10 + 1;
*v14 = 0;
++v14;
*v14 = 0;
v14[1] = 0;
*&this[1].
gap4[12] = 0; *&this[1].
gap4[16] = 0; *&this[1].
gap4[20] = 0; *&this[1].
gap4[24] = 0; *&this->gap44[28] = 0; v12 = InvalidParameter; } else { *this->gap4 = 1970422321; *v10 = LODWORD(rect->X); v11 = (v10 + 1); *v11 = rect->Y; ++v11; *v11 = rect->Width; v11[1] = rect->Height; *&this->gap44[28] = zero; v12 = 0; *&this[1].
gap4[12] = *p1; *&this[1].
gap4[20] = *p2;
}
return v12;
}
SetLineGradient также только инициализирует поля.
Итак, нам нужно пойти глубже: int __fastcall CalcLinearGradientXform(int zero, GpRectF *rect, float angle, int a4)
{
//.
//.
//.
return GpMatrix::InferAffineMatrix(a4, points, rect) != OK ? InvalidParameter : OK;
}
И наконец: GpStatus __thiscall GpMatrix::InferAffineMatrix(int this, GpPointF *points, GpRectF *rect)
{
//.
double height; // st6 double y; // st5 double width; // st4 double x; // st3 double bottom; // st2 float right; // ST3C_4 float rectArea; // ST3C_4 //.
x = rect->X; y = rect->Y; width = rect->Width; height = rect->Height; right = x + width; bottom = height + y; rectArea = bottom * right - x * y - (y * width + x * height); rectArea = fabs(rectArea); if ( rectArea < 0.00000011920929 ) return InvalidParameter; //.
}
В методе InferAffineMatrix происходит именно то, что нас интересует. Здесь проверяется площадь rect — исходный прямоугольник из точек, и если она меньше 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+.
Но все, что вам нужно было сделать, это переслать приведенный выше статус из конструктора 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 из Wine /******************************************************************************
* GdipCreateLineBrush [GDIPLUS.@]
*/
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;
В остальном ничего интересного — выделение памяти и заполнение полей.
Из исходного кода ясно, что создание проблемной градиентной заливки в Wine должно пройти гладко.
И действительно - если запустить следующую программу в 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 будет:
Недостаточно памяти Хорошои в Ubuntu с Wine:
Хорошо ХорошоПолучается, что либо я что-то делаю не так, либо Wine в этом вопросе работает более логично, чем Windows.
Заключение
Очень надеюсь, что я что-то неправильно понял и поведение GDI+ логично.Правда, совершенно непонятно, почему Microsoft сделала все именно так.
Я много покопался в других их продуктах, и тоже есть вещи, которые в приличном обществе точно не пройдут Code Review. Теги: #microsoft #gdiplus #обратное проектирование #.
NET #C++ #cpp #outofmemory #синтаксический анализ #.
NET #обратное проектирование #разработка для Windows
-
Почему Вам Следует Найти Стилиста Онлайн
19 Oct, 24 -
Семен Семенов Знает, Каким Будет Googlephone
19 Oct, 24 -
Насколько Маленьким Может Быть Ядро Linux?
19 Oct, 24 -
Вперед В Прошлое. Geforce Fx. Рассвет Войны
19 Oct, 24 -
Что Делает Beos И Haikuos Уникальными?
19 Oct, 24 -
Apple Запатентовала Значок Youtube Для Ios
19 Oct, 24