Как написать внешнюю COM-компоненту для 1С на C#: решаем проблемы с разрядностью DLL, маршалингом и памятью

Программист IT и автоматизация бизнеса
← На главную

Рассмотрим ситуацию, с которой часто сталкиваются разработчики при интеграции внешнего оборудования или сторонних библиотек с 1С. Допустим, мы пишем внешнюю компоненту (ВК) по технологии COM на языке C#. Наша компонента выступает "оберткой" для работы с нативным SDK устройства (например, сканера отпечатков). При разработке сразу же возникают сложные вопросы: как динамически загружать нужную версию (32 или 64 бита) сторонней библиотеки, как передавать массивы из 1С в .NET и обратно, а также как не допустить утечек памяти.

Разберем по шагам все эти аспекты и найдем эффективные решения на основе классических методов и современных возможностей платформы .NET.

Динамическая загрузка 32- и 64-разрядных нативных DLL

Одной из главных проблем при разработке ВК является разрядность клиента 1С или сервера, на котором выполняется код. Если наша COM-компонента (написанная на C# с параметром сборки AnyCPU) обращается к нативной библиотеке на C++, платформа ожидает найти DLL правильной разрядности. Иначе возникает ошибка "попытка загрузить программу в неверном формате" (BadImageFormatException).

Создавать два отдельных проекта под x86 и x64 — плохая практика. Универсальный подход заключается в том, чтобы хранить нативные библиотеки в подпапках (например, x32 и x64) рядом с нашей сборкой и загружать их динамически при инициализации.

Проанализируем классический вариант с использованием функции Windows API LoadLibrary в статическом конструкторе класса:


static class MyDll
{
    static MyDll()
    {            
        var myPath = new Uri(typeof(MyDll).Assembly.CodeBase).LocalPath;
        var myFolder = Path.GetDirectoryName(myPath);
        var is64 = IntPtr.Size == 8;
        var subfolder = is64 ? "\\x64\\" : "\\x32\\";
        LoadLibrary(Path.Combine(myFolder, subfolder, "tc-b_new_sdk.dll"));
    }

    [DllImport("kernel32.dll")]
    private static extern IntPtr LoadLibrary(string dllToLoad);

    [DllImport("tc-b_new_sdk.dll")]
    public static extern int MyFunction(int var1, int var2);
}

Здесь мы проверяем размер указателя IntPtr.Size (4 байта для 32-битных систем и 8 байт для 64-битных). На основе этого выбирается нужная папка, и библиотека загружается в память процесса до того, как будет вызван любой метод с атрибутом [DllImport].

Важное замечание по безопасности вызовов: Использование API SetDllDirectory, которое часто рекомендуют, может вызвать побочные эффекты (side effects), так как оно меняет пути поиска библиотек для всего рабочего процесса 1С (rphost или клиента). Это может сломать работу других загруженных внешних компонент. В современных версиях .NET (начиная с .NET Core 3.0 и .NET 5+) лучше использовать встроенный механизм NativeLibrary.SetDllImportResolver(). Он позволяет перехватить обращение к библиотеке и точечно подсунуть нужный файл без изменения системных путей.

Работа с типами Variant и COMSafeArray

При передаче неопределенных типов (в 1С это Произвольный тип, маршалируемый как COM-структура Variant), в C# параметры следует объявлять как object. Среда .NET автоматически разворачивает простые типы 1С (числа, строки, булево) в соответствующие системные типы C#.

Рассмотрим работу с параметрами, которые могут принимать разные типы данных. Мы можем использовать конструкцию switch по имени параметра и преобразовывать object к нужному типу через Convert:


public object SetParam(string param, object value = null)
{
    object prev = value;
    if (value != null)
    {
        switch (param)
        {
            case "GetUsersCount":
                long GetUsersCount = Convert.ToInt64(value);
                // Обработка
                break;
            case "UseLongName":
                bool UseLongName = Convert.ToBoolean(value);
                // Обработка
                break;
        }
    }
    return prev;
}

Что касается массивов COMSafeArray, платформа .NET прекрасно справляется с их автоматическим преобразованием. Если из 1С передается массив элементов, на стороне C# метод можно описать как принимающий массив object[] или Array. Для возврата массива в 1С достаточно просто вернуть массив C#, и CCW (COM Callable Wrapper) сам запакует его в COMSafeArray.

Проблема возврата двумерных массивов

Частая ошибка при разработке COM-компонент на C# — попытка вернуть в 1С "ступенчатый" массив массивов (jagged array), объявленный как object[][]. В 1С такой массив читается неудобно: приходится извлекать первый массив, а затем из него нужный элемент (конструкция вида РезультатМассив.GetValue(0).GetValue(0)).

Разберем, почему так происходит. Ступенчатый массив object[][] превращается в одномерный SAFEARRAY, каждый элемент которого содержит тип VARIANT со ссылкой на вложенный SAFEARRAY. Чтобы 1С увидела полноценный двумерный массив, в котором можно использовать GetValue(x, y), в C# необходимо возвращать строго многомерный массив object[,].


public object[,] GetAllMessageHead(int id = 0)
{
    object[,] result = new object[10, 2];
    result[0, 0] = "Первое значение";
    result[0, 1] = "Второе значение";
    return result;
}

Среда .NET автоматически сформирует правильный дескриптор размерностей для COM, и в 1С мы получим классический многомерный массив.

Управление неуправляемой памятью

Если компонента активно общается с SDK на C++ и получает структуры данных через указатели (например, считывая информацию об отпечатках пальцев), программисту приходится самостоятельно выделять неуправляемую память.

Обычно для этого используют Marshal.AllocHGlobal(размер). Однако если просто выделить буфер и забыть о нем, произойдет утечка памяти, что в случае работы на сервере 1С быстро приведет к падению службы агента сервера. Выделенную память обязательно нужно возвращать системе с помощью Marshal.FreeHGlobal(указатель).

Посмотрим на современную альтернативу. Для локальных буферов (когда память нужна только внутри одного метода, например, для распаковки структуры) лучше использовать выделение на стеке. В C# для этого применяются типы Span<T> и инструкция stackalloc:


// Безопасное выделение 32 КБ памяти на стеке
Span<byte> buffer = stackalloc byte[32000];
// Использование буфера в вызовах нативного API
// ...
// Память освободится автоматически и мгновенно при выходе из метода

Этот подход не нагружает сборщик мусора (GC) и полностью исключает вероятность утечек.

Корректное уничтожение COM-объекта и очистка ресурсов

Разберем еще один нюанс. Объекты .NET, созданные как COM-объекты, не выгружаются из памяти немедленно при выполнении команды КомКомпонента = Неопределено; в 1С. Они остаются в куче до следующего прохода сборщика мусора. Если внутри класса остаются открытые сетевые порты или соединения с оборудованием, повторная инициализация ВК может зависнуть.

Чтобы избежать этой проблемы, класс компоненты должен реализовывать интерфейс IDisposable. В методе Dispose() мы явно разрываем соединение с оборудованием, закрываем порты и очищаем неуправляемую память.

Из кода 1С перед удалением объекта мы можем вызвать специальный метод ВК, например Stop(), который внутри C# вызовет очистку. Принудительный сбор мусора с помощью вызовов GC.Collect() и GC.WaitForPendingFinalizers() является экстренной мерой (паттерном "подчистки огрехов") и его стоит применять только в крайних случаях, если архитектура не позволяет корректно управлять жизненным циклом объектов.

Альтернатива COM: технология NativeAOT

В завершение стоит отметить, что технология COM постепенно устаревает. Начиная с .NET 7, разработчикам доступна мощная технология NativeAOT. Она позволяет компилировать C# код напрямую в нативный машинный код.

Используя атрибут [UnmanagedCallersOnly], можно создать из C# кода обычную C-совместимую библиотеку, реализующую стандартный интерфейс Native API для 1С. Такая ВК не требует регистрации через regasm, легко грузится из макета как `zip`-архив и автоматически выгружается платформой без "застрявших" в памяти оболочек.

← На главную