Хочу показать, как можно разрабатывать внешние компоненты для 1С на Native API с помошью языка Rust, используя вот эту библиотеку. Пост разделен на 3 части, где я поэтапно разработаю компоненту, которая читает файл в двоичные данные, тесты для нее и make файл:
- Разработка
- Тестирование
- Сборка и упаковка
1. Разработка
Для разработки должны быть установлены:
- Компилятор - MSVC или gcc
- Rust - https://www.rust-lang.org/tools/install
- Toolchain'ы - https://rust-lang.github.io/rustup/concepts/toolchains.html
- Для MSVC: stable-x86_64-pc-windows-msvc и stable-i686-pc-windows-msvc
- Для gcc: stable-x86_64-pc-windows-gnu и stable-i686-pc-windows-gnu
- Редактор кода с плагином rust-analyzer (но, конечно, можно и в блокноте)
Для начала создадим пустой проект и установим необходимые пакеты:
> cargo init byte_reader_addin --lib > cd byte_reader_addin > cargo add native_api_1c utf16_lit
Изменим файл манифест Cargo.toml, добавив указание о том, что результатом компиляции будет dll:
# Cargo.toml .... [lib] crate-type = ["cdylib"] |
Вставим содержание нашей компоненты:
use std::{ fs::{metadata, File}, io::Read, sync::Arc, }; use native_api_1c::{ native_api_1c_core::ffi::connection::Connection, native_api_1c_macro::AddIn }; #[derive(AddIn)] pub struct ByteReader { #[add_in_con] // соединение с 1С для вызова внешних событий connection: Arc<Option<&'static Connection>>, // Arc для возможности многопоточности #[add_in_func(name = "ReadBytes", name_ru = "ПрочитатьБайты")] #[arg(Str)] #[returns(Blob, result)] read_bytes: fn(&Self, String) -> Result<Vec<u8>, Box<dyn std::error::Error>>, } impl ByteReader { pub fn new() -> Self { Self { connection: Arc::new(None), read_bytes: Self::read_bytes, } } pub fn read_bytes(&self, path: String) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut f = File::open(&path)?; let metadata = metadata(&path)?; let mut buffer = vec![0; metadata.len() as usize]; f.read(&mut buffer)?; Ok(buffer) } } |
Подробнее про структуру описания функции:
#[add_in_func(name = "ReadBytes", name_ru = "ПрочитатьБайты")] #[arg(Str)] #[returns(Blob, result)] read_bytes: fn(&Self, String) -> Result<Vec<u8>, Box<dyn std::error::Error>>, |
-
#[add_in_func(name = "ReadBytes", name_ru = "ПрочитатьБайты")] // Имена, по которым будем обращаться к функции из 1С
-
#[arg(Str)] // Тип параметра, который будет передан из 1С. Если будет // передан другой параметр, то будет вызвано исключение. // В данном случае путь к файлу в виде строки
-
#[returns(Blob, result)] // Тип возвращаемого параметра, который будет получен в 1С. // Обвернут в результат, т.к. фунцкия может быть выполнена // с ошибкой, но не с критичной.
-
read_bytes: fn(&Self, String) -> Result<Vec<u8>, Box<dyn std::error::Error>>, // описание функции фнутри Rust.
Подробнее про описание фходных параметров, возвращаемых параметров, а также свойств объекта компоненты можно посмотреть здесь.
Теперь попробуем собрать:
> cargo build --target=x86_64-pc-windows-gnu
Отлично, по пути target\x86_64-pc-windows-gnu\debug\byte_reader_addin.dll находится наша компонента. Попробуем подключить ее в 1С и проверить правильность работы. Для этого используем следующий код:
|
2. Тестирование
Rust предоставляет возможность тестирования из коробки, поэтому напишем простой тест, который проверяет корректность работы функций нашей компоненты. Создадим в корне файл dummy_file.txt и запишем туда что-нибудь. В моем случае это будет просто "ABC". Теперь в файле lib.rs в конец:
#[cfg(test)] mod tests { #[test] fn test_valid_file_path() { let byte_reader_obj = super::ByteReader::new(); let path = String::from("dummy_file.txt"); let result = byte_reader_obj.read_bytes(path); assert!(result.is_ok()); let bytes = result.unwrap(); assert_eq!(bytes.len(), 3); assert_eq!(&bytes, &[65, 66, 67]); } #[test] fn test_invalid_file_path() { let byte_reader_obj = super::ByteReader::new(); let path = String::from("dummy_file_2.txt"); let result = byte_reader_obj.read_bytes(path); assert!(result.is_err()); } } |
Запустим тестирование:
> cargo test ... running 2 tests test tests::test_invalid_file_path ... ok test tests::test_valid_file_path ... ok
3. Сборка и упаковка
Для сборки и упаковки будем использовать инструмент cargo make, который устанавливается так:
> cargo install --force cargo-make
Компоненту в рабочую базу 1С будем добавлять через макет, а для этого нужно упаковать ее в zip архив рядом с манифестом. Добавим в корень файл Manifest.xml:
|
Добавим в корень файл Makefile.toml:
[tasks.clean] command = "cargo" args = ["clean"] [tasks.remove-out.linux] command = "rm" args = ["-rf", "out"] [tasks.remove-out.windows] script_runner = "powershell" script_extension = "ps1" script = ''' Remove-Item -LiteralPath "out" -Force -Recurse -ErrorAction SilentlyContinue ''' [tasks.build-release-windows-32.linux] command = "cargo" args = ["build", "--release", "--target", "i686-pc-windows-gnu"] [tasks.build-release-windows-64.linux] command = "cargo" args = ["build", "--release", "--target", "x86_64-pc-windows-gnu"] [tasks.build-debug-windows-32.linux] command = "cargo" args = ["build", "--target", "i686-pc-windows-gnu"] [tasks.build-debug-windows-64.linux] command = "cargo" args = ["build", "--target", "x86_64-pc-windows-gnu"] [tasks.build-release-windows-32.windows] command = "cargo" args = ["build", "--release", "--target", "i686-pc-windows-msvc"] [tasks.build-release-windows-64.windows] command = "cargo" args = ["build", "--release", "--target", "x86_64-pc-windows-msvc"] [tasks.build-debug-windows-32.windows] command = "cargo" args = ["build", "--target", "i686-pc-windows-msvc"] [tasks.build-debug-windows-64.windows] command = "cargo" args = ["build", "--target", "x86_64-pc-windows-msvc"] [tasks.debug] run_task = { name = [ "build-debug-windows-32", "build-debug-windows-64", ], parallel = true } [tasks.release] run_task = { name = [ "build-release-windows-32", "build-release-windows-64", ], parallel = true } [tasks.pack-to-zip.linux] script = ''' mkdir -p out cp target/i686-pc-windows-gnu/release/byte_reader_addin.dll out/ByteReader_x32.dll cp target/x86_64-pc-windows-gnu/release/byte_reader_addin.dll out/ByteReader_x64.dll cp Manifest.xml out/ zip -r -j out/ByteReader.zip out/ByteReader_x64.dll out/ByteReader_x32.dll out/Manifest.xml ''' [tasks.pack-to-zip.windows] script_runner = "powershell" script_extension = "ps1" script = ''' New-Item -ItemType Directory -Path out -Force -ErrorAction SilentlyContinue cp target/i686-pc-windows-msvc/release/byte_reader_addin.dll out/ByteReader_x32.dll cp target/x86_64-pc-windows-msvc/release/byte_reader_addin.dll out/ByteReader_x64.dll cp Manifest.xml out/ Compress-Archive -DestinationPath out/ByteReader.zip -Path out/ByteReader_x64.dll, out/ByteReader_x32.dll, out/Manifest.xml -Force ''' [tasks.pack] dependencies = ["clean", "release", "remove-out", "pack-to-zip"] |
И запустим команду сборки:
> cargo make pack
На выходе получаем готовый архив, который можно добавлять в конфигурацию и запускать и под 64, и под 32.
Заключение
Я вижу такой вариант реализации ВК для 1С как хорошую альтернативу реализации на C++ в случае, когда еще нет большой уже реализованной кодовой базы на плюсах, тем более если целевая компонента не очень функциональна. На собственном опыте за 15 минут была сделана компонента для проигрывания звука, не блокирующая процесс 1С, тем самым закрыв требование заказчика.
Rust дает гарантию адресной безопасности, т.е. не случится такого, что ошибка в коде внешней компоненты будет вызывать исключение, которое будет аварийно закрывать 1С.
EDIT 1:
Хочу уточнить, что компонента проверена только на десктопных клиентах Windows и Linux. До мобильных устройств руки пока не дошли :(