Интеграция 1С с DLL с помощью Python

Привет Хабр! Недавно я разработал алгоритм для логистики, и нужно было его куда-то пристроить. Помимо веб-сервиса решено было внедрить данный модуль в 1С, и тут появилось довольно много подводных камней.

Начнем с того, что сам алгоритм представлен в виде dll библиотеки, у которой одна точка входа, принимающая JSON строку как параметр, и отдающая 2 колбэка. Первый для отображения статуса выполнения, другой для получения результата. С web-сервисом все довольно просто, у питона есть замечательный пакет ctypes, достаточно подгрузить нужную библиотеку и указать точку входа.

Выглядит это примерно так:

import ctypes
def callback_recv(*args):
	print(args)

lib = ctypes.cdll.LoadLibrary('test.dll')
Callback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
my_func = getattr(lib, '_ZN7GtTools4testEPKcPFviS1_E')
cb_func = Callback(callback_recv)
my_func(ctypes.c_char_p('some data'), cb_func)
 

Как можно заметить, точка входа не совсем читабельная. Чтобы найти данную строчку в скомпилировнанных данных, нужно открыть соответствующий файл с расширением .lib и применить утилиту objdump с параметром -D, в выводе легко можно найти нужный метод по названию.

Данное коверканье метода происходит из-за того, что компилятор манглит («mangle» — калечить) название всех точек входа, причем разные компиляторы «калечат» по разному. В примере указан метод полученный MinGW

В 1С все оказалось гораздо менее тривиально. Для подключения dll нужно, чтобы у нее был специальный интерфейс Native API, позволяющий зарегестрировать Внешнюю Компоненту. Все написал по примеру, но ничего не взлетало. Я подумал, что это из-за gcc. Все мои попытки поставить Visual Studio были провальны, то ничего не устанавливалось, то не хватало стандартных библиотек.

Уже засыпая мне в голову пришла гениальная гипотеза. Наверное данную проблему не могли не оставить питонисты, ведь на Питон разработно все, что вообще возможно. А-ля правило интернета 34, только по отношению к чудесному Python. И ведь я оказался прав!

Для python существует пакет win32com который позволяет регестрировать Python объекты, как COM объекты. Для меня это было какой то магией, ведь я даже не очень понимаю что такое COM объект, но знаю что он умеет в 1С.

Пакет pypiwin32 не нужно ставить с помощью pip, а скачать его установщик, т.к. почему-то объекты не регестрировались после установки pip'ом.

Разобравшись с небольшим примером, я взялся за разработку. Для начала нужно создать Объект с интерфейсом идентифицирующим COM-Объект в системе

class GtAlgoWrapper():
    # com spec
    _public_methods_ = ['solve','resultCallback', 'progressCallback',] # методы объекта
    _public_attrs_ = ['version',] # атрибуты объекта
    _readonly_attr_ = []
    _reg_clsid_ = '{2234314F-F3F1-2341-5BA9-5FD1E58F1526}' # uuid объекта
    _reg_progid_= 'GtAlgoWrapper' # id объекта
    _reg_desc_  = 'COM Wrapper For GTAlgo' # описание объекта
    def __init__(self):
        self.version = '0.0.1'
        self.progressOuterCb = None
        # ...

    def solve(self, data):
        # ...
        return ''

    def resultCallback(self, obj): 
        # ...
        return obj

    def progressCallback(self, obj): 
       # в колбэк необходимо передавать 1С объект, в котором идет подключение 
       # например ЭтотОбъект или ЭтаФорма
        if str(type(obj)) == "<type 'PyIDispatch'>": 
            com_obj = win32com.client.Dispatch(obj)
            try:
               # сохраним функцию из 1С (progressCallback) в отдельную переменную
               self.progressOuterCb = com_obj.progressCallback1C; 
           except AttributeError:
                raise Exception('"progressCallback" не найден в переданном объекте')
        return obj

и конечно опишем его регистрацию

def main():
    import win32com.server.register
    win32com.server.register.UseCommandLine(GtAlgoWrapper)
    print('registred')

if __name__ == '__main__':
    main()

Теперь при запуске данного скрипта в системе появится объект GtAlgoWrapper. Его вызов из 1С будет выглядеть вот так:

Функция progressCallback1C(знач, тип) Экспорт
    Сообщить("значение = " + знач);
    Сообщить("тип = " + тип);
КонецФункции
//...
Процедура Кнопка1Нажатие(Элемент)
    //Создадим объект
   ГТАлго =  Новый COMОбъект("GtAlgoWrapper");
    //Установим колбэки
   ГТАлго.progressCalback(ЭтотОбъект);
   //...
   Данные = ...; // JSON строка
   ГТАлго.solve(Данные);
КонецПроцедуры

Таким образом, все попадающие в колбэки даные можно будет обработать. Единственное, что может еще остаться непонятным — как передать данные из dll в 1C:

_dependencies = ['libwinpthread-1.dll',
                     'libgcc_s_dw2-1.dll',
                     # ...,
                     'GtRouting0-0-1.dll']
def solve(self, data):
        prefix_path = 'C:/release'
        # должны быть подключены все зависимые библиотеки
        try:
            for dep in self._dependencies:
                ctypes.cdll.LoadLibrary(os.path.join(prefix_path, dep))
            # запоминаем библиотеку с нужной нам точкой входа
            lib = ctypes.cdll.LoadLibrary(os.path.join(prefix_path, 'GtAlgo0-0-1.dll'))
        except WindowsError:
            raise Exception('cant load' + dep)

        solve_func = getattr(lib, '_ZN6GtAlgo5solveEPKcPFviS1_ES3_')
       
        # создаем колбэки
        StatusCallback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
        ResultCallback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
        scb_func = StatusCallback(self.progressOuterCb)
        rcb_func = ResultCallback(self.resultOuterCb)
        # колбэки 1C превратились в функции которые мы передадим в DLL. Magic!
        if self.resultOuterCb is None:
            raise Exception('resultCallback function is not Set')
        if self.progressOuterCb is None:
            raise Exception('progressCallback function is not Set')
        # запустим алгоритм
        solve_func(ctypes.c_char_p(data), scb_func, rcb_func)

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

Вот так просто можно связать dll библиотеку и 1C с помощью питона, не уползая в сильные дебри.
Всем Магии!

Полезные ссылки
docs.python.org/3/library/ctypes.html — Пакет ctypes
citforum.ru/book/cook/dll0.shtml — Динамические библиотеки для чайников
habrahabr.ru/post/191014 — NativeAPI
infostart.ru/public/115486 — COM объект на C++
infostart.ru/public/190166 — COM объект на Python
pastebin.com/EFLnnrfp — Полный код скрипта на Python из статьи
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 15

    0
    регестрировать Python объекты, как COM объекты
    … но это уже не Native API. Да, имею сказать про Native API — оно и правда весьма такое себе… Полный C++, с множественным наследованием, причем на ровном месте. Имею с ним не то чтобы богатый, но опыт — так вот моё мнение — проще, как для разработчиков API, так и для разработчиков расширений было бы всё это реализовать на Plain old good C. Тем более что если там вчитаться в код — всё с Plain C и начинается: библиотека экспортирует всего 3 обычные __stdcall — функции. Но первая возвращает указатель на класс. Который еще три наследует. Короче, какое-то «горе от ума» там.
      +4

      Я бы не использовал конструкцию вида str(type(obj)) == "<type 'PyIDispatch'>", такой код, как мне кажется, выглядит немного лучше:


      from win32com.client.dynamic import PyIDispatchType
      
      if isinstance(obj, PyIDispatchType):
          ...

      Второй момент — чтобы экспортируемое имя не было испорчено, оберните его в extern "C" { ... }

        0
        Большое спасибо, учту
        0
        проще было бы 1С с web сервисом соединить, если это, конечно, не 7.7
          0
          Для 7.7 есть Веб-расширение. Продаётся совсем отдельно за многоденег, давно не поддерживается (как и вся ветка 7.7, впрочем), иногда радует странными багами, но на его основе в своё время делали много интересного.
          По сути позволяет выполнять произвольный 1С код внутри ASP страниц на IIS.
          +1
          Единственное, что может еще остаться непонятным — как передать данные из dll в 1C
          Там тоже весело. Не помню что там насчёт COM, но насколько я понял Native API делали «по мотивам». Так вот, там надо сначала получить указатель на аллокатор памяти самой 1С, и возвращаемые значения должны возвращаться, сорри за тавтологию, исключительно по указателю на область памяти, аллоцированную этим аллокатором, сорри за тавтологию еще раз, не я это придумал.
            0
            Помимо веб-сервиса

            Вот его и нужно было юзать из 1С
              0

              Использую для 1С Native компонент очень удобную обертку, а тут костыли какие то с неочень нужным здесь Python.

                0
                Костыль пришлось делать, т.к. время поджимало. Решение понятное и простое, а писать NativeApi самому у меня не получилось. Попробую собрать под MinGW с данным wrapper`ом, если взлетит буду очень вам благодарен)
                0
                Для библиотек со стороны, без вариантов, надо расколдовывать mangle, который не стандартизован (по очевидным причинам). Примеры, как может быть: https://en.wikipedia.org/wiki/Name_mangling#How_different_compilers_mangle_the_same_functions

                А для своих библиотек правильне было бы использовать extern «C».
                  +1
                  Вот даже не знаю — надо было писать каммент или нет.
                  Но вот потом берешь базу после татких писателей, и тихо тихо плачешь.
                  Ничего личного, каждый имеет право на свои костыли.
                  Но, после таких извратов и начинаешь страдать.
                  Если бы вы просто немного, совсем чуть-чуть воспользоватлись гуглом, ты вы бы увидели готовые компоненты на гите, причем сразу и под x64, Linux,Android и т.д.
                  Вы даже не представлете себе какой трешь и старье — этот самый ком.

                  Просто обидно за свою сферу. Никто не скажет что у 1с есть полноценный oData протокол, интегрируй сколько влезет. Нет, зачем? Мы сайт будет интегрировать через xml, даже не json.
                  Мы не будет использовтаь rest full api на стороне 1с, для сброса заказов. Потому что разработчик интернет магазина где то увидел, что 1с умеет только в SOAP.

                  Короче, вот такие статьи и делают из 1С какое то убожество :(

                    0
                    К слову, как раз понемногу планирую статью на тему почему не нужно oData в 1с использовать для интеграций, только для разовых выгрузок и анализов) А лучше написать свой отдельный http/web сервис) Правда вряд ли раньше нового года ее напишу наверно, если не позже))
                      0
                      Ну попробуйте удивить человека, у которого уже около 5 интернет магазинов по одата работают.
                      Только рассчет скидки и выгрузка заказа идет через json и rest, так как иначе надо сильно много лишнего рассказывать вебам
                        0
                        На самом деле не исключаю что в вашем случае это работает или преимущества метода перевешивают недостатки для вас, но для многих кейсов лучше именно самописный rest сервис сделать. Я даже себе записал Вам ссылку скинуть когда допишу статью, правда раньше НГ точно не ждите.
                      0
                      Вот да! Блин, один заголовок чего стоит: «Интеграция с DLL»! О, да, бэйби!

                      Зачем тут питон, если уж есть либа на С, так и делайте компонент 1С на Си, как все нормальные люди по нормальной документации, будет и кроссплатформенно и без прослойки в виде питона… НО неет же

                    Only users with full accounts can post comments. Log in, please.