При разработке и использовании внешних компонент для 1С:Предприятия мы можем столкнуться с неожиданными трудностями, особенно когда компонента демонстрирует нестабильное поведение, зависая на определенных версиях платформы или операционных систем. Рассмотрим проблему, когда внешняя компонента, созданная на основе шаблона C++, периодически зависает на рабочих местах с Windows 7 (x64) и платформами 1С версий 8.3.18 или 8.3.19, при этом стабильно работая на 8.3.17 и Windows 10. Зависание происходит на этапе подключения компоненты или при вызове её методов.
Прежде всего, давайте внимательно изучим предоставленные симптомы и диагностическую информацию, которая помогает нам сузить круг возможных причин. Мы наблюдаем следующие ключевые моменты:
Предположим, мы получили дамп зависания от пользователя. Анализ дампа позволяет нам увидеть состояние ожидающих потоков в момент блокировки. Рассмотрим фрагмент такого анализа:
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). Два или более потока ждут друг друга, пытаясь получить доступ к ресурсам, занятым другими потоками. В нашем случае, это происходит в критических секциях, связанных с:
LdrpLoadDll).__acrt_thread_attach, construct_ptd)._wsetlocale, CompareStringEx).То, что эта проблема проявляется на платформах 8.3.18+ и Windows 7, является ключевым моментом для нашего расследования.
Платформа 1С:Предприятие 8.3.18 и последующие версии внесли значительные изменения, особенно касающиеся асинхронных функций и методов. Был добавлен новый синтаксис (модификатор процедур и функций Асинх, оператор Ждать, тип Обещание), а также реализованы новые асинхронные вызовы методов и свойств внешних компонент. Эти изменения не могли не затронуть внутренние механизмы загрузки и работы с библиотеками, особенно в многопоточной среде. Отмечается даже выросшее потребление стека, что может быть признаком более сложной логики инициализации или новых требований к рантайму.
Мы предполагаем, что изменения в платформе могли сделать процесс загрузки и инициализации внешней компоненты более чувствительным к корректности работы Universal C Runtime (UCRT) и его инициализации. Если платформа 1С теперь более активно использует многопоточность или специфические возможности UCRT, это может выявить скрытые проблемы взаимодействия, особенно на системах с нестандартными или устаревшими пакетами UCRT.
Как мы уже отметили, стеки вызовов из дампа отчетливо указывают на функции Universal C Runtime (UCRT): __acrt_lock_and_call, construct_ptd, __acrt_thread_attach, _wsetlocale_nolock. Эти функции отвечают за инициализацию рантайма C++, управление данными потоков (per-thread data) и работу с локалью. Зависание происходит из-за взаимной блокировки, когда:
_wsetlocale) в процессе загрузки компоненты или при подготовке к работе, захватывая при этом одну из внутренних критических секций UCRT или загрузчика DLL.DllMain или Init), также пытается инициализировать CRT или получить свои данные потока. Для этого ему нужна та же самая критическая секция или другая, которая уже занята первым потоком.Этот сценарий усугубляется на Windows 7 по нескольким причинам:
kb2999226 и kb2919355), или установленные распространяемые пакеты Microsoft Visual C++ Redistributable (2015, 2017, 2019, 2022), от которых зависит UCRT, могут быть устаревшими. Отсутствие этих обновлений может приводить к ошибкам или нестабильности при загрузке DLL, использующих UCRT (например, api-ms-win-crt-runtime-l1-1-0.dll), что может вызвать блокировки._wsetlocale, CompareStringEx). Взаимодействие с системными функциями локали может быть источником блокировок, особенно если эти функции вызываются в некорректном контексте или из нескольких потоков одновременно, или если системные настройки локали на Win7 отличаются от Win10.Внимательно изучив исходный код внешней компоненты (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С справедливо указывает на то, что "нельзя выделять память для возврата значений с помощью new или malloc, т.к. это приведет к утечке памяти и к нестабильности работы программы" (согласно ITS.1C). Мы должны разобраться в этом вопросе, так как некорректная работа с памятью может привести к трудноуловимым ошибкам и зависаниям.
Мы выделяем память следующим образом, строго придерживаясь правил:
new или malloc. Мы несем полную ответственность за своевременное и корректное освобождение этой памяти.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
Мы уже предприняли попытки исправить потенциальные проблемы, связанные с памятью и инициализацией:
/Zc:threadSafeInit-. Эта опция отключает безопасную для потоков инициализацию статических переменных C++11. Это может быть полезно, если есть подозрения на проблемы с инициализацией статических объектов в многопоточной среде, которые могут приводить к блокировкам.GetClassNames() строки с динамически выделяемой convToShortWchar() на статически выделяемую static WcharWrapper s_names(g_kClassNames). Этот шаг также направлен на исключение проблем с динамическим выделением памяти в потенциально чувствительных местах инициализации компоненты.Однако, как мы узнали из сообщения пользователя, эти изменения, к сожалению, не решили проблему полностью, что подтверждает её более глубокий характер, вероятно, связанный с UCRT и COM.
Поскольку проблема воспроизводится лишь на некоторых рабочих местах и связана с определенными версиями платформы и ОС, нам потребуется систематический подход к отладке и тестированию.
CoInitializeEx и CoUninitialize.ShellExecuteEx, передаваемые ему параметры и возвращаемые значения/ошибки.Этот подробный лог позволит нам точно определить, какой именно метод или участок кода компоненты (или платформы, взаимодействующий с компонентой) приводит к зависанию на проблемных машинах.
ShellExecuteEx. Если зависание прекратится, мы сузим область поиска до этой функции и её окружения. Если нет, перейдем к другим функциям, чтобы постепенно локализовать источник сбоя. Это итеративный процесс, который поможет понять, какой именно функционал компоненты вызывает сбой.dllhost.exe, если компонента запускается out-of-process) и пошагово отследить выполнение кода внешней компоненты в момент зависания. Это дает наиболее полную информацию о состоянии программы.В качестве более радикального, но очень эффективного решения, мы можем рассмотреть изменение архитектуры взаимодействия с внешней компонентой. Идея заключается в том, чтобы максимально минимизировать логику внутри самой DLL компоненты, тем самым снизив риски зависаний и крахов.
Этот подход имеет несколько значительных преимуществ:
dllhost.exe), если платформа 1С это поддерживает и компонента является COM-объектом. В этом случае, даже если компонента зависнет, она не приведет к зависанию всего приложения 1С. Однако диагностика в этом случае может потребовать анализа дампа процесса-контейнера (dllhost.exe), что усложняет процесс.Проблема зависания внешней компоненты на определенных версиях платформы 1С и операционной системы является сложной и многогранной. Мы проанализировали вероятные причины, включая изменения в платформе 8.3.18+, особенности инициализации Universal C Runtime (UCRT) и критическую необходимость правильной инициализации COM-объектов при использовании ShellExecuteEx. Систематическая отладка с использованием подробных логов, заглушение функционала и, при необходимости, удаленная отладка являются нашими основными инструментами для локализации проблемы. В долгосрочной перспективе, рассмотрение архитектуры с внешним сервисом может значительно повысить стабильность и отказоустойчивость всего решения, делая его менее зависимым от тонкостей взаимодействия с рантаймом 1С и системными библиотеками.