Прежде всего хочется написать тут ДИСКЛЕЙМЕР!!!
Я не умею писать на плюсах, не умею писать на питоне, я просто любознательный. Если у вас кровь бежит из глаз при просмотре моего кода, напишите в комментариях, как надо писать правильно.
Спасибо!
Ну теперь можно и начать. Все, что нам понадобится сегодня:
Устанавливаем питон и открываем шаблон компоненты в CLion. Во-первых, нас будет интересовать файл CMakeLists.txt. Нам нужно указать, что в проекте будет использована библиотека питона для С. Для этого вписываем такие строки в файл:
// тут какой-то код который был написан до нас
// это тоже код, который писали не мы, просто показываю после чего я воткнул свою вставочку
add_library(${TARGET} SHARED
${SOURCES})
// Это наш код для поиска библиотек питона
find_package(Python3 3.12 REQUIRED COMPONENTS Development)
if (Python3_FOUND)
message("Python include directory: " ${Python3_INCLUDE_DIRS})
include_directories(${Python3_INCLUDE_DIRS})
target_link_libraries(${TARGET} ${Python3_LIBRARIES})
endif (Python3_FOUND)
// тут дальше есть какой-то код, который сейчас нам не очень то интересен
Отлично, с самым сложным разобрались. Идем дальше.
Есть тут такие файлы, которые называются SapmleAddIn.h и SapmleAddIn.cpp. Мы конечно же захотим, чтобы наша компонента называлась по-другому. Нажимаем SapmleAddIn.h в CLion левой клавишей мыши, выбираем Refactor->Rename и называем по-своему. У меня это будет Component4Python. Готово, все вызовы в коде, а также файл .cpp изменили название. Вернемся в файл CMakeLists.txt и в самом верху поменяем название:
project(Component4Python)
set(TARGET Component4Python)
и в файле addin.def:
LIBRARY "Component4Python"
С подготовкой закончено, идем ваять свою компоненту. В чем сама суть? В коде плюсов мы вызываем интерпритатор Python и даем ему команды, что импортировать, что вызвать.
Последовательность простая:
- Инициализация
- Полезная нагрузка
- Деинициализация
С полезной нагрузкой разберемся позднее. С инициализацией и вот этим сложным словом в последнем пункте, не все так просто. Если почитать доку, можно увидеть, что:
Некоторые расширения могут работать некорректно, если их процедура инициализации вызывается более одного раза; это может произойти, если приложение вызывает Py_Initialize()
и Py_FinalizeEx()
более одного раза.
То есть вызывать Py_Initialize()
и Py_FinalizeEx()
мы должны только один раз. Где это удобнее всего сделать? Конечно же при создании и уничтожении компоненты. За эти события отвечает файл exports.cpp:
long GetClassObject(const WCHAR_T *clsName, IComponentBase **pInterface) {
if (!*pInterface) {
auto cls_name = std::u16string(reinterpret_cast<const char16_t *>(clsName));
if (cls_name == u"Component4Python") {
*pInterface = new Component4Python;
// Вот тут инициализируем
Py_Initialize();
}
return (long) *pInterface;
}
return 0;
}
long DestroyObject(IComponentBase **pInterface) {
if (!*pInterface) {
return -1;
}
// Вот тут сделаем то сложное слово из последнего пункта
Py_FinalizeEx();
delete *pInterface;
*pInterface = nullptr;
return 0;
}
Но боюсь можно отхватить по голове за то, что в момент подключения или уничтожении компоненты, платформа может упасть. Причем, платформа особо не будет говорить ничего вменяемого, кроме "Аварийная ошибка" или "Произошла неисправимая ошибка". Так что забудем весь этот ужас и создадим 2 метода нашей компоненты. Пойдем в файл Component4Python.h и объявим о намерении иметь следующие методы:
#ifndef QRCode41ADDIN_H
#define QRCode41ADDIN_H
#include "Component.h"
// Подключим заголовочный файл питона
#include "Python.h"
class Component4Python final : public Component {
public:
const char *Version = u8"1.0.0";
Component4Python();
private:
std::string extensionName() override;
variant_t isInitialized;
void initializePython();
void uninitializePython();
void initMethods();
void initProperty();
};
#endif //QRCode41ADDIN_H
И приступим к реализации наших методов в файле Component4Python.cpp
std::string Component4Python::extensionName() {
return "Component4Python";
}
Component4Python::Component4Python() {
initProperty();
initMethods();
}
void Component4Python::initProperty() {
AddProperty(L"Version", L"ВерсияКомпоненты", [&]() {
auto s = std::string(Version);
return std::make_shared<variant_t>(std::move(s));
});
AddProperty(L"isInitialized", L"ПитонВключен", [&]() {
return std::make_shared<variant_t>(isInitialized);
});
}
void Component4Python::initMethods() {
AddMethod(L"initializePython", L"ВключитьПитон", this, &Component4Python::initializePython);
AddMethod(L"uninitializePython", L"ВыключитьПитон", this, &Component4Python::uninitializePython);
}
void Component4Python::uninitializePython() {
Py_Finalize();
if (Py_IsInitialized()) {
throw std::runtime_error(u8"Ошибка выключения Python.");
}
isInitialized = false;
}
void Component4Python::initializePython() {
Py_Initialize();
if (!Py_IsInitialized()) {
throw std::runtime_error(u8"Ошибка инициализации Python.");
}
isInitialized = true;
}
Таким образом, мы сможем импортировать библиотеки, установленные на компьютере и вызывать скрипты на python. НО! Ходят слухи, что это плохая практика. Лучше создать виртуальное окружение для питона и хранить необходимые либы в этом окружении. Ну что, давайте так и сделаем. Сделать это можно не только лишь из командной строки (а я обычный 1Сник, я люблю тыкать кномпачки и чтобы все по волшебству работало), но и в IDE CLion. Для этого открываем Settings->Build->Python interpreter, нажимаем Add Interpreter, выбираем директорию, где это окружение будет обитать и нажимаем OK.
В заголовочном файле Component4Python.h объявим еще одну переменную:
И перепишем реализацию некоторых методов:
void Component4Python::initProperty() {
AddProperty(L"Version", L"ВерсияКомпоненты", [&]() {
auto s = std::string(Version);
return std::make_shared<variant_t>(std::move(s));
});
AddProperty(L"isInitialized", L"ПитонВключен", [&]() {
return std::make_shared<variant_t>(isInitialized);
});
AddProperty(L"pathToVenv", L"ПутьКВиртуальномуОкружению", [&]() {
return std::make_shared<variant_t>(pathToVenv);
}, [&](const variant_t &value) {
pathToVenv = value;
});
}
void Component4Python::initializePython() {
// Преобразуем тип variant_t в string
auto ccPath = std::get<std::string>(pathToVenv).c_str();
// если переменная пустая, просто инициализируем питон
if (!ccPath) {
Py_Initialize();
} else {
// создаем конфигурацию
PyConfig config;
// говорим, что запускаем конфигурацию изолированно
PyConfig_InitIsolatedConfig(&config);
// приколы языка С. Строк в С нету, зато есть 100500 вариантов хранения строк
// в данный момент мы вытащили string из variant_t, но конфигурация готова принять
// строку только в варианте wchar_t. Тут мы все это преобразовываем
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
auto wsPath = converter.from_bytes(ccPath);
auto venv_path = new wchar_t[wsPath.size() + 1];
std::copy(wsPath.begin(), wsPath.end(), venv_path);
venv_path[wsPath.size()] = L'\0';
// Устанавливаем переданный путь в конфигурацию
PyConfig_SetString(&config, &config.executable, venv_path);
// инициализируем с конфигурацией
auto status = Py_InitializeFromConfig(&config);
PyConfig_Clear(&config);
// обработаем ошибку
if (PyStatus_Exception(status)) {
auto ss = std::stringstream();
auto msg = u8"Ошибка инициализации Python.";
ss << msg << u8" status.func: " << (status.func ? status.func : "N/A") << u8" status.err_msg: "
<< (status.err_msg ? status.err_msg : "N/A");
throw std::runtime_error(ss.str());
}
}
// проверим состояние
if (!Py_IsInitialized()) {
throw std::runtime_error(u8"Ошибка инициализации Python.");
}
isInitialized = true;
}
То есть, теперь мы можем сказать компоненте путь до виртуального окружения. Если же окружение не установлено, компонента найдет python, установленный в системе.
Еще один момент, тут использованы исключения throw. Вообще API 1С говорит что все ошибки надо обрабатывать через метод AddError. Последним параметром этот метод принимает булево, которое говорит, надо прерывать код или нет. Вот у меня не получилось через этот метод прервать код, поэтому решил просто кидать исключение, 1С это прекрасно принимает.
Давайте перейдем уже к вызову питоновских методов. Для примера, я хочу вызывать метод, который возвращает мне QR код из строки. QR будет возвращен в виде картинки, нам надо будет его преобразовать в байты и вернуть в 1С. В 1С мне лень городить обработки, я буду все выполнять в консоли кода "Инструментов разработчика" от Tormozit.
Для простого примера я хочу выполнить вот такой код Python:
Какой-то простой код Python
import qrcode
import io
img = qrcode.make('Some data here')
bytes_io = io.BytesIO()
img.save(bytes_io)
return bytes_io.getvalue()
Передаем строку в библиотеку QR на питоне, сохраняем в поток данных и возвращаем двоичные данные. На 1С код будет выглядеть вот так:
Если Не ПодключитьВнешнююКомпоненту(Путь, "Vissarion", ТипВнешнейКомпоненты.Native) Тогда
Сообщение = "Не удалось подключить";
КонецЕсли;
Компонента = Новый("AddIn.Vissarion.Component4Python");
Если Компонента = Неопределено Тогда
Сообщение = "Не удалось создать";
КонецЕсли;
Компонента.ПутьКВиртуальномуОкружению = "E:\PythonVenvs\Component4Python\Scripts\python.exe";
Компонента.ВключитьПитон();
Данные = Компонента.СоздатьПростойQR("Привет, мир!");
Картинка = Новый Картинка(Данные);
Компонента.ВыключитьПитон();
Компонента = "";
Из кода на 1С видно, что мы должны вызвать метод СоздатьПростойQR, которого в компоненте еще нет. Идем писать его, начиная с заголовочного файла:
#ifndef QRCode41ADDIN_H
#define QRCode41ADDIN_H
#include "Component.h"
#include "Python.h"
class Component4Python final : public Component {
public:
const char *Version = u8"1.0.0";
Component4Python();
private:
std::string extensionName() override;
variant_t isInitialized;
variant_t pathToVenv;
void initializePython();
void uninitializePython();
void initMethods();
void initProperty();
// Наш метод СоздатьПростойQR
variant_t makeSimpleQR(const variant_t &data);
// метод для работы с исключениями
static void exceptionHandler(std::shared_ptr<std::string> msg);
// метод для преобразования байтов питона в двоичные данные 1С
static std::shared_ptr<std::vector<char>> imgToBytes(PyObject *img);
};
#endif //QRCode41ADDIN_H
И реализацию:
void Component4Python::initMethods() {
AddMethod(L"initializePython", L"ВключитьПитон", this, &Component4Python::initializePython);
AddMethod(L"uninitializePython", L"ВыключитьПитон", this, &Component4Python::uninitializePython);
AddMethod(L"MakeSimpleQR", L"СоздатьПростойQR", this, &Component4Python::makeSimpleQR);
}
void Component4Python::exceptionHandler(std::shared_ptr<std::string> msg) {
// получаем инфу об исключении внутри питона
auto error = PyErr_GetRaisedException();
if (!error) {
// если данных нет, просто кидаем исключение
throw std::runtime_error(msg->c_str());
} else {
// если данные есть, парсим их и выводим нормальную ошибку
auto error_str = PyObject_Str(error);
auto error_cstr = PyUnicode_AsUTF8(error_str);
auto ss = std::stringstream();
ss << msg->c_str() << u8" по причине: " << error_cstr;
Py_DECREF(error_str);
Py_DECREF(error);
throw std::runtime_error(ss.str());
}
}
std::shared_ptr<std::vector<char>> Component4Python::imgToBytes(PyObject *img) {
// импортируем модуль io
auto io = PyImport_ImportModule("io");
// если модуль не импортировался, то выводим сообщение об ошибке
if (!io) {
auto msg = std::make_shared<std::string>(u8"Ошибка при импорте модуля io");
exceptionHandler(msg);
}
// создаем объект BytesIO
auto bytesio_name = PyUnicode_FromString("BytesIO");
auto bytesio = PyObject_CallMethodNoArgs(io, bytesio_name);
// если объект не создался, то выводим сообщение об ошибке
if (!bytesio) {
auto msg = std::make_shared<std::string>(u8"Ошибка при создании объекта BytesIO");
exceptionHandler(msg);
}
// преобразуем имя метода save из строки в объект Python
auto save_method = PyUnicode_FromString("save");
// вызываем метод save объекта img передавая объект bytesio
PyObject_CallMethodOneArg(img, save_method, bytesio);
// преобразуем имя метода getvalue из строки в объект Python
auto getvalue_method = PyUnicode_FromString("getvalue");
// вызываем метод getvalue объекта bytesio
auto image_bytes = PyObject_CallMethodNoArgs(bytesio, getvalue_method);
// если метод не вернул объект, то выводим сообщение об ошибке
if (!image_bytes) {
auto msg = std::make_shared<std::string>(u8"Ошибка при получении байтового представления изображения");
exceptionHandler(msg);
}
// конвертируем объект bytes в std::vector<unsigned char>
char* buffer;
Py_ssize_t length;
PyBytes_AsStringAndSize(image_bytes, &buffer, &length);
auto result = std::make_shared<std::vector<char>>(buffer, buffer + length);
// удаляем объекты Python
Py_DECREF(image_bytes);
Py_DECREF(bytesio);
Py_DECREF(io);
return result;
}
variant_t Component4Python::makeSimpleQR(const variant_t &data) {
// импортируем модуль qrcode
auto qrcode = PyImport_ImportModule("qrcode");
if (!qrcode) {
auto msg = std::make_shared<std::string>(u8"Ошибка при импорте модуля qrcode");
exceptionHandler(msg);
}
// преобразуем имя метода make из строки в объект Python
auto make_method = PyUnicode_FromString("make");
// вызываем метод make передавая строку с данными
auto img = PyObject_CallMethodOneArg(qrcode, make_method, PyUnicode_FromString(std::get<std::string>(data).c_str()));
if (!img) {
auto msg = std::make_shared<std::string>(u8"Ошибка при создании изображения QR кода");
exceptionHandler(msg);
}
// конвертируем объект img в std::vector<unsigned char>
auto result = imgToBytes(img);
// удаляем объекты Python
Py_DECREF(img);
Py_DECREF(qrcode);
return *result;
}
Вот и сама начинка Python C API. Для того, чтобы вызвать import qrcode мы делаем вызов PyImport_ImportModule("qrcode"); ну и далее можно по названиям объектов и методов сопоставить код в питоне и код на плюсах. Что важно понимать, в API есть множество методов для вызова объектов/методов питона. В примере показан PyObject_CallMethodNoArgs если надо вызвать метод объекта без аргументов, PyObject_CallMethodOneArg для вызова метода объекта с одним параметром, но помимо этих есть еще куча методов. Например, создание объекта с передачей *args мы рассмотрим ниже.
Надо понимать, что каждый вызов метода python может вернуть результат выполнения кода или null, если что-то пошло не так. А значит, после каждого вызова нам надо делать проверку на null, попытаться получить причину возникновения ошибки, распарсить ее и вернуть в 1С (любителям в 1С возвращать результат или неопределено передаю большой привет).
Мы не можем передать строку из плюсов в питон, для передачи строки ее надо сначала привести в правильный тип. Этим занимается метод PyUnicode_FromString.
Функция Py_DECREF нужна для удаления неиспользуемых более объектов, т.н. чистка памяти, которую мы все очень боимся при работе на плюсах.
Кстати, я конечно не исключаю, что в моем коде есть утечки памяти, хоть я очень старался правильно объявлять объекты. Если найдете ошибку, добро пожаловать в комменты.
Пришло время собирать нашу компоненту и использовать ее в 1С.
Настроим сборочку. Идем в Settings->Build->Toolchains и добавляем Visual Studio:
Потом идем в Settings->Build->CMake и создаем debug и release сборки:
В CLion есть кнопочка сверху справа в виде молотка, называется она build и предназначена для сборки проекта.
Нажимаем на нее и ждем когда в логах внизу нам скажут что все ок:
Слева в дереве проекта мы сможем увидеть созданные при сборке файлы:
Нас конечно же интересует *.dll файл, нажмем на него правой кнопкой мыши и скопируем полный путь. В коде 1С, который я привел выше, этот путь надо поместить в переменную "Путь". Все, открываем 1С, пытаемся выполнить наш код и получаем ошибку:
То есть, питон явно говорит, что не может импортировать qrcode. Это сообщение обрабатывается в нашей компоненте и отдается в 1С. Правда здорово?
Опять же, код на 1С говорит, что мы запускаемся в виртуальном окружении, а значит такой библиотеки на питоне не хватает в этом окружении. Мòзги из JetBrains уже продумали, что и как надо делать в их IDE и дали нам возможность устанавливать пакеты питона в виртуальное окружение прямо из IDE.
Убедимся, что сейчас IDE настроена на наше виртуальное окружение, зайдем в Settings->Build->Python Interpreter
Путь должен быть точно таким, каким мы передаем в компоненту в коде 1С.
Далее в CLion слева выбираем Python Package:
в поиске забиваем qrcode и нажимаем install:
Снова запускаем код в 1С и получаем результат:
Мы использовали готовые библиотеки и смогли имитировать небольшой скрипт на питоне. Но что, если скрипт написан был не какими-то чуваками, а вами? А если там при создании объекта несколько параметров? Давайте рассмотрим такой вариант. Откроем PyCharm и создадим проект:
Так же мы должны сразу при создании проекта указать наше созданное виртуальное окружение.
Будем писать маленький валидатор json, который будет проверять наш json согласно переданной json schema.
Вот такую структуру проекта мы создадим:
init файл оставим в покое, он нам тут не особо понадобится. В файле validator будет вот такой код:
from jsonschema import validate
import json
class Validator:
def __init__(self, data, schema):
self.schema = json.loads(schema)
self.data = json.loads(data)
def validate(self):
validate(self.data, self.schema)
А в файле setup объявим наш пакет для установки:
from setuptools import setup, find_packages
setup(
name='validator',
version='0.1',
packages=find_packages(),
install_requires=[
'jsonschema',
],
)
Все, что нам остается сделать, это в терминале IDE выполнить команду "pip install ." для установки нашего нового пакета (подробнее о сборке пакетов можно узнать на Хабре):
И теперь будем дальше мучить нашу компоненту. Объявляем необходимые методы:
#ifndef QRCode41ADDIN_H
#define QRCode41ADDIN_H
#include "Component.h"
#include "Python.h"
class Component4Python final : public Component {
public:
const char *Version = u8"1.0.0";
Component4Python();
private:
std::string extensionName() override;
variant_t isInitialized;
variant_t pathToVenv;
void initializePython();
void uninitializePython();
void initMethods();
void initProperty();
variant_t makeSimpleQR(const variant_t &data);
static void exceptionHandler(std::shared_ptr<std::string> msg);
static std::shared_ptr<std::vector<char>> imgToBytes(PyObject *img);
variant_t validator(const variant_t &data, const variant_t &schema);
};
#endif //QRCode41ADDIN_H
Далее нам надо собрать параметры в массив, в том порядке, в котором параметры объявлены в методе питона и создать класс валидатор.
variant_t Component4Python::validator(const variant_t &data, const variant_t &schema) {
// проверяем параметры на заполненность
if (std::get<std::string>(data).empty() || std::get<std::string>(schema).empty()) {
auto msg = std::make_shared<std::string>(u8"Параметры data и schema должны быть заполнены");
exceptionHandler(msg);
}
// импортируем модуль validator
auto validator = PyImport_ImportModule("schema.validator");
if (!validator) {
auto msg = std::make_shared<std::string>(u8"Ошибка при импорте модуля validator");
exceptionHandler(msg);
}
// создаем класс Validator
auto validator_attr = PyObject_GetAttrString(validator, "Validator");
auto py_data = PyUnicode_FromString(std::get<std::string>(data).c_str());
auto py_schema = PyUnicode_FromString(std::get<std::string>(schema).c_str());
// тот самый массив, в который мы передаем первым параметром данные, а вторым схему
auto args = PyTuple_Pack(2, py_data, py_schema);
auto validator_class = PyObject_CallObject(validator_attr, args);
if (!validator_class) {
auto msg = std::make_shared<std::string>(u8"Ошибка при создании объекта класса Validator");
exceptionHandler(msg);
}
// вызываем метод validate класса Validator
auto validate_method = PyUnicode_FromString("validate");
PyObject_CallMethodNoArgs(validator_class, validate_method);
auto error = PyErr_GetRaisedException();
if (!error) {
// удаляем объекты Python
Py_DECREF(validator_class);
Py_DECREF(validator_attr);
Py_DECREF(validator);
Py_DECREF(args);
// если ошибки нет, тогда возвращаем ок
auto res = std::make_shared<std::string>("OK");
return *res;
} else {
// если ошибка есть, перехватываем ее в питоне и возвращаем в 1С
auto error_str = PyObject_Str(error);
auto error_cstr = PyUnicode_AsUTF8(error_str);
Py_DECREF(error_str);
Py_DECREF(error);
Py_DECREF(validator_class);
Py_DECREF(validator_attr);
Py_DECREF(validator);
Py_DECREF(args);
auto res = std::make_shared<std::string>(error_cstr);
return *res;
}
}
Мы просто будем вызывать валидацию, если будет валиться ошибка, тогда будем перехватывать ее и отправлять в 1С в строковом виде
Если Не ПодключитьВнешнююКомпоненту(Путь, "Vissarion", ТипВнешнейКомпоненты.Native) Тогда
Сообщение = "Не удалось подключить";
КонецЕсли;
Компонента = Новый("AddIn.Vissarion.Component4Python");
Если Компонента = Неопределено Тогда
Сообщение = "Не удалось создать";
КонецЕсли;
Компонента.ПутьКВиртуальномуОкружению = "E:\PythonVenvs\Component4Python\Scripts\python.exe";
Компонента.ВключитьПитон();
Схема = "{""type"": ""object"", ""properties"": {""name"": {""type"": ""string""}, ""age"": {""type"": ""number""}}}";
Данные = "{""name"": ""John"", ""age"": ""30""}";
Ответ = Компонента.Валидатор(Данные, Схема);
Если Не Ответ = "OK" Тогда
Сообщение = СтрШаблон("Произошла ошибка: %1", Ответ);
ПоказатьЗначение(,Сообщение);
КонецЕсли;
Компонента.ВыключитьПитон();
Компонента = "";
Запускаем и видим ошибку "Модуль не найден"
Очень много времени и сил я потратил на эту ошибку. В интернете по ней особо инфы нет ни у нас, ни за бугром. Оказалось все очень просто, мы собираем debug сборку, а rpds - это не скрипт на питоне, это библиотека, собранная под release. Можно упороться и собрать ее под debug, но я думаю это информация для отдельной статьи, а мы можем просто поменять сборку на release, собрать dll и подключить к 1С именно ее.
Запускаем:
Поговорим об отладке.
К сожалению, release сборку отладить мы не можем, а вот debug очень даже. Вернемся к методу формирования qr кода и поставим точку остановы на вызове
auto qrcode = PyImport_ImportModule("qrcode");
Запустим 1С и в CLion нажмем комбинацию CTRL+ALT+F5.
Выбираем 1С и нажимаем attach.
После этого запускаем формирование qr кода в 1С и попадаем в отладчик в CLion, где мы можем посмотреть какие объекты мы получаем. Либо можно в отладчике выполнить произвольный код и посмотреть вывод.
Итоги.
Конечно, это очень поверхностная статья. К тому же это API очень низкоуровневое, использовать его сложно. Но если вам эта тема интересна, если эта статья "зайдет" сообществу, я с удовольствием опишу другие варианты вызова python скриптов, или же покажу этот API более развернуто.
Спасибо за внимание!
Весь код можно посмотреть на гитхаб
Читайте другие мои статьи: