» » OutOfMemory и GDI+ иногда совсем не OutOfMemory

 

OutOfMemory и GDI+ иногда совсем не OutOfMemory

Автор: admin от 31-05-2018, 16:40, посмотрело: 36

При выполнении последнего проекта на работе мы с коллегой столкнулись с тем, что некоторые методы и конструкторы в System.Drawing падают с OutOfMemory в совершенно обычных местах и когда памяти свободной ещё очень и очень много.

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. Загрузим в IDA gdiplus.dll. Если надо определить, какую именно версию библиотеки отлаживать, то можно воспользоваться Process Explorer от SysInternals. Кроме того, могут возникнуть проблемы с правами на папку, где лежит gdiplus.dll. Они решаются сменой владельца этой папки.

Итак, откроем gdiplus.dll в IDA. Дождёмся обработки файла. После этого выберем в меню: View  Open Subviews  Exports, чтобы открыть все функции, которые экспортируются из этой библиотеки, и найдём там GdipCreateLineBrush.

Благодаря загрузке символов, мощности HexRays и документации можно без труда перевести код метода из ассемблера в читабельный код на С++:



Код этого метода абсолютно понятен. Его суть заключена в строках:

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:



Здесь происходит инициализация переменных, которые потом заполняются в LinearGradientRectFromPoints и SetLineGradient. Смею предположить, что rect — это прямоугольник заливки, основанный на point1 и point2, чтобы убедиться в этом можно заглянуть в LinearGradientRectFromPoints:



Как и предполагалось, rect — прямоугольник из точек point1 и point2.

Теперь вернёмся к нашей основной проблеме и разберёмся что происходит внутри SetLineGradient:



В SetLineGradient тоже происходит только инициализация полей. So, we need to go deeper:

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 = rectX;
  y = rectY;
  width = rectWidth;
  height = rectHeight;
  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 — это машинный эпсилон для float (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.

  • После этого status поменяется на OutOfMemory, что и получит .NET на выходе из GDI+ метода.



Выходит, что Microsoft зачем-то игнорирует возвращаемый статус некоторых методов, делает из-за этого неверные предположения и усложняет понимание работы библиотеки для других программистов. Но ведь всего-то надо было из конструктора GpLineGradient пробрасывать статус выше, а в GdipCreateLineBrush проверять возвращаемое значение на OK и в противном случае возвращать статус конструктора. Тогда для пользователей GDI+ сообщение об ошибке, произошедшей внутри библиотеки, выглядело бы более логичным.

Вариант с заменой очень маленьких чисел на ноль, т.е. с вертикальной заливкой, выполняется без ошибок из-за магии, которую Microsoft выполняет в методе LinearGradientRectFromPoints в строках с 35 по 45:



Как лечить?



Как же избежать этого падения в .NET коде? Самый простой и очевидный вариант — сравнить площадь прямоугольника из точек point1 и point2 с FLT_EPSILON и не создавать градиент, если площадь меньше. Но при таком варианте мы потеряем информацию о градиенте и нарисуется незакрашенная область, что нехорошо. Мне видится более приемлемым вариант, когда проверяется угол градиентной заливки и если выясняется, что заливка близка к горизонтальной или вертикальной, то выставляем одинаковыми соответствующие параметры у точек:



А как дела у конкурентов?



Давайте узнаем что происходит в Wine. Для этого посмотрим на исходный код Wine, строка 306:



Здесь есть единственная проверка параметров на валидность:

if(!line || !startpoint || !endpoint || wrap == WrapModeClamp)
    return InvalidParameter;


Скорее всего следующее было написано для совместимости с Windows:

if (startpointX == endpointX && startpointY == endpointY)
    return OutOfMemory;


А в остальном нет ничего интересного — выделение памяти и заполнение полей. Из исходного кода становится очевидно, что в Wine создание проблемной градиентной заливки должно выполняться без ошибок. И действительно — если запустить следующую программу в Windows (я запускал в Windows10x64)



То в консоли Windows будет:

OutOfMemory

Ok
а в Ubuntu c Wine:

Ok

Ok
Выходит, что либо я что-то делаю не так, либо Wine в этом вопросе работает логичнее, чем Windows.



Заключение



Я очень надеюсь, что это я что-то не понял и поведение GDI+ является логичным. Правда, совсем не понятно зачем Microsoft всё сделала именно так. Я много копался в других их продуктах, и там тоже встречаются такие вещи, которые в приличном обществе точно бы не прошли Code Review.

Источник: Хабр / Интересные публикации

Категория: Программирование, Microsoft

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *