Для тех, кто на минуту хочет забыть про средиземье, эффективных манагеров и прочие эфемерные сопли про джаваскрипт — Back to the roots! К дикому коду, сырым данным и хардкорному C++.
Предлагаю вашему внимаю краткое пошаговое руководство по подключению библиотеки tool1cd к 1С через внешнюю компоненту. Предполагается, что вы уже умеете разрабатывать внешние компоненты по типу Native, разбираетесь в C++, умеете готовить CMake, а слова «собрать Boost» вызывают у вас приступ иронии и боли, примерно так:
Приведённый в статье пример разрабатывался в следующих условиях:
- Версия платформы: 8.3.10.2667 х32
- Под Windows 10 Home: Microsoft Visual Studio 2024 Community Edition с модулем CMake
- Под Ubuntu 16.04: CLion 2024.3 (встроенная поддержка CMake), g++ 5.4
- Boost: 1.65
- ZLIB: 1.2.8
Пара слов про ограничения выше. Версия платформы значения не имеет: пример будет работать под 8.2 и 8.3 любых версий. Среда разработки может быть любая, лишь бы поддерживала формат проектов CMake: CLion, QtCreator, консоль. Буст: версия 1.66 и выше поддерживается только в CMake начиная с версии 3.11.0, поэтому проверьте версию. Поддержка CMake 3.11 в Visual Studio появилась буквально на днях в версии 15.7 — обновитесь. Если нет возможности обновиться, используйте Буст версии 1.65 и ниже.
Часть 0. Подготовка каркаса
В качестве подготовки надо скачать ZLib и Boost указанных версий и собрать (вы ведь умеете это делать, да?). Буст должен быть собран с параметрами “link=static runtime-link=static”. Все мы помним статью на ИТС и её совет включать зависимости статически? Вот это как-раз для этого.
Собрали? Теперь переходим к вкусному. Создадим компоненту, которая будет делать нечто невообразимое простейшее – получение сведений о конфигурации из файла 1CD: создадим функцию ПолучитьДанныеОФайле / GetFileInfo, которая на вход получает строку – путь к файлу, а на выходе отдаёт строку с описанием конфигурации или число с кодом ошибки (как упрощение).
Итак, скачиваем с ИТС архив VNCOMPS.ZIP, распаковываем, заходим в каталог examples (не template) и вашим любимым гит-клиентом клонируем в этот каталог URL https://github.com/e8tools/tool1cd.git .
Запускаем студию и открываем CMake-проект в каталоге examples. Укажем необходимые параметры для CMake (CMake – Изменить параметры CMake – AddIn):
- Нам нужна конфигурация x86-Release
- Убедимся, что inheritEnvironments = msvc_x86
- Убедимся, что configurationType установлен во что угодно помимо Debug
- В разделе variables необходимо указать настройки:
TARGET_PLATFORM_32 |
YES |
BOOST_ROOT |
Корневой каталог, где лежит Буст |
BOOST_LIBRARYDIR |
В Бусте подкаталог stagelib |
BOOST_INCLUDEDIR |
Корневой каталог Буста |
ZLIB_LIBRARY |
Полный путь к библиотеке zlibstatic.lib |
ZLIB_INCLUDE_DIR |
Корневой каталог zlib |
NOGUI |
YES |
Boost_USE_STATIC_LIBS |
YES |
Boost_USE_STATIC_RUNTIME |
YES |
Теперь подключим библиотеку tool1cd к нашему CMake-проекту. Открываем CMakeLists.txt в корне и после условия if (TARGET_PLATFORM_32) … endif() дописываем немножко волшебства:
if (MSVC)
foreach (flag_var
CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
if (${flag_var} MATCHES "/MD")
string (REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
endif (${flag_var} MATCHES "/MD")
endforeach(flag_var)
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /NODEFAULTLIB:MSVCRT")
endif()
Коротко опишу, что здесь мы подменяем сборку с динамическим рантаймом на сборку со статическим (про статическое включение зависимостей помним, да?).
Потом убираем из сборки проекты AddInChrome и AddInIE, чтобы они сейчас нам не мешали, и в самом конце дописываем заветную строчку:
add_subdirectory(tool1cd/src/tool1cd)
Тем самым мы включаем в свой проект исходные коды библиотеки tool1cd и будем их собирать вместе с компонентой. Дико? Дико. Но на данном этапе жизни проекта tool1cd – это самый безболезненный способ избежать подводных камней.
Теперь открываем CMakeLists.txt в каталоге NativeAPI. Там нам надо подключить Буст и tool1cd, дописываем в конце:
find_package (Boost 1.53 REQUIRED COMPONENTS filesystem)
include_directories(${Boost_INCLUDE_DIRS})
target_link_libraries (${PROJECT_NAME} ${Boost_LIBRARIES})
include_directories(${CMAKE_SOURCE_DIR}/tool1cd/src/tool1cd)
target_link_libraries(${PROJECT_NAME} tool1cd)
а также после SET(AddInNative_SRC) дописать:
if (MSVC)
set (AddInNative_SRC ${AddInNative_SRC} AddInNative.def)
endif()
Запускаем сборку (CMake – Собрать всё), убеждаемся, что нет ошибок, и идём дальше.
Часть 1. Создаём полезную нагрузку
Добавим новую функцию: добавить в enum Methods, g_MethodNames, g_MethodNamesRu, GetNParams, HasRetVal – это всё за рамками статьи.
Добавим в класс CAddInNative функцию GetFileInfo с сигнатурой, совпадающей с CallAsFunc (копипастим объявление). В коде функции CallAsFunc допишем вызов новой функции:
case eMethGetFileInfo:
{
try {
return GetFileInfo(lMethodNum, pvarRetValue, paParams, lSizeArray);
}
catch (std::exception &exc) {
// TODO: сделать addError
TV_VT(pvarRetValue) = VTYPE_I4;
TV_I4(pvarRetValue) = 1;
}
return true;
}
Перенаправляем вызов в новую функцию и ловим все исключения, создавая код возврата 1.
Перейдём к реализации функции GetFileInfo. Вкратце, Алгоритм действий:
-
-
- Открываем файл с базой
- Находим таблицу CONFIG
- В таблице CONFIG находим запись с FILENAME=”root”, считываем BINARYDATA
- Извлекаем из BINARYDARA гуид конфигурации
- В таблице CONFIG находим запись с FILENAME=гуид_конфигурации, считываем BINARYDATA
- Извлекаем из BINARYDATA данные о конфигурации
-
Подключим заголовочные файлы:
#include <boost/filesystem.hpp>
#include <Class_1CD.h>
#include <TableIterator.h>
Чтобы открыть файл базы, нам нужно извлечь путь к базе из параметра. Возьмём за образец готовый кусок кода из eMethLoadPicture и немножко доработаем:
boost::filesystem::path filepath;
// переделанный кусок кода из eMethLoadPicture
{
if (!lSizeArray || !paParams)
return false;
switch (TV_VT(paParams))
{
case VTYPE_PSTR:
filepath = boost::filesystem::path(std::string(paParams->pstrVal));
break;
case VTYPE_PWSTR:
{
wchar_t *wsTmp = nullptr;
::convFromShortWchar(&wsTmp, TV_WSTR(paParams));
filepath = boost::filesystem::path(std::wstring(wsTmp));
delete[] wsTmp;
break;
}
default:
return false;
}
}
// теперь в filepath у нас путь к файлу.
Открываем базу:
// заранее заготовим тип Число для возврата ошибки
TV_VT(pvarRetValue) = VTYPE_I4;
// теперь в filepath у нас путь к файлу.
if (!boost::filesystem::exists(filepath)) {
TV_I4(pvarRetValue) = 2;
return true;
}
T_1CD db(filepath, nullptr, false);
if (!(db.is_open() && db.is_infobase())) {
TV_I4(pvarRetValue) = 3;
return true;
}
Находим таблицу CONFIG:
// находим таблицу CONFIG
Table *cfg = nullptr;
{
for (int i = 0; i < db.get_numtables(); i++) {
Table *tbl = db.get_table(i);
if (System::EqualIC(tbl->get_name(), "Config")) {
cfg = tbl;
break;
}
}
if (cfg == nullptr) {
// не нашли :(
TV_I4(pvarRetValue) = 4;
return true;
}
}
Находим root, считываем BINARYDATA и разбираем ГУИД конфигурации:
std::string guid;
{
// находим root
TableIterator it(cfg);
while (!it.eof()) {
std::string filename = it.current().get_string("FILENAME");
if (!System::EqualIC(filename, "root")) {
it.next();
continue;
}
// нашли - считываем данные
Field *f_BinaryData = cfg->get_field("BINARYDATA");
System::Classes::TStream *data;
if (!it.current().try_store_blob_data(f_BinaryData, data, true)) {
TV_I4(pvarRetValue) = 4;
return true;
}
// поток data содержит текстовое представление дерева (скобочные данные)
data->Seek(3, soFromBeginning); // пропускаем первые 3 байта - BOM
// извлекаем гуид
std::unique_ptr<Tree> tree = parse_1Cstream(data, "");
guid = (*tree)[0][1].get_value();
break;
}
if (guid.empty()) {
TV_I4(pvarRetValue) = 5;
return true;
}
}
Последний шаг, ищем файл с описанием конфигурации и извлекаем нужные данные:
{
TableIterator it(cfg);
while (!it.eof()) {
std::string filename = it.current().get_string("FILENAME");
if (!System::EqualIC(filename, guid)) {
it.next();
continue;
}
// нашли нужный файл, извлекаем данные
Field *f_BinaryData = cfg->get_field("BINARYDATA");
System::Classes::TStream *data;
if (!it.current().try_store_blob_data(f_BinaryData, data, true)) {
TV_I4(pvarRetValue) = 6;
return true;
}
// поток data содержит текстовое представление дерева (скобочные данные)
data->Seek(3, soFromBeginning); // пропускаем первые 3 байта - BOM
std::unique_ptr<Tree> tree = parse_1Cstream(data, "");
// цепочки цифр вычислены опытным путём
// широкомасштабного обследования конфигураций не проводилось
std::string config_name = (*tree)[0][3][1][1][1][1][2].get_value();
std::string config_ver = (*tree)[0][3][1][1][15].get_value();
// тут мы получили нужный нам итог в кодировке utf-8
std::string utf8result = config_name + ", " + config_ver;
// во имя кроссплатформенности, нужно перевести его в utf-16
std::vector<uint8_t> result = System::SysUtils::TEncoding::Unicode->fromUtf8(utf8result);
m_iMemory->AllocMemory((void**)&pvarRetValue->pwstrVal, result.size());
memcpy((void*)pvarRetValue->pwstrVal, result.data(), result.size());
// меняем результат на Строка и успешно завершаем работу
TV_VT(pvarRetValue) = VTYPE_PWSTR;
pvarRetValue->wstrLen = result.size() / sizeof(WCHAR_T);
return true;
}
}
// сюда доходим в случае, если не нашли файл метаданных конфигурации
TV_I4(pvarRetValue) = 7;
return true;
На этом наша функция готова.
Тестовый код для 1С:
ПутьККомпоненте = "C:Usersdmpas...RelWithDebInfoAddInNativeWin32.dll";
Если Не ПодключитьВнешнююКомпоненту(ПутьККомпоненте, "CDReader", ТипВнешнейКомпоненты.Native) Тогда
Сообщить("Ошибочка");
Возврат;
КонецЕсли;
ФайлБазы = "C:1C...1Cv8.1CD";
Читалка = Новый("AddIn.CDReader.AddInNativeExtension");
Сообщить("GFI = " + ЗначениеВСтрокуВнутр(Читалка.ПолучитьДанныеОФайле(ФайлБазы)));
Если что-то не заработало, внимательно читаем ещё раз и задаём вопросы в комментариях.
Часть 2. Что дальше?
Перед тем, как рассказать, что будет дальше, я расскажу, что было раньше.
Год назад, в апреле 2024, автор проекта Валерий Агеев, опубликовал исходный код своей программы, что должно было стать толчком к её дальнейшему развитию. Для того, чтобы привлечь к разработке больше заинтересованных разработчиков, исходный код должен соответствовать определённым требованиям, некоему «духу опенсурса». Для программ на C++ под этим обычно подразумевают свежий стандарт языка, поддержка разных компиляторов и переносимость на другие платформы. С учётом всех этих факторов исходный формат проекта – C++ Builder – совершенно нас не устраивал и мы, закатав рукава, взялись за адаптацию. Вот теперь, спустя год, мы можем похвастаться:
- Проект отвязан от C++ Builder и от Windows. Отныне можно работать также под Ubuntu и MacOS, используя любую среду разработки, которая поддерживает CMake.
- Основной функционал выделен в библиотеку. Что как-раз представлено в статье: есть библиотека tool1cd, а к ней уже можно прикручивать свои оболочки.
- Настроена инфраструктура проекта. Сборка, тестирование, поддержка через гиттер и гитхаб
Соответственно, что дальше — очевидно. Клонируйте проект, дорабатывайте функционал, присылайте ваши доработки!
Сейчас проект в статусе Беты, нам крайне важны ваша помощь и обратная связь.
Эфемерные сопли про джаваскрипт!!!
Начало потрясное просто.
Позволю пару слов уточнения для тех, кто не понял что это было… Знаменитая утилита tool1cd Валерия Агеева (R.I.P.) для работы с файловыми базами 1С, в том числе для их спасения, когда они не открываются ничем кроме tool1cd — теперь доступна в виде исходных кодов на c++, доступна на linux и macos (вероятно)
Теперь если вам нужно прочесть файловую базу 1С в вашем приложении на плюсах (или любом другом языке, который умеет делать вызовы в нативный код, т.е. практически любом языке) — вы можете взять этот проект и использовать.
Спасибо ребята!
.. и вы почему-то работаете «программистом 1С»
Понимаю, что статья писалась в основном, чтобы рассказать об успех в наведении порядка в кодовой базе tool1CD, но местная публика, очевидно, ждет готовые бинарники графической утилиты, либо собранную native API обвязку.
(2) таки пока мы в полной бете, то из местной публики хочется выцепить разрабов-плюсовиков — кто готов мириться с неидеальностью бытия. Как выйдем из беты — там будут и бинари, и нескучные обои.
Очень круто!
Вышли из тени наконец-то 🙂 ?
Плюсиков маловато пока…
(5) ты погоди, скоро начнут рассказывать, что это не нужно и что мы фигнёй занимаемся 🙂
(6)
Это очень нужно. Памятник при жизни из чистого золота в полный рост 🙂
(7)
боюсь, немного не успели.
(3) а GUI-бинарники в виде беты может тоже выложить? Пусть фидбек хотя бы люди дадут, ошибки на ГХ накидают…
Эгеей!! Сотрудники фирмы 1С! Мы знаем, что вы читаете инфостарт, и что вы плюсовики. Даешь контрибьютинг инкогнито! #ДиджиталРезистанс! 🙂 🙂
(9) виндовые гуй-бинари есть на гитхабе:https://github.com/e8tools/tool1cd/releases
На инфостарт я их побоялся выкладывать — тут уже один раз баннили такое.
(9)
эмм… чую, первым пулл-реквестом будет проверка на наличие лицензии 🙂
(11) Ну докажут свою троллепригодность, как минимум. Уже хорошо
(12)
вот когда появится конфа «1С:Восстановление файловых баз», вот тогда это будет истинное 1С:Трололо.
(13) если там будет внешняя компонента из статьи, то да
(3) а с каким форматом баз работает компонента ? 8.3.8 поддерживает ?
(15)
да, поддерживает. От 8.2.14 (форматы ниже не проверял, может и работают) до 8.3.8 (с ним могут быть косяки, но пока они не встречались).
(16) не редко приходится восстанавливать файловые базы , для этого использую связку Tool1d , и библиотеку 1сd_lib ( тоже внешняя компонента так же открывает базу и читает данные + там организована взаимосвязь между метаданными базы и Таблицами, так же в отличии от tools1cd позволят удалять сразу несколько таблиц) , но работает только со старым форматом , что не очень удобно. Есть один алгоритм восстановления базы от ошибки «ошибка формата потока» который хотелось автоматизировать , но так как я не очень знаком с C++ хотелось бы узнать возможно ли с помощью вашей компоненты на уровне 1с осуществить удалять таблицы А так же импортировать и Экспортировать данные таблиц целиком ?
(17)
с помощью компоненты, что в статье — нет. это просто пример использования библиотеки. Библиотека, да, имеет функционал экспорта/импорта таблиц, их удаления — эти возможности надо ещё проверить и обкатать, потому сейчас я их не показываю. Опишите словами алгоритм и я, возможно, попробую его воспроизвести в одной из следующих статей.
(18) алгоритм таков
1) берётся чистая база того же релиза что и поврежденная , из неё удаляются все таблицы данных кроме служебных , за исключением таблиц Params , и DBSHEMA( которые будут перенесены из служебной базы ) имена служебных таблиц известны , желательно перед удалением экспортировать таблицы в виде файлов в каталог ( в Tools1cd кнопка экспорт таблиц данных )
2) из повреждённой базы выгружаются только таблицы данных , + 2 служебные таблицы Params и DBShema (тоже в виде файлов)
3) далее в чистую базу грузятся сначала служебные таблицы , а потом таблицы данных ( в tools кнопка — Импорт и создание таблиц)
В принципе все , единственный момент , что таблицы данных могут быть битыми ( например недавно столкнулся , в одной из таблиц файл BLOB весил 300 МБ ) , и при загрузке Tools валился с ошибкой , и надо было загружать таблицы по несколько шт. что бы отловить на какой происходит ошибка. А для исправления ситуации приходилось искать такую же таблицу из выгруженные таблиц чистой базы , и загружать ее с подменой файла описания таблицы descr из повреждённой базы ( т.е . Грузилась чистая таблица ) .
Надеюсь , что описал понятно
(19) да, спасибо. Попробую воспроизвести этот сценарий.
(20) И Вам спасибо , надеюсь получится
Вот это действительно стоящая статья!!! Побольше бы таких на данном ресурсе!!!
(22) Спасибо! Рад, что понравилось.
(8)
Прошу прощения, но что значит «не успели»?
«`
if (MSVC)
set (AddInNative_SRC ${AddInNative_SRC} AddInNative.def)
endif()
«`
В шаблоне не указано подключение дефа? а как тогда dll работает? или шаблон предназначен не для CMake, он просто рядом что ли?
(25)
В шаблоне для DLL, насколько я помню, предполагается делать по-старинке — через проект Студии. Там всё прописано.