Как избавиться от NET COM-объекта взаимодействия на Release () - PullRequest
9 голосов
/ 12 ноября 2009

У меня есть COM-объект, написанный на управляемом коде (C ++ / CLI). Я использую этот объект в стандарте C ++.
Как заставить деструктор моего COM-объекта вызываться сразу после освобождения COM-объекта? Если это невозможно, вызовите функцию Release () для вызова метода MyDispose () моего COM-объекта?

Мой код для объявления объекта (C ++ / CLI):

    [Guid("57ED5388-blahblah")]
    [InterfaceType(ComInterfaceType::InterfaceIsIDispatch)]
    [ComVisible(true)]
    public interface class IFoo
    {
        void Doit();
    };

    [Guid("417E5293-blahblah")]
    [ClassInterface(ClassInterfaceType::None)]
    [ComVisible(true)]
    public ref class Foo : IFoo
    {
    public:
        void MyDispose();
        ~Foo() {MyDispose();} // This is never called
        !Foo() {MyDispose();} // This is called by the garbage collector.
        virtual ULONG Release() {MyDispose();} // This is never called
        virtual void Doit();
    };

Мой код для использования объекта (родной C ++):

#import "..\\Debug\\Foo.tlb"
...
Bar::IFoo setup(__uuidof(Bar::Foo)); // This object comes from the .tlb.
setup.Doit();
setup->Release(); // explicit release, not really necessary since Bar::IFoo's destructor will call Release().

Если я добавлю метод-деструктор в мой COM-объект, он никогда не будет вызван. Если я добавляю метод финализатора, он вызывается, когда сборщик мусора обходит его. Если я явно вызываю переопределение Release (), оно никогда не вызывается.

Мне бы очень хотелось, чтобы когда мой родной объект Bar :: IFoo выходил из области видимости, он автоматически вызывал код удаления моего объекта .NET. Я думаю, я мог бы сделать это путем переопределения Release (), и если число объектов = 0, то вызовите MyDispose (). Но, очевидно, я неправильно переопределяю Release (), потому что мой метод Release () никогда не вызывается.

Очевидно, что я могу сделать это, поместив мой метод MyDispose () в интерфейс и требуя, чтобы люди, использующие мой объект, вызывали MyDispose () перед Release (), но было бы гораздо приятнее, если бы Release () просто очистил объект.

Можно ли заставить принудительно вызывать деструктор COM-объекта .NET или какой-либо другой метод сразу после освобождения COM-объекта?

Попытка погуглить эту проблему заставляет меня обращать внимание на вызов System.Runtime.InteropServices.Marshal.ReleaseComObject (), но, конечно, именно так вы говорите .NET выпустить COM-объект. Я хочу, чтобы COM Release () избавился от объекта .NET.

Ответы [ 6 ]

10 голосов
/ 21 декабря 2010

У меня есть COM-объект, написанный на управляемом коде (C ++ / CLI). Я использую этот объект в стандарте C ++. Как заставить деструктор моего COM-объекта вызываться сразу после освобождения COM-объекта? Если это невозможно, могу ли я, чтобы Release () вызывал метод Dispose () (не MyDispose () - GBG) для моего COM-объекта (управляемого DotNet - GBG)?

RE: Принудительное освобождение ресурсов, связанных с COM-сервером DotNet, когда клиент использует неуправляемый код. Эти ресурсы могут быть организованы для освобождения, когда сборщик мусора собирает элемент, но это не так. детерминированные и такие ресурсы, как файловые потоки, не могут высвобождаться часами или днями для больших систем памяти, где сборка мусора происходит редко.

Это распространенная проблема с COM Callable Wrappers (CCW), которая может быть замечена другим несколько связанным потоком по адресу: Возможно ли перехватить (или знать о) COM-подсчет ссылок на объекты CLR, выставленные COM . в этом случае, как и в любом случае, когда кто-то пишет свой собственный COM-клиент, независимо от того, находится ли он под управляемым или неуправляемым кодом, это легко решить, просто вызвав метод IDisposable.Dispose (), как это было сделано там. Однако этот метод не будет работать, скажем, для класса COM-кодеков DotNet, чьим клиентом может быть сама операционная система, и которому не нужно знать, что COM-сервер неуправляем или не управляется (DotNet).

Можно реализовать шаблон IDisposable.Dispose () на COM-сервере DotNet согласно ссылке MSDN: http://msdn.microsoft.com/en-us/library/system.idisposable.aspx,, но это не принесет пользы, потому что метод Dispose () никогда не будет вызван CCW. В идеале реализация CCW в mscoree.dll должна действительно проверять и вызывать метод IDisposable.Dispose (), если он реализован как часть выпуска CCW и / или финализации / деструктора. Я не уверен, почему Microsoft не сделала этого, поскольку, имея полный доступ к информации о сборке, они могли легко определить, поддерживает ли класс DotNet COM класс IDisposable, и просто вызвать Dispose () в окончательном выпуске, если это произойдет, и так как это будет в пределах CCW, можно избежать всех сложностей с обработкой подсчета ссылок из-за дополнительной ссылки на интерфейс.

Я не вижу, как это «сломало бы» любой существующий код, поскольку любой клиент, который знает IDisposable, мог бы все еще вызывать Dispose (), который, если реализован в соответствии с вышеуказанным шаблоном, только эффективно что-либо делает при первом вызове. Microsoft может беспокоиться о том, что класс Disposed, хотя до сих пор существуют управляемые ссылки на него, которые не будут знать о его удалении до тех пор, пока не начнут возникать исключения, возникающие при попытке использовать уже удаленные ресурсы, но это потенциальная проблема для любого неправильного использования. использование интерфейса IDisposable даже с только клиентами DotNet: если существует несколько ссылок на один и тот же экземпляр объекта, и любой из них вызывает Dispose (), другие обнаружат, что попытка использовать требуемые удаленные ресурсы вызывает исключения. В таких случаях всегда следует устанавливать защитные элементы с использованием логического значения (согласно шаблону IDisposable) или ссылаться на объект только через общую оболочку.

Поскольку Microsoft не выполнила необходимые несколько строк кода при реализации CCW в mscoree.dll, я написал оболочку для mscoree.dll, которая добавляет эту дополнительную функциональность. Это немного сложно, поскольку для управления созданием моей обертки вокруг любого экземпляра любого COM-класса DotNet мне нужно также обернуть интерфейс IClassFactory и объединить экземпляр CCW в моем классе-обертке "CCW_Wrapper". Эта оболочка также поддерживает дополнительные уровни агрегации из еще одного внешнего класса. Код также подсчитывает ссылки на экземпляры классов в используемой реализации mscoree.dll, чтобы иметь возможность вызывать FreeLibrary для mscoree.dll, когда нет ссылок (и снова загружать LoadLibrary, если потребуется позже). Код также должен быть многопоточным, что требуется для COM в Windows 7. Мой код C ++ выглядит следующим образом:

РЕД., 22 декабря 2010 г .: исключен один ненужный параметр конструктора COM_Wrapper:

#include <windows.h>

HMODULE g_WrappedDLLInstance = NULL;
ULONG g_ObjectInstanceRefCnt = 0;

//the following is the C++ definition of the IDisposable interface
//using the GUID as per the managed definition, which never changes across
//DotNet versions as it represents a hash of the definition and its
//namespace, none of which can change by definition.
MIDL_INTERFACE("805D7A98-D4AF-3F0F-967F-E5CF45312D2C")
    IDisposable : public IDispatch {
    public:
        virtual VOID STDMETHODCALLTYPE Dispose() = 0;
    };

class CCW_Wrapper : public IUnknown {
public:
    // constructor and destructor
    CCW_Wrapper(
        __in IClassFactory *pClassFactory,
        __in IUnknown *pUnkOuter) :
            iWrappedIUnknown(nullptr),
            iOuterIUnknown(pUnkOuter),
            iWrappedIDisposable(nullptr),
            ready(FALSE),
            refcnt(0) {
        InterlockedIncrement(&g_ObjectInstanceRefCnt);
        if (!this->iOuterIUnknown)
            this->iOuterIUnknown = static_cast<IUnknown*>(this);
        pClassFactory->CreateInstance(
            this->iOuterIUnknown,
            IID_IUnknown,
            (LPVOID*)&this->iWrappedIUnknown);
        if (this->iWrappedIUnknown) {
            if (SUCCEEDED(this->iWrappedIUnknown->QueryInterface(__uuidof(IDisposable), (LPVOID*)&this->iWrappedIDisposable)))
                this->iOuterIUnknown->Release(); //to clear the reference count caused by the above.
        }
        this->ready = TRUE; //enable destruction of the object when release decrements to zero.
        //OUTER IUNKNOWN OBJECTS MUST ALSO PROTECT THEIR DESTRUCTORS IN SIMILAR MANNERS!!!!!
    }
    ~CCW_Wrapper() {
        this->ready = FALSE; //protect from re-entering this destructor when object released to zero.
        if (this->iWrappedIDisposable) {
            //the whole reason for this project!!!!!!!!
            this->iWrappedIDisposable->Dispose();
            //the following may be redundant, but to be sure...
            this->iOuterIUnknown->AddRef();
            this->iWrappedIDisposable->Release();
        }
        if (this->iWrappedIUnknown)
            this->iWrappedIUnknown->Release();
        if (!InterlockedDecrement(&g_ObjectInstanceRefCnt)) {
            //clear all global resources including the mutex, multithreading safe...
            HMODULE m = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)0);
            if (m)
                FreeLibrary(m);
        }
    }

    // IUnknown Interface
    STDMETHOD(QueryInterface)(REFIID riid, void **ppv) {
        if (ppv) {
            *ppv = nullptr;
            if (riid == IID_IUnknown) {
                *ppv = static_cast<IUnknown*>(this);
                this->AddRef();
                return S_OK;
            }
            else if (this->iWrappedIUnknown) {
                return this->iWrappedIUnknown->QueryInterface(riid, ppv);
            }
            return E_NOINTERFACE;
        }
        return E_INVALIDARG;
    }

    STDMETHOD_(ULONG, AddRef)() {
        return InterlockedIncrement(&this->refcnt);    
    }

    STDMETHOD_(ULONG, Release)() {
        if (InterlockedDecrement(&this->refcnt))
            return this->refcnt;
        if (this->ready) //if not being constructed or destructed...
            delete this;
        return 0;
    }

private:
    IUnknown *iOuterIUnknown;
    IUnknown *iWrappedIUnknown;
    IDisposable *iWrappedIDisposable;
    BOOL ready;
    ULONG refcnt;
};

class ClassFactoryWrapper : public IClassFactory {
public:
    // constructor and destructor
    ClassFactoryWrapper(IClassFactory *icf) : wrappedFactory(icf), refcnt(0), lockcnt(0) {
        InterlockedIncrement(&g_ObjectInstanceRefCnt);
    }
    ~ClassFactoryWrapper() {
        if (wrappedFactory)
            wrappedFactory->Release();
        if (!InterlockedDecrement(&g_ObjectInstanceRefCnt)) {
            //clear all global resources, multithreading safe...
            HMODULE m = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)0);
            if (m)
                FreeLibrary(m);
        }
    }

    // IUnknown Interface
    STDMETHOD(QueryInterface)(REFIID riid, void **ppv) {
        if (ppv) {
            *ppv = nullptr;
            if (riid == IID_IUnknown) {
                *ppv = static_cast<IUnknown*>(this);
                this->AddRef();
            }
            else if (riid == IID_IClassFactory) {
                *ppv = static_cast<IClassFactory*>(this);
                this->AddRef();
            }
            else {
                return E_NOINTERFACE;
            }
            return S_OK;
        }
        return E_INVALIDARG;
    }

    STDMETHOD_(ULONG, AddRef)() {
        return InterlockedIncrement(&this->refcnt);    
    }

    STDMETHOD_(ULONG, Release)() {
        if (InterlockedDecrement(&this->refcnt) || this->lockcnt)
            return this->refcnt;
        delete this;
        return 0;
    }

    // IClassFactory Interface
    STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppv) {
        HRESULT result = E_INVALIDARG;

        if (ppv) {
            *ppv = nullptr;
            if (pUnkOuter && (riid != IID_IUnknown))
                return result;
            CCW_Wrapper *oipm = new CCW_Wrapper(wrappedFactory, pUnkOuter);
            if (!oipm)
                return E_OUTOFMEMORY;
            if (FAILED(result = oipm->QueryInterface(riid, ppv)))
                delete oipm;
        }

        return result;
    }

    STDMETHOD(LockServer)(BOOL fLock) {
        if (fLock)
            InterlockedIncrement(&this->lockcnt);
        else {
            if (!InterlockedDecrement(&this->lockcnt) && !this->refcnt)
                delete this;
        }
        return wrappedFactory->LockServer(fLock);
    }

private:
    IClassFactory *wrappedFactory;
    ULONG refcnt;
    ULONG lockcnt;
};


STDAPI DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, __deref_out LPVOID FAR* ppv) {
    HRESULT result = E_INVALIDARG;

    if (ppv) {
        *ppv = nullptr;
        if ((riid != IID_IUnknown) && (riid != IID_IClassFactory))
            return E_NOINTERFACE;
        HMODULE hDLL = LoadLibrary(L"mscoree.dll");
        if (!hDLL)
            return E_UNEXPECTED;
        typedef HRESULT (__stdcall *pDllGetClassObject) (__in REFCLSID, __in REFIID, __out LPVOID *);
        pDllGetClassObject DllGetClassObject = (pDllGetClassObject)GetProcAddress(hDLL, "DllGetClassObject");
        if (!DllGetClassObject) {
            FreeLibrary(hDLL);
            return E_UNEXPECTED;
        }
        IClassFactory *icf = nullptr;
        if (FAILED(result = (DllGetClassObject)(rclsid, IID_IClassFactory, (LPVOID*)&icf))) {
            FreeLibrary(hDLL);
            return result;
        }
        ClassFactoryWrapper *cfw = new ClassFactoryWrapper(icf);
        if (!cfw) {
            icf->Release();
            FreeLibrary(hDLL);
            return E_OUTOFMEMORY;
        }
        //record the HMODULE instance in global variable for freeing later, multithreaded safe...
        hDLL = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)hDLL);
        if (hDLL)
            FreeLibrary(hDLL);
        if (FAILED(result = cfw->QueryInterface(IID_IClassFactory, ppv)))
            delete cfw; //will automatically free library and the held class factory reference if necessary.
    }
    return result;    
}

extern "C"
HRESULT __stdcall DllCanUnloadNow(void) {
    if (g_ObjectInstanceRefCnt)
        return S_FALSE;
    return S_OK;
}

extern "C"
BOOL APIENTRY DllMain( HMODULE hModule,
                                                DWORD  ul_reason_for_call,
                                                LPVOID lpReserved ) {
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

Файл '.def' также требуется для DLL следующим образом:

LIBRARY mscoreeCOM_DisposeWrapper

EXPORTS
    DllCanUnloadNow         PRIVATE
    DllGetClassObject       PRIVATE

Чтобы использовать этот исходный код, скомпилируйте его в DLL и установите DLL в папку Windows SYSTEM, затем попросите программу установки или метод [COMRegisterFunction] на вашем COM-сервере DotNet изменить запись реестра класса для InprocServer32 из mscoree. .dll к имени этой оболочки (скажем, mscoreeWrapper.dll). Его можно скомпилировать в 32- и / или 64-разрядных системах, а для установки в 64-разрядных системах необходимо поместить 64-разрядную версию в системную папку, а 32-разрядную - в папку SysWOW64; Кроме того, как обычная регистрация CLSID, так и виртуализированные версии WOW6432 должны быть изменены в соответствии с записями InprocServer32. Некоторые приложения могут требовать, чтобы эта DLL-оболочка имела цифровую подпись, чтобы работать без проблем, что является совершенно другой темой. Если кто-то захочет, я сделаю ссылку на мои скомпилированные версии этих DLL, доступные здесь.

Как я уже сказал, в mscoree.dll должна быть включена необходимая техника в несколько строк (исключая требования к оболочке). Кто-нибудь знает, как связаться с кем-то из соответствующего отдела Microsoft, чтобы сделать это предложение?

EDITADD: Я отправил предложение для DotNet Framework в Microsoft Connect. Похоже, это лучший способ дать обратную связь Microsoft.

EDITADD2: При реализации обходного пути для этой проблемы я понял, почему MIcrosoft, скорее всего, не будет реализовывать это "Автоматический вызов Dispose, если поддерживается, когда счетчик ссылок CCW падает до нуля". При написании обходного пути я должен был получить указатель ссылки на интерфейс COM на управляемом объекте, чтобы передать его чисто неуправляемому методу COM, а затем должен был Release () с этим счетчиком ссылок, чтобы сильно не иметь CCW ссылаясь на объект и, следовательно, вызывая утечку памяти, никогда не имея ее доступной для сборки мусора. Я сделал это, потому что знаю, что уменьшение числа ссылок до нуля на управляемом объекте в настоящее время только заставляет CCW отбросить его сильную ссылку на объект , что делает его пригодным для сбора мусора, если нет других ссылок. Однако, если Microsoft внедрила исправление Auto Dispose, как я предлагал, или если этот код был на месте, оборачивая функциональность mscoree.dll, это вызвало бы Dispose () на управляемом объекте, когда он не был нужен. В этом конкретном случае я мог бы «защитить» виртуальный метод Dispose (bool dispose), чтобы предотвратить возникновение Dispose (), , но для любого существующего кода, использующего это поведение, с тем же допущением, включая реализацию Microsoft среды выполнения DotNet. Библиотеки, реализующие это «исправление» в CCW, сломали бы существующий код . Это исправление обертки все еще применяется к COM-серверам, которые каждый пишет самостоятельно и знает об этом побочном эффекте, поскольку они могут поставить «охранников» на место в Dispose ().

EDITADD 3: В дальнейшей работе с этим я вижу, что мое предложение Microsoft остается в силе, и проблем с «взломом» существующего кода можно избежать с помощью исправления, которое вызовет IDisposable.Dispose () в экземпляре объекта, реализующем управляемый COM-сервер, если интерфейс существует , только если новый настраиваемый атрибут, такой как [AutoComDispose (true)], значение по умолчанию которого равно false, применяется к классу управляемого COM-сервера . Таким образом, программист выбрал бы реализацию функциональности, а документация по новому атрибуту предостерегла бы его использование в отношении необходимости «охранять» метод Dispose (), например, с «искусственным подсчетом ссылок», когда есть возможность Метод Marshal.Release (), вызываемый явно в коде, используемом управляемым сервером, или неявно посредством вызовов таких методов, как Marshal.GetObjectForIUnknown (), которые в некоторых случаях могут вызывать QueryInterface и Release контрольной точки, если ComObject является управляемым объект.

Основная проблема с этим Ответом - сложность установки его для использования, как отмечено выше.

3 голосов
/ 12 ноября 2009

На самом деле ни Dispose (или я должен сказать ~ Foo), ни Release не будут вызваны из COM-клиента, когда будет выпущена последняя ссылка. Это просто не реализовано. Вот некоторая идея, как такое можно сделать.

http://blogs.msdn.com/oldnewthing/archive/2007/04/24/2252261.aspx#2269675

Но метод не рекомендован даже автором.

Если вы также внедрили COM-клиент, лучшим вариантом было бы запросить IDisposable и явно вызвать Dispose, iid для запроса:

{805D7A98-D4AF-3F0F-967F-E5CF45312D2C}

Другой вариант, о котором я могу подумать, это реализовать своего рода «COM Garbage Collector». Каждый объект, созданный COM, будет помещен в список (при условии, что объекты вашего типа могут создаваться только с помощью COM - я не могу придумать какой-либо метод, который бы отличал место создания объекта). И тогда вам придется периодически проверять список, и для каждого объекта вызывать что-то вроде этого:

IntPtr iUnk = Marshal.GetIUnknownForObject(@object);
int refCount = Marshal.Release(iUnk);
if (refCount == 0)
    @object.Dispose();

но это какая-то безумная идея.

1 голос
/ 08 января 2011

Мне кажется, что есть три подхода, позволяющие решить поставленные здесь вопросы, а именно:

  1. Если у вас есть прямой контроль над собственным неуправляемым клиентом, он может вызывать IDisposable.Dispose () напрямую, когда это поддерживается, как я ясно показал в ответе; это самое простое решение, но не всегда можно контролировать клиента.

  2. В соответствии с еще одним ответом на дополнительный вопрос о том, как сделать так, чтобы метод Dispose () вызывался автоматически, когда счетчик ссылок CCW обнуляется, я показал, что можно обернуть реализацию неуправляемого кода COM Callable Wrappers (CCW) с неуправляемым кодом DLL для поддержки этой функции. Это решение не сложно написать, но сложно установить.

  3. В предыдущем ответе я попытался представить третью альтернативу решению вторичного вопроса, описанного выше, используя методы «исправления» / «взлома» / «перехвата» / «перебора», чтобы функциональность могла быть полностью поддерживается управляемым кодом, поэтому не требует дополнительной установки, отличной от той, которая обычно требуется для управляемых COM-серверов DotNet. Я почти отказался от этого решения, так как после его завершения, включая обнаружение и реализацию поддержки агрегирования клиентов, я обнаружил, что какое-то событие, связанное с процессом сканирования сборки мусора, приводит к тому, что исправленные указатели не исправляются, что означает, что мой процедуры теряют контроль за отслеживанием количества ссылок и, следовательно, процессом удаления.

  4. Здесь я предлагаю свой последний ответ на дополнительный вопрос, используя новый интерфейс ICustomQueryInterface, доступный только в DotNetFramework версии 4.0 (и, по-видимому, более новой), что позволяет избежать необходимости выполнять большинство рискованных исправлений существующие таблицы виртуальных методов. Используя этот новый интерфейс, один все еще создает таблицы виртуальных методов, но ему нужно заниматься только управлением внутреннего интерфейса IUnknown, используемого только тогда, когда COM-сервер агрегируется клиентом. Метод имеет небольшое, но не серьезное ограничение, которое я расскажу после того, как будет представлена ​​реализация.

Вторичный вопрос действительно связан с тем, почему фактический Dispose (), связанный с деструктором ~ Foo (), не вызывается автоматически, когда счетчик ссылок COM обнуляется, поэтому причина в том, что эта функция не реализована COM Callable Wrapper (CCW), по крайней мере, в настоящее время. Этот вторичный вопрос, на который необходимо ответить, связан с проблемой, которая заключается в том, что, когда не пишется COM-клиент, эти клиенты в общем случае пишутся с учетом неуправляемых COM-серверов с собственным кодом, но COM-сервер требует детерминированного удаления ресурсов, как в Текст вопроса выглядит следующим образом:

Мне бы очень хотелось, чтобы когда мой родной объект Bar :: IFoo выходил из области видимости, он автоматически вызывал код удаления моего объекта .NET.

Здесь, спрашивающий, на самом деле не понимает, что нативный C ++ не имеет механизма для определения, когда переменные на основе стека выходят из области видимости, в отличие от C ++ / CLI, использующего «семантику стека», и что необходимо вручную вызовите метод Release (), как я показал. Таким образом, правильный ответ дается следующим образом:

На самом деле ни Dispose (или я должен сказать ~ Foo), ни Release не будут вызваны из COM-клиента, когда будет выпущена последняя ссылка. Это просто не реализовано. Вот некоторая идея, как это можно сделать.

http://blogs.msdn.com/oldnewthing/archive/2007/04/24/2252261.aspx#2269675

But метод даже не рекомендован автором.

Выше верно, что неуправляемый Com Callable Wrapper (CCW) не поддерживает интерфейс IDisposable (интерфейс, который автоматически подразумевается синтаксисом ~ Foo () C ++ / CLI), и, следовательно, не вызывает (фактический) Dispose () метод, который запускает все, что происходит по желанию OP (и всех остальных). Тем не менее, есть обходные пути, чтобы вызвать это, как я привел здесь.

Я подумал об идее, выраженной "MadQ" в соответствии со страницей, указанной выше в нижней части страницы, и его идеях о реализации исправления в управляемом коде путем "перехвата" / "перебора" указателей vTable, и Я подумал, что эта идея может работать с некоторыми изменениями. Хотя это правда, что эта идея не может быть полностью целесообразной в том смысле, что вы эффективно пишете самоизменяющийся код при создании собственных записей vTable, это ничем не отличается от интерфейсных прокси, созданных CCW для поддержки DotNet. COM серверы. Есть довольно много упущений и проблем с основной посылкой, изложенной «MadQ» следующим образом:

  1. Было предложено подключить или «перевернуть» интерфейс IUnknown, чтобы можно было проверить, когда счетчик ссылок падает до нуля, но для более распространенного неагрегированного случая все остальные интерфейсы также может изменить счетчик ссылок экземпляра объекта, включая все управляемые интерфейсы, а также неуправляемые интерфейсы, автоматически предоставляемые CCW. Эти последние могут включать IMarshal, IProvideClassInfo, ISupportErrorInfo, возможно, поддерживающие интерфейсы для этих последних двух в ItypeInfo и IErrorInfo соответственно, IDispatch, если класс использует параметры AutoDispatch или AutoDual, которые также создадут интерфейс coclass, IConnectionPointContainer и, возможно, IConnectionPoint, если события COM реализовано, IDispatchEx, если интерфейс IExpando реализован в управляемом классе, и IEnumVARIANT, если управляемый класс реализует интерфейс IEnumerable, как и для коллекций (могут быть другие неоткрытые интерфейсы).

  2. Для неагрегированного случая, когда он был обнаружен, можно добавить «перестановки» / перехват для всех управляемых указателей Release (), как это было предложено для IUnknown; однако, поскольку другие неуправляемые интерфейсы реализованы в неуправляемом C ++, они находятся в пространстве кода, и таблицы vTable не могут быть записаны / исправлены. Это можно решить путем создания копий виртуальных таблиц в выделенной памяти задач Co, но затем сами процедуры обнаруживают, что объекты оторвались от истинных виртуальных таблиц. Обходной путь для этого заключается в использовании статического словаря экземпляров объектов и указателей, к которым они принадлежат, чтобы можно было искать фактические объекты, затем пропатченные указатели vTable интерфейса с исправлениями, вызываемый метод и повторное исправление указателей для каждого метода в каждом из неуправляемых интерфейсов - очень сложная работа, и она не так хороша во всех потоках в обоих направлениях между неуправляемым и управляемым кодом. Кроме того, существуют серьезные потенциальные проблемы, связанные с поддержкой многопоточности, поэтому необходимо найти другие обходные пути к проблеме. Таким образом, я попытался применить исправления для управляемых интерфейсов и только новые моделируемые таблицы виртуальных методов для внутреннего интерфейса IUnknown, используемые для агрегирования; однако я столкнулся с серьезной проблемой, описанной ниже в 4.

  3. Я заметил в документации Microsoft на http://msdn.microsoft.com/en-us/library/aa720632(VS.71).aspx], что класс .Net может предоставлять свои собственные реализации этих интерфейсов, которые переопределяют эти реализации собственного кода, поэтому все интерфейсы могут выглядеть как управляемые классы, а реализации появляются заменить неуправляемые интерфейсы. Это работоспособное решение, но при необходимости потребуется много работы для реализации всех этих интерфейсов в управляемом коде.

  4. В качестве альтернативы можно просто пропатчить методы QueryInterface, чтобы неуправляемые интерфейсы не поддерживались результирующим управляемым COM-сервером, что наиболее необходимо, как в первоначальном вопросе. Фактически это делает вышеупомянутые неуправляемые реализации интерфейсов полной проблемой, поскольку версия QueryInterface () с управляемым кодом может отвечать только на реализованные управляемые классы и отклонять любые запросы для тех других интерфейсов, которые не требуются или не реализуются как управляемые версии. из интерфейсов. Это основное ограничение для реализации этого с использованием DotNetFramework менее версии 4.0, за исключением того, что сборщик мусора не исправляет / восстанавливает таблицы виртуальных методов, если исправлен исходный интерфейс IUnknown, то больше нельзя использовать Marshal.GetObjectForIUnknown () и Marshal.GetUniqueObjectForIUnknown () на этом сервере, что было бы необходимо, если бы управляемый COM-сервер DotNet должен был быть передан в качестве параметра другому объекту управляемого COM-сервера другим неуправляемым кодом. Например, части системы неуправляемых кодеков Windows Imaging Component (WIC) могут передавать экземпляры управляемого объекта IBitmapDecoder в управляемый код WPF, который не сможет вызывать методы из этого класса через Runtime Callable Wrapper (RCW), если только эти методы Marshal не работают , Мне не удалось обмануть систему, заставив ее думать, что реализация - это неуправляемый интерфейс, который вызывает поддержку полного RCW, как и для неуправляемого кода.

  5. Еще одна альтернатива управлению этими неуправляемыми «дополнительными» интерфейсами заключается в написании управляемых реализаций этих интерфейсов для простого вызова неуправляемого кода, уже внедренного в CCW, что, вероятно, является самым простым способом управления этими интерфейсами. если они требуются.

  6. Наконец, "MadQ" пропустил хитрость в том, что нет ничего плохого в том, что счетчик CCW увеличивается и уменьшается до нуля до того, как связи "зацепятся" , поскольку это не вызывает Удаляет тип C ++ и удаляет только сильную ссылку на экземпляр управляемого объекта, предотвращая его финализацию и сборку мусора. Таким образом, один не зависит от того, что управляемый класс можно использовать только для неуправляемых COM-клиентов, поскольку счетчик ссылок, по крайней мере, одного никогда не позволяет собирать мусор для клиентов .Net, поскольку при построении счетчик ссылок можно оставить равным нулю. несмотря на некоторые ссылки на различные интерфейсы экземпляров.

Что хорошего в этой технике, так это:

Этот альтернативный обходной путь реализован исключительно на стороне управляемого кода, которая работает путем «исправления» / «перехвата» / «перебора» виртуальных таблиц методов интерфейсов CCW, чтобы взять под контроль и добавить желаемую функциональность автоматического вызова IDisposable.Dispose. () для производных классов, когда их псевдо-ссылочные значения, предоставленные CCW, уменьшаются до нуля. Это гораздо сложнее написать, чем обертка, но имеет то преимущество, что его очень легко использовать, так как он вносит очень мало изменений в класс COM-сервера, чтобы использовать его, как я покажу, и нет никакой дополнительной работы для установки на все.

Таким образом, преимущества просты в использовании. Тем не менее, есть следующие недостатки:

  1. Даже используя новый интерфейс ICustomQueryInterface, доступный в DotNet 4.0, невозможно контролировать / отслеживать обычный неагрегированный интерфейс IUnknown для экземпляра объекта, и в то же время можно запретить запросам на этом интерфейсе находить любые другие интерфейсы, которые не хочет выставлять на COM, нельзя отследить подсчет ссылок, выполненный через этот интерфейс. К счастью, кажется, что можно контролировать отслеживание подсчета ссылок для обычно скрытого внутреннего интерфейса IUnknown, используемого только для использования, когда объект агрегируется клиентом.

  2. Чтобы сохранить его достаточно простым, в настоящее время я изменяю хуки реализации QueryInterface, чтобы «лишние» интерфейсы IMarshal ... IConnectionContainer игнорировались и не были доступны из COM. Если они необходимы, можно либо реализовать все необходимые интерфейсы в управляемом коде - довольно большую работу - либо найти способ «зацепить» эти интерфейсы, что также можно сделать, но с меньшим количеством дополнительного кода и исследований. .

  3. Код, вероятно, не очень быстрый из-за всей несоответствия между неуправляемым и управляемым кодом и, вероятно, не подходит для управляемого COM-сервера, где вызовы методов COM-сервера быстрые и частые и "детальные" «Это означает, что каждый звонок не так уж много значит. Если требуемый управляемый COM-сервер может выполнять большую часть работы с небольшим количеством вызовов методов, то все должно быть в порядке, и затраты производительности при внедрении его в управляемый код, а не в неуправляемый C ++, должны быть минимальными.

Этот проект больше похож на хакерский проект "белого рыцаря", работающий вокруг текущей реализации!

В порядке, обратном Вопросу, для начала реализация неуправляемого COM-клиента изменяется только так, что не требует вызова метода IDisposable.Dispose () (так же, как для ответа оболочки), следующим образом:

    #import "..\\Bar\\Bar.tlb" //raw_interfaces_only

    ...
    CoInitialize(NULL);
    ...
        //the syntax for a "Smart Pointer" is as follows:
        Bar::IFooPtr pif(__uuidof(Bar::Foo)); // This object comes from the .tlb.
        if (pif)
        {
            //This is not stack based so the calling syntax for an object instance is as follows:
            pif->Doit();
            //THE FOLLOWING IS NOT REQUIRED AND IS COMMENTED OUT!!!
            //IDisposable *id = nullptr;
            //if (SUCCEEDED(pif->QueryInterface(__uuidof(IDisposable), (LPVOID*)&id)) && id)
            //{
            //  id->Dispose();
            //  id->Release();
            //}
            //The Release on the IUnknown is absolutely necessary, as without it the reference count stays as one!
            //This would result in a memory leak, as the Bar::Foo's destructor is never called,
            //and knows nothing about the IUnknown::Release() even if it were!!!
            pif->Release();
        }
    ...
    CoUninitialize();
    ...

Далее, единственное изменение в управляемом COM-сервере - заставить класс COM-сервера наследовать (прямо или косвенно) от нового базового класса AutoComDisposenC. Я также добавил добавленную функциональность «защиты» кода деструктора, чтобы сделать его эффективным только при первом вызове для тех случаев, когда это имеет значение, например, в файлах, которые закрываются только один раз, и так далее:

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

namespace Bar {

        [Guid("57ED5388-blahblah")]
        [InterfaceType(ComInterfaceType::InterfaceIsIUnknown)]
        [ComVisible(true)]
        public interface class IFoo
        {
            void Doit();
        };

        [Guid("417E5293-blahblah")]
        [ClassInterface(ClassInterfaceType::None)]
        [ComVisible(true)]
        public ref class Foo : AutoComDisposenC, IFoo
        {
        //these don't need to be seen...
        private:
            bool disposed;
            Object ^disposedlock;
            void DisposeManaged() {};
            void DisposeUnmanaged() {};
            ~Foo() // called directly or by Dispose() on this instance
            {
                //multi-threading safe, only effective first time it's called...
                bool d;
                Monitor::Enter(this->disposedlock);
                    //in critical section = only one thread at a time
                    d = this->disposed;
                    this->disposed = false;
                Monitor::Exit(this->disposedlock);
                if (!d)
                {
                    DisposeManaged();
                    this->!Foo();
                }
            }
            !Foo() {DisposeUnmanaged();} // This is called by the garbage collector and the above.
        public:
            Foo() : disposed(FALSE), disposedlock(gcnew Object()) {};
            //THE FOLLOWING IS WRONG AS ONE CANNOT OVERRIDE THE HIDDEN IUNKNOWN RELEASE() METHOD!!!
//      virtual ULONG Release() {MyDispose(); return 0;} // This is never called automatically!!!
            [PreserveSig]
            [ComVisible(true)]
            virtual void Doit() {};
        };
}

И, наконец, будет действительная реализация класса AutoComDisposenC:

Когда я закончу, я действительно должен отправить этот альтернативный ответ в виде статьи на какой-нибудь веб-сайт, такой как «Проект кода», так как я думаю, что он будет полезен для многих.

1 голос
/ 08 января 2011

Гордон,

Я очень заинтересован в ответе на это. вам повезло с вашей реализацией

Чарли

Чарли, я так понимаю, что вас больше всего интересуют мои Ответы и 2. или 3. ниже. Мой ответ 1. работает так же, как я описываю, как и 2. Однако существуют серьезные проблемы не с реализацией 3. Но с сохранением его работоспособности, поскольку исправление не исправляется каким-либо событием при проверке доступности для сборки мусора, даже когда COM-объект не подходит для сбора. Способы следующие, но с тех пор я нашел способ использовать версию 4 DotNetFramework для решения основных проблем с 3.:

  1. Краткий ответ, показывающий, как вызвать IDisposable.Dispose () из собственного неуправляемого клиента, написанного на C ++, который отвечает на прямой вопрос. Конечно, это работает, и это было только неопытностью автора оригинального вопроса в письменной форме для управляемого COM, который сделал это трудным для него. Однако второстепенный вопрос о том, можно ли это сделать автоматически, - это то, что я действительно отвечаю здесь.

  2. Обходной путь, использующий оболочку C ++ для COM Callable Wrapper (CCW), реализованную в среде выполнения Microsoft COM в среде mscoree.dll. Это работает, как опубликовано, и вы можете использовать его, с указанием основных ограничений: вам нужно установить файл DLL в системный каталог (или оба, если в 64-битной системе), и вам нужно переписать записи реестра для InProcServer32 для связи с этой DLL-оболочкой (опять же для 32- и 64-битных версий реестра). Файл DLL также должен иметь цифровую подпись, чтобы быть приемлемым для некоторых встроенных клиентов Microsoft COM, таких как Windows Imaging Component (WIC), чтобы он был полностью функциональным.

  3. Альтернативный обходной путь, выполняемый исключительно со стороны управляемого кода, который работает путем «исправления» / «перехвата» / «перебора» таблиц виртуальных методов интерфейсов CCW, чтобы получить контроль и добавить желаемую функциональность автоматического вызова IDisposable :: Dispose () для производных классов, когда количество псевдо-ссылок, предоставленных CCW, уменьшается до нуля. Похоже, что это будет работать, так как у меня это работает для случая неагрегированного экземпляра, но я пытаюсь поддержать агрегацию, прежде чем отправить его здесь. Надеюсь, у меня все получится через несколько дней. Я пишу на C #, но, конечно, он также может быть написан на управляемом / CLI C ++ и в конечном итоге будет предоставлять ссылки на обе реализации. Это гораздо сложнее в написании, чем обертка, но имеет то преимущество, что его очень легко использовать, так как для его использования нужно сделать очень мало изменений в классе COM-сервера, как я покажу, и никакой дополнительной работы для этого нет. установка вообще. Это кажется ненадежным, поскольку исправление, которое мне нужно сделать, отменяется каким-либо событием в циклах сбора мусора, так что я теряю контроль над отслеживанием количества ссылок.

Таким образом, преимущества просты в использовании. Тем не менее, есть следующие недостатки:

  1. Код исправляет таблицы виртуальных методов путем прямой записи в них в случае имитированных управляемых интерфейсов, что было возможно в течение последних многих лет, но нет никаких гарантий, что Microsoft не может изменить это в будущем с новой моделью памяти для новой версии операционной системы. Я попытался внести изменения в копии таблиц виртуальных методов в неуправляемой памяти, но не могу легко заставить работать некоторые ключевые функции - вызов метода Marshal.GetObjectForIunknown () больше не работает, как ожидалось, что требует некоторых очень грязных статических таблиц поиска ... Я не использую это в настоящее время и не исправляю напрямую для управляемых vtables. Если Microsoft изменит ограничения на запись, я, скорее всего, получу «грязный» метод. Не выглядит возможным, если не использовать новые функциональные возможности, предоставляемые DotNetFramework версии 4.0.

  2. Чтобы сохранить его достаточно простым, в настоящее время я модифицирую хуки реализации QueryInterface, чтобы «лишние» интерфейсы IMarshal ... IConnectionContainer игнорировались и не были доступны из COM. Если они необходимы, можно либо реализовать все необходимые интерфейсы в управляемом коде - довольно большую работу - либо найти способ «зацепить» эти интерфейсы, что, вероятно, возможно, поскольку я уже «зацепил» внутренний неуправляемый интерфейс IUnknown как было необходимо для поддержки агрегации. Тем не менее, довольно большая задача программирования.

  3. Код, вероятно, не очень быстрый из-за всей несоответствия между неуправляемым и управляемым кодом и, вероятно, не подходит для управляемого COM-сервера, где вызовы методов COM-сервера быстрые и частые и "детальные" «Это означает, что каждый звонок не так уж много значит. Если требуемый управляемый COM-сервер может выполнять большую часть работы с небольшим количеством вызовов методов, то все должно быть в порядке, и затраты на производительность при его внедрении в управляемый код, а не в неуправляемый C ++, должны быть минимальными.

Этот проект больше похож на хакерский проект "белого рыцаря", в котором используется текущая реализация!

Когда я закончу, я действительно должен отправить его в виде статьи на какой-нибудь веб-сайт, такой как «Проект кода», так как я думаю, что он будет полезен для многих.

В любом случае, посмотрите это место для еще одного ответа, используя средства из DotNetFramework 4.0 ...

1 голос
/ 27 декабря 2010

Код для объявления объекта (C ++ / CLI), исправленный для VS 2010 (GBG):

using namespace System;
using namespace System::Runtime::InteropServices;

namespace Bar {

        [Guid("57ED5388-blahblah")]
        [InterfaceType(ComInterfaceType::InterfaceIsIDispatch)]
        [ComVisible(true)]
        public interface class IFoo
        {
                void Doit();
        };

        [Guid("417E5293-blahblah")]
        [ClassInterface(ClassInterfaceType::None)]
        [ComVisible(true)]
        public ref class Foo : IFoo
        {
        //these don't need to be seen...
        private:
            void DisposeManaged() {};
            void DisposeUnmanaged() {};
            ~Foo() {DisposeManaged(); this->!Foo();} // Only called by Dispose() on object instance or direct call and delete in C++/CLI
            !Foo() {DisposeUnmanaged();} // Called by the garbage collector and by the above.
        public:
        //THE FOLLOWING IS WRONG, ONE CANNOT OVERRIDE THE HIDDEN IUNKNOWN RELEASE() METHOD IN THIS WAY!!!
//      virtual ULONG Release() {MyDispose(); return 0;} // This is never called automatically!!!
            [PreserveSig];
            virtual void Doit() {};
        };
}

Код исправляется следующим образом:

  1. Метод Release не переопределяет скрытый метод IUnknown :: Release (), о котором ничего не знает компилятор CLI, если он был исправлен для фактического возврата значения ULONG,

  2. Рекомендуется, чтобы деструктор ~ Foo () просто вызвал финализатор! Foo (), чтобы избежать повторения того, что ему нужно для освобождения неуправляемых ресурсов,

  3. Деструктор ~ Foo () должен располагать как управляемыми, так и неуправляемыми ресурсами, но финализатор! Foo () должен располагать только неуправляемыми ресурсами, как реализовано здесь,

  4. Нет необходимости открывать какие-либо методы, кроме реализованных методов интерфейса, и

  5. Все методы интерфейса должны быть выполнены [PreserveSig] для максимальной совместимости с COM.

Код для использования объекта (нативный C ++), исправленный для VS 2010 (GBG), с исправлениями, прокомментированными следующим образом ( Обратите внимание, что здесь содержится ответ на вопрос, когда пишется COM-клиент! ):

    #import "..\\Bar\\Bar.tlb" //raw_interfaces_only

    //C++ definition of the managed IDisposable interface...
    MIDL_INTERFACE("805D7A98-D4AF-3F0F-967F-E5CF45312D2C")
        IDisposable : public IDispatch
    {
    public:
        virtual VOID STDMETHODCALLTYPE Dispose() = 0;
    }

    ...
    CoInitialize(NULL);
    ...
        //the syntax for a "Smart Pointer" is as follows:
        Bar::IFooPtr pif(__uuidof(Bar::Foo)); // This object comes from the .tlb.
        if (pif)
        {
            //This is not stack based so the calling syntax for an object instance is as follows:
            pif->Doit();
            //THE FOLLOWING ANSWERS THE QUESTION: HOW TO DISPOSE ON RELEASE:  when one controls the COM client!!!
            IDisposable *id = nullptr;
            if (SUCCEEDED(pif->QueryInterface(__uuidof(IDisposable), (LPVOID*)&id)) && id)
            {
                id->Dispose();
                id->Release();
            }
            //The Release on the IUnknown is absolutely necessary, as without it the reference count stays as one!
            //This would result in a memory leak, as the Bar::Foo's destructor is never called,
            //and knows nothing about the IUnknown::Release() even if it were!!!
            pif->Release(); // explicit release, not really necessary since Bar::IFoo's destructor will call Release().
        }
    ...
    CoUninitialize();
    ...

Похоже, что открывающий вопрос не совсем понимает взаимосвязь между методом подсчета ссылок Release COM-серверов и абсолютным отсутствием подсчета ссылок в управляемом коде, отличном от моделируемого CCW:

Если я добавлю метод-деструктор в мой COM-объект, он никогда не будет вызван. Если я добавляю метод финализатора, он вызывается, когда сборщик мусора обходит его. Если я явно вызываю переопределение Release (), оно никогда не вызывается.

Деструктор ~ Foo () и поведение финализатора! Foo () были объяснены выше; так называемое переопределение Release () никогда не вызывается, потому что оно не переопределено ни к чему, особенно к скрытому интерфейсу IUnknown, предоставленному CCW. Однако эти ошибки кодирования не означают, что дополнительный вопрос не имеет значения, и есть несколько обходных путей, чтобы сделать это возможным, о чем я расскажу в других ответах.

Вы можете сделать это с помощью IDisposable и Finalize. weblogs.asp.net/cnagel/archive/2005/04/27/404809.aspx

Этот ответ не дает прямого ответа на вопрос, так как IDisposable и Finalize уже реализованы в C ++ / CLI ~ Foo () и! Foo (); спрашивающий просто не понимает, как вызвать метод Dispose (), который я показал выше.

0 голосов
/ 15 декабря 2017

Если не считать создания оболочки в C ++ (без .NET), я не знаю способа. Проблема состоит в том, что когда при вызове Release сбрасывается счетчик ссылок COM до 0, .NET Framework не знает, есть ли еще управляемые ссылки, которые не включены в счетчик ссылок COM.

Основное ограничение заключается в том, что в .NET отсутствует концепция объекта, доступного только из COM. А поскольку ссылки из объектов .NET не могут быть определены до сборки мусора, нет никакого детерминированного способа удаления при выпуске, как в чистом COM.

...