Почему внешняя COM-компонента на C# не выгружается из памяти 1С 7.7 и как это исправить?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

Разработка внешних компонент для 1С:Предприятия 7.7 на платформе .NET (C#) сопряжена с рядом тонкостей, связанных с управлением памятью. Часто возникает ситуация, когда компонента успешно подключается и работает, но после закрытия программы процесс 1cv7.exe продолжает висеть в памяти или сама библиотека не выгружается. Рассмотрим, почему это происходит и как правильно управлять временем жизни COM-объектов в управляемом коде.

Суть проблемы: Невидимые связи COM и .NET

Когда 1С передает в компоненту указатель на IDispatch (например, через метод Init интерфейса IInitDone), среда .NET создает вокруг этого указателя специальную оболочку, называемую RCW (Runtime Callable Wrapper). Эта оболочка ведет себя как обычный объект .NET, но внутри себя хранит счетчик ссылок на реальный COM-объект 1С.

Проблема заключается в том, что любое обращение к объектам 1С из C#, будь то получение свойств через InvokeMember или работа с AppDispatch, создает дополнительные ссылки. Если счетчик ссылок хотя бы на одну единицу больше нуля, COM-объект считается используемым, и 1С не может корректно завершить работу компоненты и выгрузить её из адресного пространства.

Анализируем механизм финализации

Обычно разработчики полагаются на стандартный метод Done(), где обнуляют ссылки. Проанализируем типичный (но не всегда работающий) код:


public void Done()
{
    V7Data.V7Object = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Выясним причину, по которой этого недостаточно. Метод GC.Collect() лишь помечает объекты для удаления. Если внутри кода компоненты были созданы временные переменные, указывающие на подобъекты 1С (например, на справочники или документы), и они не были явно освобождены, RCW продолжит удерживать COM-сервер. Особенно это критично, если вы используете статические поля (static) для хранения ссылок на объекты 1С — такие ссылки живут до завершения работы всего домена приложения.

Решение проблемы через принудительное освобождение ссылок

Как показывает практика, стандартного обнуления переменной (= null) недостаточно, так как среда CLR сама решает, когда уменьшить счетчик ссылок в COM. Чтобы гарантированно выгрузить компоненту, мы должны вручную управлять счетчиком ссылок через класс Marshal. Рассмотрим рабочий алгоритм, найденный опытным путем.

Разберем по шагам логику метода Done(), который принудительно «добивает» ссылки:

  1. Получаем указатель IntPtr на COM-интерфейс текущего объекта.
  2. В цикле вызываем Marshal.Release до тех пор, пока возвращаемое значение (количество оставшихся ссылок) не станет равным нулю.
  3. Обнуляем переменную в управляемом коде.
  4. Запускаем сборку мусора.

Пример реализации правильного освобождения ресурсов:


public void Done()
{
    if (_obj1c != null)
    {
        // Получаем указатель на IDispatch сохраненного объекта
        IntPtr obj1cPtr = Marshal.GetIDispatchForObject(_obj1c);
        // Принудительно уменьшаем счетчик ссылок до нуля
        int rc = Marshal.Release(obj1cPtr);
        while (rc > 0)
        {
            rc = Marshal.Release(obj1cPtr);
        }
        _obj1c = null;
    }

    if (V7Data.V7Object != null)
    {
        IntPtr connectionPtr = Marshal.GetIDispatchForObject(V7Data.V7Object);
        int rc = Marshal.Release(connectionPtr);
        while (rc > 0)
        {
            rc = Marshal.Release(connectionPtr);
        }
        V7Data.V7Object = null;
    }

    // Финальная очистка памяти
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Использование Marshal.FinalReleaseComObject

Существует более изящный способ добиться того же результата без использования циклов while. В .NET предусмотрен метод Marshal.FinalReleaseComObject, который сразу устанавливает счетчик ссылок RCW в ноль. Проанализируем ситуацию: если ваша компонента активно создает объекты агрегатных типов 1С (запросы, таблицы значений), рекомендуется сохранять ссылки на них в список (например, List<object>) и в методе Done() обходить этот список.

Рассмотрим пример с использованием этого метода:


public void ReleaseResources()
{
    try 
    {
        if (V7Data.V7Object != null)
        {
            // Метод возвращает 0, если объект успешно освобожден
            Marshal.FinalReleaseComObject(V7Data.V7Object);
            V7Data.V7Object = null;
        }
    }
    catch (Exception ex)
    {
        // Обработка возможных исключений при высвобождении
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        // Повторный вызов GC часто помогает очистить объекты, 
        // которые прошли стадию финализации
        GC.Collect();
    }
}

Важные рекомендации по архитектуре компоненты

Чтобы избежать проблем с выгрузкой в будущем, придерживайтесь следующих правил при написании кода на C# для 1С 7.7:

  1. Избегайте статических ссылок: Старайтесь не хранить pConnection (объект 1С) в статических полях классов. Если ссылка статическая, она будет удерживать 1С в памяти до тех пор, пока жив AppDomain. Лучше передавать ссылку как параметр или хранить в поле экземпляра основного класса компоненты.
  2. Явное освобождение при InvokeMember: Если вы получаете объект через рефлексию (например, AppDispatch), сразу после использования вызывайте для него Marshal.ReleaseComObject. Помните, что каждое обращение вида obj.GetType().InvokeMember(...) может порождать новый временный RCW.
  3. Метод Done и HRESULT: Обратите внимание, что в 1С 7.7 методы интерфейса IInitDone должны возвращать статус. В C# метод void по умолчанию возвращает S_OK (0). Если вы измените сигнатуру на int и будете возвращать значения вручную, следите за тем, чтобы не возникало исключений внутри метода, иначе 1С может аварийно завершиться.

Посмотрим на пример правильного получения AppDispatch с последующей очисткой:


object appDispatch = V7Data.V7Object.GetType().InvokeMember(
    "AppDispatch", 
    System.Reflection.BindingFlags.GetProperty, 
    null, 
    V7Data.V7Object, 
    null);

// ... выполняем действия с appDispatch ...

// Освобождаем сразу, как только объект перестал быть нужен
if (appDispatch != null && Marshal.IsComObject(appDispatch))
{
    Marshal.ReleaseComObject(appDispatch);
}

Соблюдение этих правил гарантирует, что ваша внешняя компонента будет вести себя корректно, не оставляя «фантомных» процессов в диспетчере задач и позволяя пользователям беспрепятственно закрывать 1С.

← На главную