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