Почему внешняя компонента 1С зависает на Windows 7 и платформах 8.3.18+?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке и использовании внешних компонент для 1С:Предприятия мы можем столкнуться с неожиданными трудностями, особенно когда компонента демонстрирует нестабильное поведение, зависая на определенных версиях платформы или операционных систем. Рассмотрим проблему, когда внешняя компонента, созданная на основе шаблона C++, периодически зависает на рабочих местах с Windows 7 (x64) и платформами 1С версий 8.3.18 или 8.3.19, при этом стабильно работая на 8.3.17 и Windows 10. Зависание происходит на этапе подключения компоненты или при вызове её методов.

Анализируем симптомы и диагностические данные

Прежде всего, давайте внимательно изучим предоставленные симптомы и диагностическую информацию, которая помогает нам сузить круг возможных причин. Мы наблюдаем следующие ключевые моменты:

  1. Зависимость от операционной системы: Проблема проявляется исключительно на Windows 7 x64. На Windows 10 зависаний не наблюдается. Это указывает на возможные проблемы совместимости с более старыми версиями ОС, отсутствие необходимых системных обновлений или различия в поведении базовых системных библиотек, таких как UCRT.
  2. Зависимость от версии платформы 1С: Компонента стабильно работает на платформе 8.3.17, но систематически зависает на 8.3.18 и 8.3.19. Это критически важное наблюдение, поскольку оно прямо указывает на изменения в механизме работы платформы 1С, которые были внесены в версии 8.3.18 и повлияли на взаимодействие с внешними компонентами.
  3. Отсутствие зависимости от режима работы: Зависание происходит как в файловом, так и в клиент-серверном режиме, что позволяет исключить проблемы, специфичные для одного из этих режимов.
  4. Момент зависания: Зависание происходит при подключении внешней компоненты к 1С или при вызове её методов. Изначально могло казаться, что проблема в создании объекта, но уточняется, что именно в момент подключения.

Предположим, мы получили дамп зависания от пользователя. Анализ дампа позволяет нам увидеть состояние ожидающих потоков в момент блокировки. Рассмотрим фрагмент такого анализа:


0:000> !locks
CritSec ntdll!LdrpLoaderLock+0 at 00000000774b80d8
WaiterWoken No
LockCount 1
RecursionCount 1
OwningThread 6520
EntryCount 0
ContentionCount f
*** Locked
CritSec +57e6488 at 00000000057e6488
WaiterWoken No
LockCount 0
RecursionCount 1
OwningThread 5744
EntryCount 0
ContentionCount 0
*** Locked
*** WARNING: Unable to verify checksum for v8_BE9A_8.dll
CritSec v8_BE9A_8!GetClassNames+1c3b0 at 000007fecce5d4e0
WaiterWoken No
LockCount 1
RecursionCount 1
OwningThread 5744
EntryCount 0
ContentionCount 1
*** Locked
Scanned 16879 critical sections

Из анализа дампа мы видим, что заблокировано несколько критических секций, в том числе одна, связанная с модулем v8_BE9A_8.dll, который, по всей видимости, является частью платформы 1С или её рантайма. Это указывает на проблемы с синхронизацией в многопоточной среде. Мы уделим особое внимание стекам вызовов ожидающих потоков.

Поток 6520 ожидает в ntdll!ZwWaitForSingleObject, пытаясь войти в критическую секцию ntdll!RtlEnterCriticalSection. Его стек вызовов демонстрирует последовательность функций, характерную для инициализации C Runtime Library (CRT) при создании нового потока или загрузке динамической библиотеки (DLL): dllmain_crt_dispatch, __acrt_thread_attach, construct_ptd, __acrt_lock_and_call. Это указывает на попытку платформы или компоненты подготовить среду для работы потока, используя базовые функции рантайма C++.


0:000> ~~[6520] k
 # Child-SP             RetAddr          Call Site
00 00000000`6003f6a8 00000000`773c2b08 ntdll!ZwWaitForSingleObject+0xa
01 00000000`6003f6b0 00000000`773bd261 ntdll!RtlpWaitOnCriticalSection+0xe8
02 00000000`6003f760 000007fe`cce4afb8 ntdll!RtlEnterCriticalSection+0xd1
03 (Inline Function) --------`-------- v8_BE9A_8!__acrt_lock_and_call::__l3::<lambda_5e887d1dcbef67a5eb4283622ba103bf>::operator()+0x7
04 00000000`6003f790 000007fe`cce4b12b v8_BE9A_8!__crt_seh_guarded_call<void>::operator()<<lambda_5e887d1dcbef67a5eb4283622ba103bf>,<lambda_4466841279450cc726390878d4a41900> & __ptr64,<lambda_341c25c0346d94847f1f3c463c57e077> >+0x1c
05 (Inline Function) --------`-------- v8_BE9A_8!__acrt_lock_and_call+0x8a
06 (Inline Function) --------`-------- v8_BE9A_8!construct_ptd+0xba
07 00000000`6003f7c0 000007fe`cce4b3f7 v8_BE9A_8!construct_ptd_array+0xc7
08 (Inline Function) --------`-------- v8_BE9A_8!get_ptd_head+0x56
09 (Inline Function) --------`-------- v8_BE9A_8!internal_getptd_noexit+0x5c
0a 00000000`6003f820 000007fe`cce46381 v8_BE9A_8!__acrt_getptd_noexit+0x6b
0b 00000000`6003f850 000007fe`cce4246e v8_BE9A_8!__acrt_thread_attach+0x9
0c 00000000`6003f880 000007fe`cce41c29 v8_BE9A_8!__scrt_dllmain_crt_thread_attach+0x16
0d 00000000`6003f8b0 000007fe`cce41e59 v8_BE9A_8!dllmain_crt_dispatch+0x2d
0e 00000000`6003f8e0 00000000`773c83cc v8_BE9A_8!dllmain_dispatch+0x5d
0f 00000000`6003f940 00000000`773d36bf ntdll!LdrpInitializeThread+0x17c
10 00000000`6003fa40 00000000`773d373e ntdll!LdrpInitialize+0x9f
11 00000000`6003fab0 00000000`00000000 ntdll!LdrInitializeThunk+0xe

Поток 5744 также заблокирован в ntdll!ZwWaitForSingleObject, но его стек вызовов существенно отличается. Он связан с загрузкой DLL (ntdll!LdrpLoadDll, KERNELBASE!LoadLibraryExW) и функциями, работающими с локалью (_wsetlocale_nolock, _expandlocale, __acrt_can_use_vista_locale_apis), а также с получением адресов функций из DLL (try_get_function). В частности, упоминаются функции CompareStringEx, что указывает на попытки платформы определить возможности локали и сравнения строк.


0:000> ~~[5744] k
 # Child-SP             RetAddr          Call Site
00 00000000`0012a9c8 00000000`773c2b08 ntdll!ZwWaitForSingleObject+0xa
01 00000000`0012a9d0 00000000`773bd261 ntdll!RtlpWaitOnCriticalSection+0xe8
02 00000000`0012aa80 00000000`7748e207 ntdll!LdrpLoadDll+0x2c7
03 00000000`0012aab0 00000000`7747ad7d ntdll!LdrLoadDll+0xed
04 000007fe`fd02b39a KERNELBASE!LoadLibraryExW+0x2a3
05 000007fe`cce4b6c2 v8_BE9A_8!try_load_library_from_system_directory+0x11
06 (Inline Function) --------`-------- v8_BE9A_8!try_get_module+0x30
07 (Inline Function) --------`-------- v8_BE9A_8!try_get_first_available_module+0x39
08 (Inline Function) --------`-------- v8_BE9A_8!try_get_proc_address_from_first_available_module+0x39
09 (Inline Function) --------`-------- v8_BE9A_8!try_get_function+0xa2
0a 00000000`0012adf0 000007fe`cce44755 v8_BE9A_8!__acrt_can_use_vista_locale_apis+0x23
0b 00000000`0012ae20 000007fe`cce44ecc v8_BE9A_8!_expandlocale+0x16d
0c 00000000`0012b0b0 000007fe`cce44076 v8_BE9A_8!_wsetlocale_nolock+0x1fc
0d (Inline Function) --------`-------- v8_BE9A_8!_wsetlocale::__l22::<lambda_2af78c5f5901b1372d98f9ab3177dfa6>::()::__l39::<lambda_30712929f77e709619002f448b6a9510>::operator()+0x31
0e 00000000`0012b2f0 000007fe`cce441e7 v8_BE9A_8!__crt_seh_guarded_call<void>::operator()<<lambda_d67e8342c384adda8f857579ab50b2ae>,<lambda_30712929f77e709619002f448b6a9510> & __ptr64,<lambda_4525336fd7e478d965fb7ca7a337cad8> >+0x4e
0f (Inline Function) --------`-------- v8_BE9A_8!__acrt_lock_and_call+0x4f
10 00000000`0012b320 000007fe`cce44011 v8_BE9A_8!<lambda_2af78c5f5901b1372d98f9ab3177dfa6>::operator()+0x83
11 00000000`0012b380 000007fe`cce44ade v8_BE9A_8!__crt_seh_guarded_call<void>::operator()<<lambda_5df02c53a8f32f81fd64e5bbb78039f1>,<lambda_2af78c5f5901b1372d98f9ab3177dfa6> & __ptr64,<lambda_f51fe5fd7c79a33db34fc9310f277369> & __ptr64>+0x15
12 (Inline Function) --------`-------- v8_BE9A_8!__crt_call_and_cleanup+0x4c
13 00000000`0012b3b0 00000001`0e2c9bb1 v8_BE9A_8!_wsetlocale+0x92

Мы видим классическую картину взаимной блокировки (deadlock). Два или более потока ждут друг друга, пытаясь получить доступ к ресурсам, занятым другими потоками. В нашем случае, это происходит в критических секциях, связанных с:

  1. Загрузкой DLL (LdrpLoadDll).
  2. Инициализацией C Runtime Library (CRT) и данных потоков (__acrt_thread_attach, construct_ptd).
  3. Работой с локалью (_wsetlocale, CompareStringEx).

То, что эта проблема проявляется на платформах 8.3.18+ и Windows 7, является ключевым моментом для нашего расследования.

Исследуем изменения платформы 1С 8.3.18+ и их потенциальное влияние

Платформа 1С:Предприятие 8.3.18 и последующие версии внесли значительные изменения, особенно касающиеся асинхронных функций и методов. Был добавлен новый синтаксис (модификатор процедур и функций Асинх, оператор Ждать, тип Обещание), а также реализованы новые асинхронные вызовы методов и свойств внешних компонент. Эти изменения не могли не затронуть внутренние механизмы загрузки и работы с библиотеками, особенно в многопоточной среде. Отмечается даже выросшее потребление стека, что может быть признаком более сложной логики инициализации или новых требований к рантайму.

Мы предполагаем, что изменения в платформе могли сделать процесс загрузки и инициализации внешней компоненты более чувствительным к корректности работы Universal C Runtime (UCRT) и его инициализации. Если платформа 1С теперь более активно использует многопоточность или специфические возможности UCRT, это может выявить скрытые проблемы взаимодействия, особенно на системах с нестандартными или устаревшими пакетами UCRT.

Подробный разбор проблем инициализации Universal C Runtime (UCRT) и многопоточности

Как мы уже отметили, стеки вызовов из дампа отчетливо указывают на функции Universal C Runtime (UCRT): __acrt_lock_and_call, construct_ptd, __acrt_thread_attach, _wsetlocale_nolock. Эти функции отвечают за инициализацию рантайма C++, управление данными потоков (per-thread data) и работу с локалью. Зависание происходит из-за взаимной блокировки, когда:

  1. Один поток (например, основной поток платформы 1С) пытается инициализировать CRT или установить локаль (через _wsetlocale) в процессе загрузки компоненты или при подготовке к работе, захватывая при этом одну из внутренних критических секций UCRT или загрузчика DLL.
  2. Второй поток, возможно, созданный платформой 1С (например, для внутренних асинхронных операций), или даже самой внешней компонентой (например, в её DllMain или Init), также пытается инициализировать CRT или получить свои данные потока. Для этого ему нужна та же самая критическая секция или другая, которая уже занята первым потоком.
  3. В результате оба потока ждут друг друга, что приводит к зависанию.

Этот сценарий усугубляется на Windows 7 по нескольким причинам:

Критически важное решение: Инициализация COM для ShellExecuteEx

Внимательно изучив исходный код внешней компоненты (AddInNative.cpp), мы обнаруживаем, что она содержит метод Shell, который использует функцию ShellExecuteEx. Это ключевой момент в нашем расследовании. Документация Microsoft ясно указывает, что ShellExecuteEx может делегировать выполнение расширениям оболочки, которые активируются с использованием Component Object Model (COM). Следовательно, COM должен быть инициализирован до вызова ShellExecuteEx.

Если COM не инициализирован должным образом в потоке, который вызывает ShellExecuteEx, это может привести к зависаниям или некорректной работе. Для инициализации COM в однопоточном апартаменте (Single-Threaded Apartment, STA) мы используем следующую функцию:

CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

Мы должны убедиться, что эта инициализация происходит в начале работы метода, который использует ShellExecuteEx, или в методе Init компоненты, если ShellExecuteEx используется в других методах, вызываемых из 1С. После завершения работы с COM мы также должны вызвать CoUninitialize(), чтобы корректно освободить ресурсы COM. Например:


// В методе Init компоненты
STDMETHODIMP CAddInNative::Init(IAddInDefBase* pConnection)
{
    // ...
    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    // ...
    return S_OK;
}

// В методе Done компоненты
STDMETHODIMP CAddInNative::Done()
{
    // ...
    CoUninitialize();
    // ...
    return S_OK;
}

// Или прямо перед вызовом ShellExecuteEx, если COM нужен только для этого
STDMETHODIMP CAddInNative::Shell(tVariant* pvarRetValue, tVariant* paParams)
{
    // ...
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    if (SUCCEEDED(hr))
    {
        // Вызов ShellExecuteEx
        // ...
        CoUninitialize();
    }
    // ...
    return S_OK;
}

Мы настоятельно рекомендуем проверить и при необходимости добавить корректную инициализацию и деинициализацию COM в вашу внешнюю компоненту. Это может быть основной причиной зависаний, если ShellExecuteEx вызывается в контексте, где COM ещё не был инициализирован, особенно в новом потоке, который запускает платформа 1С или компонент.

Управление памятью во внешних компонентах: рекомендации 1С и лучшие практики

Техническая поддержка 1С справедливо указывает на то, что "нельзя выделять память для возврата значений с помощью new или malloc, т.к. это приведет к утечке памяти и к нестабильности работы программы" (согласно ITS.1C). Мы должны разобраться в этом вопросе, так как некорректная работа с памятью может привести к трудноуловимым ошибкам и зависаниям.

Принципы выделения памяти

Мы выделяем память следующим образом, строго придерживаясь правил:

  1. Для внутренних нужд компоненты: Всё, что мы создаем и удаляем сами (например, объекты, используемые только внутри компоненты, или временные буферы), мы можем выделять с помощью new или malloc. Мы несем полную ответственность за своевременное и корректное освобождение этой памяти.
  2. Для возвращаемых значений 1С (tVariant): Вся память, которая будет выделена для значений, передаваемых в 1С (таких как строки, двоичные данные, массивы и т.д.), должна быть выделена менеджером памяти 1С. Это необходимо, потому что 1С будет отвечать за освобождение этих значений. Если мы выделим такую память через new или malloc, платформа 1С попытается освободить её своим менеджером, что почти гарантированно приведет к ошибкам, утечкам или аварийному завершению программы.

В контексте шаблона внешней компоненты, функция GetClassObject() часто использует оператор new для создания экземпляров классов компоненты. Это абсолютно правильная и стандартная практика. Объект, возвращаемый GetClassObject(), управляется платформой 1С, и она корректно его освободит. Проблема, на которую указывает 1С, относится к выделению памяти для содержимого типа tVariant (например, строки, которые мы возвращаем из методов компоненты), а не для самого объекта компоненты.

Для примера, если мы хотим вернуть строку в 1С, мы должны использовать метод IAddInDefBase::AllocMemory для выделения памяти, а не стандартный new:


// Пример неправильного выделения для возвращаемой строки
// WCHAR_T* pResult = new WCHAR_T[length]; // Это может привести к утечкам или краху!

// Правильное выделение для возвращаемой строки с использованием менеджера 1С
WCHAR_T* pResult = static_cast<WCHAR_T*>(m_iConnect->AllocMemory(length * sizeof(WCHAR_T)));
// Далее копируем данные в pResult

Предпринятые попытки исправления, связанные с памятью и инициализацией

Мы уже предприняли попытки исправить потенциальные проблемы, связанные с памятью и инициализацией:

  1. Добавлена опция компиляции /Zc:threadSafeInit-. Эта опция отключает безопасную для потоков инициализацию статических переменных C++11. Это может быть полезно, если есть подозрения на проблемы с инициализацией статических объектов в многопоточной среде, которые могут приводить к блокировкам.
  2. Заменено формирование возвращаемой функцией GetClassNames() строки с динамически выделяемой convToShortWchar() на статически выделяемую static WcharWrapper s_names(g_kClassNames). Этот шаг также направлен на исключение проблем с динамическим выделением памяти в потенциально чувствительных местах инициализации компоненты.

Однако, как мы узнали из сообщения пользователя, эти изменения, к сожалению, не решили проблему полностью, что подтверждает её более глубокий характер, вероятно, связанный с UCRT и COM.

Рекомендации по дальнейшей отладке и тестированию

Поскольку проблема воспроизводится лишь на некоторых рабочих местах и связана с определенными версиями платформы и ОС, нам потребуется систематический подход к отладке и тестированию.

  1. Добавить подробный отладочный вывод: Встроим в компоненту максимально подробный отладочный вывод в файл. Мы должны логировать следующие события и состояния:
    • Каждый вход в метод компоненты и выход из него, указывая имя метода.
    • Моменты вызова и завершения CoInitializeEx и CoUninitialize.
    • Вызовы ShellExecuteEx, передаваемые ему параметры и возвращаемые значения/ошибки.
    • Любое выделение и освобождение памяти в критических секциях, особенно если это происходит не через менеджер 1С.
    • Инициализацию статических переменных или глобальных объектов, если они есть.
    • Моменты входа и выхода из всех внутренних критических секций или мьютексов компоненты.

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

  2. Создать тестовую версию с заглушенными вызовами: Разработаем специальную тестовую версию компоненты, в которой по очереди будем "заглушать" (комментировать) вызовы определенных методов или части их логики. Например, начнем с полного отключения функционала, использующего ShellExecuteEx. Если зависание прекратится, мы сузим область поиска до этой функции и её окружения. Если нет, перейдем к другим функциям, чтобы постепенно локализовать источник сбоя. Это итеративный процесс, который поможет понять, какой именно функционал компоненты вызывает сбой.
  3. Провести удаленную отладку: Если локальное воспроизведение проблемы невозможно, мы можем использовать удаленную отладку. Например, с помощью Visual Studio Remote Debugger. Этот метод позволяет подключиться к процессу 1С на удаленной машине (или к процессу-контейнеру dllhost.exe, если компонента запускается out-of-process) и пошагово отследить выполнение кода внешней компоненты в момент зависания. Это дает наиболее полную информацию о состоянии программы.
  4. Пересобрать компоненту с разными версиями Visual Studio и пакетами Redistributable: Поскольку проблемы часто связаны с рантаймом CRT, попробуем скомпилировать компоненту с использованием разных версий Visual Studio (например, более старой, соответствующей среде, где работает 8.3.17, если это возможно, или более новой с учетом последних обновлений UCRT). Также мы можем попросить пользователя обновить или переустановить последние Microsoft Visual C++ Redistributable, чтобы исключить проблемы с системными компонентами UCRT. Это может помочь, если проблема кроется в несовместимости версий CRT или линкера.
  5. Отправить исходный код компоненты в 1С: Если все попытки самостоятельной диагностики не привели к решению, мы можем предоставить исходный код внешней компоненты разработчикам платформы 1С. Их опыт, глубокое знание внутренних механизмов платформы и доступ к её исходному коду могут помочь выявить специфические взаимодействия, вызывающие проблему.

Архитектурные решения для повышения стабильности и отказоустойчивости

В качестве более радикального, но очень эффективного решения, мы можем рассмотреть изменение архитектуры взаимодействия с внешней компонентой. Идея заключается в том, чтобы максимально минимизировать логику внутри самой DLL компоненты, тем самым снизив риски зависаний и крахов.

  1. Разделение на минималистичную ВК и внешний сервис: Мы можем перенести основную часть функционала внешней компоненты в отдельный, независимый процесс или HTTP-сервис. Внешняя компонента в этом случае будет выполнять роль тонкого клиента, который только:
    • Запускает/останавливает внешний сервис (если он еще не запущен).
    • Осуществляет вызовы к этому сервису (например, по HTTP или через межпроцессное взаимодействие, такое как именованные каналы или общую память).
    • Получает результаты от сервиса.

    Этот подход имеет несколько значительных преимуществ:

    • Изоляция ошибок: Если внешний сервис зависнет или аварийно завершится, он не потянет за собой процесс 1С. Сервис можно будет перезапустить независимо, без перезапуска всей платформы 1С.
    • Гибкость разработки: Внешний сервис может быть написан на любом языке (C#, Python, Go) и использовать любую технологию, что позволяет выбирать наиболее подходящие инструменты для конкретной задачи.
    • Многопользовательская работа: Несколько сеансов 1С могут обращаться к одному и тому же экземпляру внешнего сервиса, обеспечивая централизованную обработку или управление общими ресурсами.
  2. Запуск ВК в изолированном процессе (Out-of-process COM): Хотя это не является прямым изменением кода компоненты, мы можем настроить запуск компоненты в отдельном процессе (например, через dllhost.exe), если платформа 1С это поддерживает и компонента является COM-объектом. В этом случае, даже если компонента зависнет, она не приведет к зависанию всего приложения 1С. Однако диагностика в этом случае может потребовать анализа дампа процесса-контейнера (dllhost.exe), что усложняет процесс.

Заключение

Проблема зависания внешней компоненты на определенных версиях платформы 1С и операционной системы является сложной и многогранной. Мы проанализировали вероятные причины, включая изменения в платформе 8.3.18+, особенности инициализации Universal C Runtime (UCRT) и критическую необходимость правильной инициализации COM-объектов при использовании ShellExecuteEx. Систематическая отладка с использованием подробных логов, заглушение функционала и, при необходимости, удаленная отладка являются нашими основными инструментами для локализации проблемы. В долгосрочной перспективе, рассмотрение архитектуры с внешним сервисом может значительно повысить стабильность и отказоустойчивость всего решения, делая его менее зависимым от тонкостей взаимодействия с рантаймом 1С и системными библиотеками.

← На главную