Привет Хабр! Недавно я разработал алгоритм для логистики, и нужно было его куда-то пристроить. Помимо веб-сервиса решено было внедрить данный модуль в 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 из статьи
Поделиться с друзьями
-->

Комментарии (10)


  1. RolexStrider
    01.07.2017 18:56

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


  1. mwizard
    01.07.2017 19:10
    +4

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


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

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


    1. atnes
      04.07.2017 20:36

      Большое спасибо, учту


  1. kxl
    01.07.2017 20:55

    проще было бы 1С с web сервисом соединить, если это, конечно, не 7.7


    1. BATC0H
      04.07.2017 20:36

      Для 7.7 есть Веб-расширение. Продаётся совсем отдельно за многоденег, давно не поддерживается (как и вся ветка 7.7, впрочем), иногда радует странными багами, но на его основе в своё время делали много интересного.
      По сути позволяет выполнять произвольный 1С код внутри ASP страниц на IIS.


  1. RolexStrider
    01.07.2017 22:09
    +1

    Единственное, что может еще остаться непонятным — как передать данные из dll в 1C
    Там тоже весело. Не помню что там насчёт COM, но насколько я понял Native API делали «по мотивам». Так вот, там надо сначала получить указатель на аллокатор памяти самой 1С, и возвращаемые значения должны возвращаться, сорри за тавтологию, исключительно по указателю на область памяти, аллоцированную этим аллокатором, сорри за тавтологию еще раз, не я это придумал.


  1. medvedevia
    03.07.2017 10:13

    Помимо веб-сервиса

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


  1. kolayuk
    04.07.2017 19:16

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


    1. atnes
      04.07.2017 20:39

      Костыль пришлось делать, т.к. время поджимало. Решение понятное и простое, а писать NativeApi самому у меня не получилось. Попробую собрать под MinGW с данным wrapper`ом, если взлетит буду очень вам благодарен)


  1. gaf
    04.07.2017 20:36

    Для библиотек со стороны, без вариантов, надо расколдовывать mangle, который не стандартизован (по очевидным причинам). Примеры, как может быть: https://en.wikipedia.org/wiki/Name_mangling#How_different_compilers_mangle_the_same_functions

    А для своих библиотек правильне было бы использовать extern «C».