Разработка внешних компонент для 1С:Предприятия 7.7 на платформе .NET (C#) сопряжена с рядом тонкостей, связанных с управлением памятью. Часто возникает ситуация, когда компонента успешно подключается и работает, но после закрытия программы процесс 1cv7.exe продолжает висеть в памяти или сама библиотека не выгружается. Рассмотрим, почему это происходит и как правильно управлять временем жизни COM-объектов в управляемом коде.
Когда 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(), который принудительно «добивает» ссылки:
IntPtr на COM-интерфейс текущего объекта.Marshal.Release до тех пор, пока возвращаемое значение (количество оставшихся ссылок) не станет равным нулю.Пример реализации правильного освобождения ресурсов:
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();
}
Существует более изящный способ добиться того же результата без использования циклов 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:
pConnection (объект 1С) в статических полях классов. Если ссылка статическая, она будет удерживать 1С в памяти до тех пор, пока жив AppDomain. Лучше передавать ссылку как параметр или хранить в поле экземпляра основного класса компоненты.AppDispatch), сразу после использования вызывайте для него Marshal.ReleaseComObject. Помните, что каждое обращение вида obj.GetType().InvokeMember(...) может порождать новый временный RCW.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С.