python

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

  • воскресенье, 2 июля 2017 г. в 03:13:54
https://habrahabr.ru/post/332082/
  • Python
  • C++


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