C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 25.05.15 23:00
Оценка:
Всем привет

странная фигня если явно не вызывать delete а дать сборщику мусора самому вызывать
финалайзер который вызывает DestroyWindow, то окна не уничтожаются и происходит утечка USER объектов

вот код, после того как увидите утечку, измените PRODUCE_BUG 1 на 0 и все тут же станет замечательно

using namespace System;
using namespace System::IO;
using namespace System::Threading;
using namespace System::Runtime::InteropServices;
using namespace System::ComponentModel;

ref class DataContainer {
    HWND hwnd;
public:
    DataContainer():m_isDisposed(false) {
        hwnd = CreateWindowA("button", "test", 0,0,0,0,0,0,0,GetModuleHandle(0),0);
    }


    ~DataContainer() {
        if (m_isDisposed)
            return;

        this->!DataContainer(); 
        m_isDisposed = true;
    }

    !DataContainer() {
        DestroyWindow(hwnd);
        Win32Exception e;
        Console::WriteLine("{0} - {1}", e.NativeErrorCode, e.Message);
    }

private:
    bool m_isDisposed; 
};

#define PRODUCE_BUG 1

int main(array<System::String ^> ^args)
{
    for (;;)
    {
        auto obj = gcnew DataContainer();
        Thread::Sleep(1000);
        
        if (!PRODUCE_BUG)
            delete obj;
        else
            GC::Collect();
    }

    return 0;
}


на консоль с PRODUCE_BUG 1 выводится

5 — Access is denied

а с PRODUCE_BUG 0 все замечательно

0 — The operation completed successfully

что это такая за фигня?
Re: C++/CLI утечка USER объектов
От: fddima  
Дата: 25.05.15 23:10
Оценка: +4
Здравствуйте, jyuyjiyuijyu, Вы писали:

J>странная фигня если явно не вызывать delete а дать сборщику мусора самому вызывать

J>финалайзер который вызывает DestroyWindow, то окна не уничтожаются и происходит утечка USER объектов
Тут даже на код не нужно смотреть.
Финализаторы — выполняются в специальном потоке, а DestroyWindow может быть вызвана только из того же самого потока, где окно было создано.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 25.05.15 23:10
Оценка:
офигеть западло там где не ждешь...
вообщем когда вызываешь сам delete то финалайзер вызывается в том потоке в котором и создавалось окно, а когда сборщик мусора запускает финалайзер то он выполняется в специальном потоке и вызов DestroyWindow терпит фейл...

вот какое западло таит в себе финалайзер выполняемый в специальном потоке!!
Re[2]: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 25.05.15 23:12
Оценка:
Здравствуйте, fddima, Вы писали:

F>Тут даже на код не нужно смотреть.

F>Финализаторы — выполняются в специальном потоке, а DestroyWindow может быть вызвана только из того же самого потока, где окно было создано.


да вы правы!
Re[2]: C++/CLI утечка USER объектов
От: fddima  
Дата: 25.05.15 23:27
Оценка:
Здравствуйте, jyuyjiyuijyu, Вы писали:

Ты сам зовёшь "финалайзер" из деструктора.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[3]: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 25.05.15 23:40
Оценка:
Здравствуйте, fddima, Вы писали:

F>Здравствуйте, jyuyjiyuijyu, Вы писали:


F>Ты сам зовёшь "финалайзер" из деструктора.


так, так и надо! тут проблема в другом...
Re[2]: C++/CLI утечка USER объектов
От: Evgeny.Panasyuk Россия  
Дата: 25.05.15 23:46
Оценка:
Здравствуйте, jyuyjiyuijyu, Вы писали:

J>вот какое западло таит в себе финалайзер выполняемый в специальном потоке!!


А ещё порядок вызова финализаторов не определён. Например сначала может отмонтироваться диск, а потом будет попытка удалить file.lock с этого диска.
Re[4]: C++/CLI утечка USER объектов
От: fddima  
Дата: 25.05.15 23:47
Оценка:
Здравствуйте, jyuyjiyuijyu, Вы писали:

F>>Ты сам зовёшь "финалайзер" из деструктора.

J>так, так и надо! тут проблема в другом...
Если коротко, то так не надо.
А если надо — то надо чётко понимать в чём разница, и в чём ограничения финализаторов.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[5]: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 26.05.15 08:25
Оценка:
Здравствуйте, fddima, Вы писали:

F>Здравствуйте, jyuyjiyuijyu, Вы писали:


F>>>Ты сам зовёшь "финалайзер" из деструктора.

J>>так, так и надо! тут проблема в другом...
F> Если коротко, то так не надо.
F> А если надо — то надо чётко понимать в чём разница, и в чём ограничения финализаторов.

почему так не надо? вот например тут так и советуют http://manski.net/2012/01/idisposable-finalizer-and-suppressfinalize/
Re[6]: C++/CLI утечка USER объектов
От: fddima  
Дата: 26.05.15 08:42
Оценка: -1
Здравствуйте, jyuyjiyuijyu, Вы писали:

F>> Если коротко, то так не надо.

F>> А если надо — то надо чётко понимать в чём разница, и в чём ограничения финализаторов.
J>почему так не надо? вот например тут так и советуют http://manski.net/2012/01/idisposable-finalizer-and-suppressfinalize/
В общем случае финализатор вызывается исключительно посредством GC, и как правило финализатор выполняет delete объекта (и по вполне понятным причинам делают именно так). В C++/CLI — по сути всё тоже самое, что и у всех. Если это общая практика совмещать код именно в таком виде — ради бога, зовите финализатор из деструктора. Но, учитывать природу финализационного кода — придется всё равно. Финализаторы вызываются в произвольном порядке, они вызываются в специальных потоках, они даже вроде могут вызываются параллельно, и короче говоря — из них нельзя обращаться к управляемым ресурсам. Если с осознанием есть проблемы — лучше что-то почитать, на эту тему.

PS: Эмоциональный ответ: Не надо потому что C++/CLI — уг от рождения. Тем более никакого там "бесшовной" интеграции не было и нет, а трамплины генерировались невменяемые. Щас мож и нашаманили уже что. И в добавок если нужно работать с разными AppDomains — то всё равно прийдется приседать на каждом шагу. Я разумеется в основном про 'callback' направление говорю (т.е. native->managed). Прямое тривиально и не очень интересно.
Если интересно, надо или очень хочется — то ради бога конечно, толк в извращениях никогда не помешает.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re: C++/CLI утечка USER объектов
От: fddima  
Дата: 26.05.15 08:54
Оценка:
Здравствуйте, jyuyjiyuijyu, Вы писали:

А вот ещё кусочек магии в твоем коде:

!DataContainer() {
    DestroyWindow(hwnd);
    Win32Exception e;
    Console::WriteLine("{0} - {1}", e.NativeErrorCode, e.Message);
    }


Я так понимаю, что DestroyWindows это тупо нативная функция.
Win32Exception зовет Marshal.GetLastWin32Error() который НЕ вызывает GetLastError(), но он всё равно какой-то магией у тебя там оказался.
А какой такой магией он у тебя там оказался?
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[7]: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 26.05.15 09:04
Оценка:
Здравствуйте, fddima, Вы писали:

F>Здравствуйте, jyuyjiyuijyu, Вы писали:


F>>> Если коротко, то так не надо.

F>>> А если надо — то надо чётко понимать в чём разница, и в чём ограничения финализаторов.
J>>почему так не надо? вот например тут так и советуют http://manski.net/2012/01/idisposable-finalizer-and-suppressfinalize/
F> В общем случае финализатор вызывается...



так вроде всё же логично в вызове финализатора из деструктора

1) вызываем мы финалайзер из деструктора (диспосе в c++/cli) потому что компилятор
неявно вставляет в конец деструктора супрессфинализе и тогда неуправляемые ресурсы
рискуют остаться неудаленными если не вызвать здесь и сейчас финалайзер

2) а размещаем удаление неуправляемых ресурсов в финалайзере из за того что, если будет забыт
вызван deletе то хоть когда нибудь эти ресурсы освободит сборщик мусора вызовом финалайзера
Re[2]: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 26.05.15 09:08
Оценка:
Здравствуйте, fddima, Вы писали:

F>Здравствуйте, jyuyjiyuijyu, Вы писали:


F>А вот ещё кусочек магии в твоем коде:


F>
F>!DataContainer() {
F>    DestroyWindow(hwnd);
F>    Win32Exception e;
F>    Console::WriteLine("{0} - {1}", e.NativeErrorCode, e.Message);
F>    }
F>


F>Я так понимаю, что DestroyWindows это тупо нативная функция.

F>Win32Exception зовет Marshal.GetLastWin32Error() который НЕ вызывает GetLastError(), но он всё равно какой-то магией у тебя там оказался.
F>А какой такой магией он у тебя там оказался?

может он не функу использует а читает это значение напрямую из tls потока, как то же он получает код последней ошибки в конструкторе
Re[8]: C++/CLI утечка USER объектов
От: fddima  
Дата: 26.05.15 09:58
Оценка: +1
Здравствуйте, jyuyjiyuijyu, Вы писали:

J>так вроде всё же логично в вызове финализатора из деструктора

J>1) вызываем мы финалайзер из деструктора (диспосе в c++/cli) потому что компилятор
J>неявно вставляет в конец деструктора супрессфинализе и тогда неуправляемые ресурсы
J>рискуют остаться неудаленными если не вызвать здесь и сейчас финалайзер
Раз это общая практика делать именно так — значит так и надо делать.
Я лишь обратил внимание, что есть 2 независимых точки входа, которые выполняются в совершенно разных контекстах. Как мы уже выяснили — это важно.

J>2) а размещаем удаление неуправляемых ресурсов в финалайзере из за того что, если будет забыт

J>вызван deletе то хоть когда нибудь эти ресурсы освободит сборщик мусора вызовом финалайзера
Это очень общий и наивный подход. Общий — потому что, вроде как должен быть применен ко всему, а наивность в том — что нас на самом деле интересует "не если" забыл, а "что бы не забыл", чего финализатор сам по себе и не даёт.

Поэтому:

1. Если это у тебя хэндл файла / что-то такое — это хорошо ложится, например, в SafeHandle. И семантика API и наши желания обычно как раз таковы, что если мы даже не позвали Dispose — хотим освобождения. Более того — мы очень хотим, т.к. SafeHandle — как раз тот пуленепробиваемый щиток. Как они правда ложатся на C++/CLI я не знаю.

2. Если это у тебя thread-bound API — то финализаторы полезны, но сами по себе работу они не сделают — нужно делегеровать работу правильному потоку. Разумеется для этого нужно получить доступ к тем или иным ресурсам. И ещё не забыть о Environment.HasShutdownStarted, и возможно ещё о чём нибудь. Это не вкладывается в обычные паттерны, хоть и используют теже самые механизмы.

3. Если это у тебя Top-Level Window — то тебе на самом деле вообще не нужен финализатор для него. Вместо этого, тебе нужно понятное и детерминированное управление временем жизни окна, из твоего API. Разумеется, ресурсы должны быть освобождены с его закрытием.

4. Если это у тебя Child Window — возможно ты хочешь управляться с финализаторами, как в (2) с thread-bound API.

5. Нативные объекты которые живут исключительно в некотором внешнем scope — и доступны только из этого scope (method scope) — очевидно финализаторов им вообще не нужно.

Придумайте свои варианты.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[3]: C++/CLI утечка USER объектов
От: fddima  
Дата: 26.05.15 10:21
Оценка:
Здравствуйте, jyuyjiyuijyu, Вы писали:

F>>А какой такой магией он у тебя там оказался?

J>может он не функу использует а читает это значение напрямую из tls потока, как то же он получает код последней ошибки в конструкторе
Marshal.GetLastWin32Error(), который использует конструктор — читает из TLS конечно. Вопрос как туда попадает нативный результат GetLastError(). Впрочем ладно. При желании можно разобраться. Я не так давно заглядывал — и ничего такого как мне казалось не видел. Поэтому и показалось странным.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[2]: C++/CLI утечка USER объектов
От: Pavel Dvorkin Россия  
Дата: 26.05.15 16:29
Оценка: 54 (2) +2
Здравствуйте, fddima, Вы писали:

F>Я так понимаю, что DestroyWindows это тупо нативная функция.

F>Win32Exception зовет Marshal.GetLastWin32Error() который НЕ вызывает GetLastError(), но он всё равно какой-то магией у тебя там оказался.
F>А какой такой магией он у тебя там оказался?

Он может там оказаться

For PInvoke, the solution is two-fold:
1) Mark the relevant PInvoke signature with SetLastError=true. This makes the CLR call GetLastError immediately after it calls the target unmanaged API and save the result.
2) Call Marshal.GetLastWin32Error (in System.Runtime.InteropServices) to retrieve the value that the CLR saved.

http://blogs.msdn.com/b/adam_nathan/archive/2003/04/25/56643.aspx

Тут еще один момент есть, и в этом часто делают ошибку (и в примере ТС тоже). GetLastError можно вызывать, только если BOOL функция вернула FALSE. В противном случае ее вообще вызывать нельзя.

If the function succeeds, the return value is nonzero.
If the function fails, the return value is zero. To get extended error information, call GetLastError.

А автор вызывает ее без проверки результата.
With best regards
Pavel Dvorkin
Отредактировано 27.05.2015 1:52 Pavel Dvorkin . Предыдущая версия . Еще …
Отредактировано 26.05.2015 16:34 Pavel Dvorkin . Предыдущая версия .
Re[3]: C++/CLI утечка USER объектов
От: fddima  
Дата: 27.05.15 03:47
Оценка: 98 (3)
Здравствуйте, Pavel Dvorkin, Вы писали:

PD>Он может там оказаться

Всё правильно.

Но, тут не PInvoke, поэтому я твои выдержки опустил, а вставлю другие из того же поста:

For managed C++ and IJW, the story is a little different. This is important for you C++ programmers out there, so I hope you're still reading! If you use DllImport explicitly in C++, the same rules apply as with C#. But when you call unmanaged APIs directly from managed C++ code, neither GetLastError nor Marshal.GetLastWin32Error will work reliably. GetLastError won't work for the same reason that a PInvoke call to it wouldn't work in C#. Marshal.GetLastWin32Error won't work because the implicit PInvoke goop emitted by the compiler doesn't set SetLastError to true. To fix this, you can use #pragma unmanaged to keep code that relies on GetLastError functionality as unmanaged code.

Именно на это я изначально и намекал, что есть какая-то магия.


Теперь о магии.

Пример 1. Упрощенный.
  Скрытый текст
// ConsoleApplication8CppCli.cpp : main project file.

#include "stdafx.h"
#include <windows.h>

using namespace System;
using namespace System::ComponentModel;

int main(array<System::String ^> ^args)
{
    ::DestroyWindow(NULL);  // здесь типа не PInvoke
    Win32Exception e;
    Console::WriteLine(L"{0}", e.NativeErrorCode); // выводит 1400
  return 0;
}


В этом коде: не генериться ничего сверх естественного, или это трудно увидеть (в студии).
Если полазить в CLR, то в Marshal — можной найти обычный геттер, берущий last error у потока (своего).
В Win32Exception вообще специального ничего нет.

Пример 2. Отключаем магию.
  Скрытый текст
// ConsoleApplication8CppCli.cpp : main project file.

#include "stdafx.h"
#include <windows.h>

using namespace System;
using namespace System::ComponentModel;

#pragma managed (push, off)
void unmanaged()
{
    ::DestroyWindow(NULL);
}
#pragma managed (pop)

int main(array<System::String ^> ^args)
{
    unmanaged(); // магия была тут
    Win32Exception e;
    Console::WriteLine(L"{0}", e.NativeErrorCode); // выводит 0
  return 0;
}


Осталось ещё немножко разобраться, почему это GetLastWin32Error в посте — не reliable. И если он не reliable — то как оно тогда работает, и можно ли этому доверять?
Всё до безобразия просто: в JIT / vclinker встроена эвристика, и PInvoke "goop" эмитится в флагом setlasterror для kernel32 (и что там ещё сейчас вместо модно), gdi32, user32, advapi32.

UPD:
1. Т.е. на самом деле — пока мы попадаем в эти эвристики — можно смело использовать и это будет reliable. Пост в блоге немного устарел, а этот вопрос не освещен.
2. Компилятор всё равно генерирует делает "goops" (трамплины, hunks, transitions) тем или иным способ, но неявно. Это логично и ожидаемо.
3. И уже просто в тему — забавно, но DllImport имеет разные значения по умолчанию своих параметров (например ExactSpelling) (C# vs VB.NET).
Отредактировано 27.05.2015 4:11 Mystic Artifact . Предыдущая версия .
Re[3]: C++/CLI утечка USER объектов
От: jyuyjiyuijyu  
Дата: 27.05.15 04:20
Оценка:
Здравствуйте, Pavel Dvorkin, Вы писали:

PD>Тут еще один момент есть, и в этом часто делают ошибку (и в примере ТС тоже). GetLastError можно вызывать, только если BOOL функция вернула FALSE. В противном случае ее вообще вызывать нельзя.


а вот интересно, когда юзается implicit pinvoke как в посте выше, эвристика ставит флаг
setlasterror для апишек сама, то CLR вызывает GetLastError для сохранения результата для GetLastWin32Error в любом случае или только когда апишка вернула FALSE?

не думаю что только когда апишка вернула FALSE, потому что функции мьютекса возвращают хендл(истину) и
устанавливают ошибку если мьютекс уже создан...

так что скорее всего GetLastError вызывается при любом раскладе а не только когда апишка
возвращает FALSE

к тому же некоторые апишки при удачном раскладе устанавливают 0 как lasterror затирая любой
ошибочный код который был прежде
Отредактировано 27.05.2015 4:26 jyuyjiyuijyu . Предыдущая версия .
Re[4]: C++/CLI утечка USER объектов
От: fddima  
Дата: 27.05.15 04:27
Оценка:
Здравствуйте, jyuyjiyuijyu, Вы писали:

PD>>Тут еще один момент есть, и в этом часто делают ошибку (и в примере ТС тоже). GetLastError можно вызывать, только если BOOL функция вернула FALSE. В противном случае ее вообще вызывать нельзя.

J>а вот интересно, когда юзается implicit pinvoke как в посте выше, эвристика ставит флаг
J>setlasterror для апишек сама, то CLR вызывает GetLastError для сохранения результата для GetLastWin32Error в любом случае или только когда апишка вернула FALSE?
Да ты и сам ответил на свой вопрос. Т.е. всегда будет вызываться GetLastError.
В том числе и поэтому в DllImport есть возможность не использовать last error, где он не нужен.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[4]: C++/CLI утечка USER объектов
От: Pavel Dvorkin Россия  
Дата: 27.05.15 14:30
Оценка: +1
Здравствуйте, fddima, Вы писали:

F> Но, тут не PInvoke, поэтому я твои выдержки опустил, а вставлю другие из того же поста:


<skipped>

Согласен. Я просто не обратил внимание, что там С++. Вижу вызов DestroyWindow, а ты еще до этого написал, что полагаешь, что это вызов нативной DestroyWindow, а как ее можно вызвать, если не через PInvoke в C#-то ? Ну я и того...

В общем, в болото этот MC++ вместе с его managed-unmanaged гибридизацией.
With best regards
Pavel Dvorkin
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.