Как правильно получать параметры в 1C Native API и настраивать зависимые поля в XML

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

При разработке внешних компонент по технологии 1C Native API (особенно для интеграции торгового и подключаемого оборудования через БСП/БПО) программисты часто сталкиваются с неочевидным поведением памяти и ограничениями интерфейса настройки. В рамках данной статьи мы подробно разберем две важнейшие проблемы, которые часто вызывают вопросы на форумах разработчиков:

  1. Порча памяти в массиве параметров при вызове метода SetParameter (когда данные во втором параметре внезапно «исчезают» или превращаются в пустые значения после обработки первого параметра).
  2. Реализация динамической зависимости параметров на форме настройки оборудования (например, когда поле выбора порта или IP-адреса должно становиться доступным только при выборе соответствующего типа подключения).

Давайте подробно проанализируем обе ситуации, выясним глубинные технические причины их возникновения и найдем красивые, безопасные решения.

 

Часть 1. Разбор порчи памяти в методе SetParameter

Типичная ситуация, с которой сталкивается разработчик Native-компоненты: при вызове метода SetParameter имя параметра передается в элементе paParams[0], а его значение — в paParams[1]. На первый взгляд, логика проста: мы считываем имя параметра, конвертируем его в удобный для C++ формат wchar_t*, а затем пытаемся считать значение. Однако в этот момент оказывается, что значение в paParams[1] испорчено или равно нулю.

Посмотрим на пример кода, в котором проявляется эта ошибка:


case SetParameter:
{
    wchar_t* wsTmp = 0;
    // После вызова этой функции структура paParams[1] оказывается испорченной!
    convFromShortWchar(&wsTmp, TV_WSTR(&paParams[0]));
    
    if (wcscmp(wsTmp, L"Port") == 0) {
        Port = TV_WSTR(&paParams[1]); // Здесь уже пусто или мусор!
    }
}
break;

Выясним причину этого поведения. Проблема кроется в стандартном шаблоне (SDK) внешней компоненты, который предоставляет фирма «1С». В нем функция конвертации строк convFromShortWchar реализована с критической ошибкой, приводящей к переполнению буфера (Buffer Overrun). Проанализируем ее стандартный код:


uint32_t convFromShortWchar(wchar_t** Dest, const WCHAR_T* Source, uint32_t len) {
    if (!len) len = getLenShortWcharStr(Source) + 1;
    if (!*Dest) *Dest = new wchar_t[len];
    wchar_t* tmpWChar = *Dest;
    WCHAR_T* tmpShort = (WCHAR_T*)Source;
    uint32_t res = 0;
    ::memset(*Dest, 0, len * sizeof(wchar_t));
    do {
        *tmpWChar++ = (wchar_t)*tmpShort++; // Инкремент указателя до проверки условия!
        ++res;
    } while (len-- && *tmpShort); // Проверка уже инкрементированного указателя!
    return res;
}

Обратите внимание на цикл do-while. Указатель tmpShort сдвигается вперед непосредственно в момент копирования символа. Проверка условия продолжения цикла *tmpShort осуществляется уже для следующего адреса памяти. Если исходная строка пуста или ее длина в точности соответствует выделенной памяти, цикл выполняет одну лишнюю итерацию, считывая «мусор» за пределами исходной строки и записывая его в целевой буфер.

Так как массив параметров paParams (состоящий из структур tVariant) располагается в памяти кучи последовательно, переполнение буфера при обработке строки из paParams[0] физически затирает заголовок или данные соседней структуры paParams[1]. Именно поэтому значение второго параметра портится.

Кроме того, в приведенном коде присутствует явная утечка памяти (Memory Leak). Локальный указатель wchar_t* wsTmp инициализируется нулем, функция выделяет под него память через оператор new[], но в конце метода эта память нигде не освобождается с помощью delete[].

 

Безопасная реализация конвертации строк и метода SetParameter

Для того чтобы полностью исключить порчу памяти и утечки, мы напишем безопасную версию функции конвертации, которая корректно контролирует границы буфера и не использует опасный цикл do-while. Также мы внедрим обязательное освобождение памяти.

Рассмотрим безопасный вариант функции конвертации строк:


uint32_t safeConvFromShortWchar(wchar_t** Dest, const WCHAR_T* Source) {
    if (!Source) return 0;
    
    // Вычисляем длину строки без завершающего нуля
    uint32_t len = 0;
    while (Source[len] != 0) {
        len++;
    }
    
    // Выделяем память с учетом завершающего нуля
    if (!*Dest) {
        *Dest = new wchar_t[len + 1];
    }
    
    // Посимвольное копирование с контролем границ
    for (uint32_t i = 0; i < len; ++i) {
        (*Dest)[i] = static_cast<wchar_t>(Source[i]);
    }
    (*Dest)[len] = L'\0'; // Гарантированный нулевой символ в конце
    
    return len;
}

Теперь применим эту функцию в методе SetParameter. Также крайне важно всегда проверять тип передаваемых данных через макрос TV_VT. Если параметр в XML-описании объявлен как число (TypeValue="Number"), платформа 1С передаст его в структуре tVariant с типом VTYPE_I4 (или VTYPE_R8), а не в виде строки. Попытка прочесть число как строку через макрос TV_WSTR приведет к немедленному аварийному завершению работы программы (Access Violation).

Разберем правильный шаблон обработки параметров:


case SetParameter:
{
    wchar_t* wsParamName = nullptr;
    safeConvFromShortWchar(&wsParamName, TV_WSTR(&paParams[0]));
    
    if (wsParamName != nullptr) {
        // Проверяем тип значения во втором параметре
        if (wcscmp(wsParamName, L"Port") == 0) {
            if (TV_VT(&paParams[1]) == VTYPE_I4) {
                this->Port = TV_INT(&paParams[1]);
            } else if (TV_VT(&paParams[1]) == VTYPE_PWSTR) {
                // Если пришла строка, конвертируем ее в число или сохраняем как текст
                wchar_t* wsValue = nullptr;
                safeConvFromShortWchar(&wsValue, TV_WSTR(&paParams[1]));
                if (wsValue) {
                    this->Port = _wtoi(wsValue);
                    delete[] wsValue; // Освобождаем временную строку значения
                }
            }
        }
        else if (wcscmp(wsParamName, L"Speed") == 0) {
            if (TV_VT(&paParams[1]) == VTYPE_I4) {
                this->Speed = TV_INT(&paParams[1]);
            }
        }
        else if (wcscmp(wsParamName, L"IPAddress") == 0) {
            if (TV_VT(&paParams[1]) == VTYPE_PWSTR) {
                wchar_t* wsValue = nullptr;
                safeConvFromShortWchar(&wsValue, TV_WSTR(&paParams[1]));
                if (wsValue) {
                    this->IPAddress = wsValue; // Предполагается использование std::wstring
                    delete[] wsValue;
                }
            }
        }
        
        // Обязательно освобождаем память, выделенную под имя параметра!
        delete[] wsParamName;
    }
    
    // Возвращаем успешный результат выполнения операции в 1С
    TV_VT(pvarRetValue) = VTYPE_BOOL;
    TV_BOOL(pvarRetValue) = true;
}
break;

 

Часть 2. Настройка динамической зависимости параметров формы в XML

Перейдем ко второму вопросу: как сделать так, чтобы форма настройки оборудования в 1С менялась динамически? Например, при выборе модели «Принтер этикеток (сетевой)» поле выбора COM-порта должно блокироваться, а поле ввода IP-адреса — становиться активным.

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

Механизм БПО полностью поддерживает описание зависимостей параметров непосредственно в XML-структуре, которую компонента возвращает при вызове метода GetParameters. Для этого у тега <Parameter> существуют три специальных служебных атрибута:

Давайте посмотрим, как составить такое описание на практическом примере. Создадим XML, в котором выбор типа подключения (COM-порт или Ethernet-подключение) управляет доступностью параметров «Порт» и «IP-адрес».


<?xml version="1.0" encoding="UTF-8"?>
<Settings>
    <Page Caption="Параметры устройства">
        <Group Caption="Настройки интерфейса связи">
            
            <!-- Главный параметр: тип подключения -->
            <Parameter Name="ConnectionType" Caption="Тип подключения" TypeValue="Number" DefaultValue="0">
                <ChoiceList>
                    <Item Value="0">Последовательный порт (COM)</Item>
                    <Item Value="1">Сетевое подключение (TCP/IP)</Item>
                </ChoiceList>
            </Parameter>
            
            <!-- Зависимый параметр: номер COM-порта -->
            <!-- Поле активно, только если ConnectionType равно 0 -->
            <Parameter Name="Port" Caption="Номер порта" TypeValue="Number" DefaultValue="1"
                       MasterParameterName="ConnectionType"
                       MasterParameterOperation="Equal"
                       MasterParameterValue="0">
                <ChoiceList>
                    <Item Value="1">COM1</Item>
                    <Item Value="2">COM2</Item>
                    <Item Value="3">COM3</Item>
                </ChoiceList>
            </Parameter>
            
            <!-- Зависимый параметр: скорость передачи (активна только для COM-порта) -->
            <Parameter Name="Speed" Caption="Скорость" TypeValue="Number" DefaultValue="9600"
                       MasterParameterName="ConnectionType"
                       MasterParameterOperation="Equal"
                       MasterParameterValue="0">
                <ChoiceList>
                    <Item Value="9600">9600</Item>
                    <Item Value="115200">115200</Item>
                </ChoiceList>
            </Parameter>
            
            <!-- Зависимый параметр: IP-адрес -->
            <!-- Поле активно, только если ConnectionType равно 1 (Сетевое подключение) -->
            <Parameter Name="IPAddress" Caption="IP Адрес" TypeValue="String" DefaultValue="192.168.1.100"
                       MasterParameterName="ConnectionType"
                       MasterParameterOperation="Equal"
                       MasterParameterValue="1"/>
            
            <!-- Зависимый параметр: Сетевой TCP-порт -->
            <Parameter Name="TCPPort" Caption="Порт TCP" TypeValue="Number" DefaultValue="4001"
                       MasterParameterName="ConnectionType"
                       MasterParameterOperation="Equal"
                       MasterParameterValue="1"/>
                       
        </Group>
    </Page>
</Settings>

Когда платформа 1С (а именно БПО) визуализирует форму настроек на основе этого XML-документа, она автоматически подпишется на событие изменения параметра ConnectionType. При интерактивном переключении пользователем значения с «Последовательный порт (COM)» на «Сетевое подключение (TCP/IP)» форма мгновенно заблокирует поля выбора порта и скорости, сделав активными поля для ввода IP-адреса и TCP-порта.

 

Рекомендации по разработке надежных Native-компонент

Для того чтобы ваши внешние компоненты работали стабильно и не вызывали непредвиденных сбоев («падений» сеанса 1С с критическими ошибками), рекомендуем придерживаться следующих правил:

  1. Контролируйте типы входящих данных: Никогда не доверяйте типу по умолчанию. Перед извлечением значения из структуры tVariant проверяйте значение TV_VT(&var). При необходимости выполняйте безопасное приведение типов.
  2. Избегайте утечек памяти: Всегда очищайте буферы строк, созданные для конвертации данных из UTF-16 (формат 1С) в системный формат вашей операционной системы (UTF-8 или UTF-32). Используйте стандартные контейнеры (например, std::wstring) для долгосрочного хранения параметров внутри класса компоненты.
  3. Не используйте стандартный do-while в convFromShortWchar: Замените его на классический цикл for с явным ограничением по длине строки, чтобы избежать перезаписи смежных блоков оперативной памяти.
  4. Используйте штатные возможности БПО для XML: Это позволит вам сократить объем кода и сделать интерфейс вашей компоненты привычным и удобным для конечных пользователей 1С.

Применение этих простых правил гарантирует бесперебойную работу вашего решения и сэкономит массу времени при отладке кода.

← На главную