Разработка внешних компонент (ВК) для платформы 1С на языке C++ с использованием технологии Native API — это мощный инструмент, позволяющий интегрировать учетную систему с внешним оборудованием, CAD-системами, криптографией и сложными вычислительными библиотеками. Однако разработчики часто сталкиваются с крайне неприятным поведением: при первом вызове метода компоненты всё отрабатывает идеально, но при повторном обращении (в рамках того же сеанса 1С) система аварийно завершает работу или выдает неинформативную ошибку «Не найден файл внешней компоненты».
В этой статье мы подробно разберем, почему возникает эта проблема, как операционная система Windows и платформа 1С работают с адресным пространством процесса, и рассмотрим четыре надежных инженерных решения для исправления этой ситуации.
Для начала проанализируем типичный сценарий, с которым сталкивается разработчик. В коде внешней компоненты C++ создается метод (например, CallBack), который обращается к функциям сторонней динамической библиотеки (DLL). Типичный пример такого кода в C++ выглядит следующим образом:
case eCallBack:
{
if (!lSizeArray || !paParams)
return false;
s1 = (paParams)->pwstrVal;
s2 = (paParams + 1)->pwstrVal;
int answerCode = 0;
// Вызовы функций инициализации и деинициализации стороннего API
answerCode = UF_initialize();
UF_terminate();
if (answerCode != 1) {
wstring_to_p(std::wstring(s1), pvarRetValue);
} else {
wstring_to_p(std::wstring(s2), pvarRetValue);
}
return true;
}
На стороне 1С вызов компоненты выглядит стандартно:
ПодключитьВнешнююКомпоненту("d:\1С_DLL.dll", "DemoVK", ТипВнешнейКомпоненты.Native);
ДемоКомп = Новый("AddIn.DemoVK.SameName");
ПервыйВызов = ДемоКомп.CallBack("YES!", "NO!!");
Сообщить(ПервыйВызов); // Отрабатывает успешно
// Повторный вызов в коде или по кнопке на форме:
ВторойВызов = ДемоКомп.CallBack("YES!", "NO!!"); // Ошибка: Не найден файл внешней компоненты
Давайте разберем по шагам, какие процессы происходят в операционной системе, и почему возникает этот сбой.
UF_initialize и UF_terminate) проектируются с расчетом на то, что инициализация API происходит строго один раз за все время жизни процесса операционной системы. Когда вы вызываете метод деинициализации (например, UF_terminate), библиотека переводит свои внутренние статические структуры в терминальное состояние. Повторный вызов функции инициализации в том же самом процессе 1cv8.exe либо игнорируется с ошибкой, либо приводит к нарушению доступа к памяти (Access Violation).FreeLibrary. Однако сторонняя DLL может быть не готова к корректной выгрузке из памяти. В результате повторный вызов LoadLibrary со стороны 1С завершается системной ошибкой, которую платформа интерпретирует для пользователя как «Не найден файл внешней компоненты», хотя сам файл физически находится на диске..lib файл), то при повторной попытке загрузки операционная система Windows начинает искать зависимости в изменившемся рабочем каталоге и, не найдя их там, блокирует загрузку компоненты.---
Самая грубая ошибка проектирования — инициализировать и уничтожать окружение сторонней библиотеки при каждом вызове прикладного метода компоненты. Вместо этого мы должны использовать стандартные методы жизненного цикла Native API 1С, предоставляемые интерфейсом IComponentBase. Это методы Init и Done.
Рассмотрим структуру правильной реализации класса компоненты на C++:
class CDemoComponent : public IComponentBase
{
private:
bool m_bAPIInitialized; // Флаг успешной инициализации стороннего API
public:
CDemoComponent() : m_bAPIInitialized(false) {}
// Вызывается платформой 1С один раз при создании объекта Новый("AddIn...")
virtual bool ADDIN_API Init(void* pConnection) override
{
m_pConnect = (IAddInDefBase*)pConnection;
// Инициализируем стороннюю библиотеку один раз при старте
if (!m_bAPIInitialized)
{
int res = UF_initialize();
if (res == 0) // Предположим, 0 - успешный статус
{
m_bAPIInitialized = true;
}
}
return m_bAPIInitialized;
}
// Вызывается платформой 1С один раз при уничтожении объекта компоненты
virtual void ADDIN_API Done() override
{
// Деинициализируем стороннее API только при полном закрытии компоненты
if (m_bAPIInitialized)
{
UF_terminate();
m_bAPIInitialized = false;
}
m_pConnect = nullptr;
}
// Прикладной метод CallBack теперь занимается только полезной работой
bool CallMethodCallBack(tVariant* paParams, tVariant* pvarRetValue)
{
if (!m_bAPIInitialized)
return false;
// Выполняем полезные функции без вызова UF_initialize/UF_terminate
// ...
return true;
}
};
При таком подходе инициализация тяжелого API происходит ровно один раз при создании объекта в 1С, а завершение работы — при закрытии формы или завершении сеанса пользователя. Это полностью решает проблему повторных вызовов метода в рамках одной сессии.
---
Если сторонняя библиотека имеет сложную структуру зависимостей, неявное связывание (через линковку .lib файла в Visual Studio) делает вашу компоненту уязвимой к путям поиска DLL в Windows. Лучшей практикой является явная динамическая загрузка библиотеки с помощью системных вызовов WinAPI.
Разберем, как реализовать этот подход на практике:
#include <windows.h>
// Определяем указатели на функции сторонней DLL
typedef int (*PFN_UF_INITIALIZE)();
typedef int (*PFN_UF_TERMINATE)();
HMODULE g_hThirdPartyDLL = nullptr;
PFN_UF_INITIALIZE g_UF_initialize = nullptr;
PFN_UF_TERMINATE g_UF_terminate = nullptr;
bool LoadThirdPartyLibrary()
{
if (g_hThirdPartyDLL != nullptr)
return true; // Уже загружено
// Рекомендуется использовать абсолютный путь к сторонней DLL
const wchar_t* dllPath = L"C:\\Program Files\\Siemens\\NX\\bin\\libufun.dll";
// Загружаем библиотеку с флагом поиска зависимостей в ее собственном каталоге
g_hThirdPartyDLL = LoadLibraryEx(dllPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (!g_hThirdPartyDLL)
{
// Обработка ошибки загрузки
return false;
}
// Получаем адреса необходимых функций
g_UF_initialize = (PFN_UF_INITIALIZE)GetProcAddress(g_hThirdPartyDLL, "UF_initialize");
g_UF_terminate = (PFN_UF_TERMINATE)GetProcAddress(g_hThirdPartyDLL, "UF_terminate");
if (!g_UF_initialize || !g_UF_terminate)
{
FreeLibrary(g_hThirdPartyDLL);
g_hThirdPartyDLL = nullptr;
return false;
}
return true;
}
Использование флага LOAD_WITH_ALTERED_SEARCH_PATH гарантирует, что если загружаемая сторонняя библиотека зависит от десятка других DLL в своей папке, Windows успешно найдет их по месту расположения основного файла, игнорируя текущий рабочий каталог процесса 1С.
---
Иногда платформа 1С агрессивно выгружает DLL внешней компоненты из оперативной памяти при уничтожении контекста (например, при закрытии обработки). Если сторонняя библиотека оставляет после себя активные системные потоки или хуки, повторный вызов LoadLibrary неизбежно вызовет краш процесса 1cv8.exe.
Мы можем принудительно запретить операционной системе выгружать нашу ВК из памяти до тех пор, пока полностью не завершится процесс 1С. Для этого в коде компоненты в функции DllMain необходимо выполнить операцию «закрепления» (Pinning) модуля:
HMODULE g_hSelfModule = nullptr;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hSelfModule = hModule;
// Запрещаем выгрузку библиотеки через FreeLibrary
HMODULE hTemp;
GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
(LPCWSTR)DllMain,
&hTemp);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Флаг GET_MODULE_HANDLE_EX_FLAG_PIN увеличивает счетчик блокировок модуля до бесконечности. Теперь, сколько бы раз платформа 1С ни вызывала внутренний метод выгрузки библиотек, операционная система оставит вашу ВК загруженной в адресном пространстве процесса. Это предотвратит деструктуризацию памяти и обеспечит стабильную работу при повторных подключениях.
---
Если архитектура стороннего API категорически не совместима со стабильной работой внутри родительского процесса 1С (например, библиотека вызывает утечки памяти, жестко перехватывает системные сигналы или требует специфического окружения выполнения), лучшим архитектурным решением является создание консольного приложения-обертки (Wrapper).
В этом случае схема взаимодействия выглядит следующим образом:
api_wrapper.exe), которое принимает параметры через аргументы командной строки или файл конфигурации.Посмотрим на пример кода на стороне 1С для вызова такой консольной обертки:
&НаКлиенте
Функция ВыполнитьРасчетЧерезСтороннееAPI(Параметр1, Параметр2)
ПутьКПрограмме = "C:\MyComponents\api_wrapper.exe";
ФайлРезультата = ПолучитьИмяВременногоФайла("txt");
// Формируем аргументы командной строки
ШаблонЗапуска = """%1"" --param1 ""%2"" --param2 ""%3"" --output ""%4""";
СтрокаЗапуска = СтрШаблон(ШаблонЗапуска, ПутьКПрограмме, Параметр1, Параметр2, ФайлРезультата);
КодВозврата = Неопределено;
// Запускаем приложение в синхронном режиме (ожидаем завершения)
ЗапуститьПриложение(СтрокаЗапуска, , Истина, КодВозврата);
Если КодВозврата <> 0 Тогда
ВызватьИсключение "Ошибка выполнения внешнего API. Код возврата: " + КодВозврата;
КонецЕсли;
// Читаем полученный результат
ТекстРез = Новый ТекстовыйДокумент;
ТекстРез.Прочитать(ФайлРезультата, КодировкаТекста.UTF8);
Результат = ТекстРез.ПолучитьТекст();
// Удаляем временный файл
Попытка
УдалитьФайлы(ФайлРезультата);
Исключение
КонецПопытки;
Возврат Результат;
КонецФункции
Преимущества данного подхода:
api_wrapper.exe и никак не влияют на стабильность работы сеанса пользователя в 1С.Мы подробно проанализировали причины сбоев при интеграции сторонних DLL во внешние компоненты 1С. При проектировании систем интеграции важно помнить:
Init и Done.