diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8e9e48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +.vscode diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ba93aff --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "addin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[profile.release] +opt-level = "z" # Optimize for size. +lto = true # Enable Link Time Optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = "abort" # Abort on panic +strip = true # Automatically strip symbols from the binary. + +[dependencies] +utf16_lit="2.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d02919 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 https://github.com/medigor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 86dcd74..f5f28d2 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ -# example-native-add-rs \ No newline at end of file +# example-native-api-rs +Пример внешней компоненты для **1С:Предприятие 8** по технологии **Native API** на языке rust + +[Документция на ИТС](https://its.1c.ru/db/metod8dev#content:3221:hdoc) | +[Шаблон компоненты на c++ от Infactum](https://github.com/Infactum/addin-template) + +## Преимущества по сравнению с компонентой на c++ +* Преимущества самого языка rust и его экосистемы +* Для Windows не требуется msvc, собирается полностью с использованием свободных инструментов + +## Обзор +Компоненты по технологии Native API предполагают разработку на языке с++, т.к. компонента должна принимать и возвращать указатели на виртуальные классы c++. Компонента для windows должна собираться только компилятором msvc, а для linux и macos подойдет gcc/clang. +Как известно, взаимодействие *rust* с *c++* из коробки не поддерживается. + +Одним из вариантов было использовать [cxx](https://github.com/dtolnay/cxx) или подобные библиотеки. Это также бы потребовало использовать msvc. + +Другой вариант - вручную реализовать виртуальные таблицы, именно этот вариант и реализован. +На [godbolt](https://godbolt.org/z/KM3jaWMWs) можно посмотреть, как выглядят виртуальные таблицы для разных компиляторов. Виртуальные таблицы *msvc* отличаются от *gcc*/*clang*, при этом *gcc* и *clang* используют одинаковое ABI. Виртуальные таблицы реализованы в объеме достаточном для создания компоненты. + +## Описание файлов +* src/lib.rs - корень крейта, здесь располагаются экспортные функции GetClassNames и др. +* src/ffi.rs - в этом модуле всё что связано с взаимодействием, также здесь находится весь небезопасный код. +* src/addin1.rs - здесь непосредственно реализация компоненты, причем весь код безопасный. +* conf1c - конфигурация 1С (выгрузка из конфигуратора 8.3.22), минимальный тестовый код. + +## Разработка +Я использую для разработки VS Code. Отлаживать и тестировать компоненту удобнее всего в файловой базе. Чтобы при нажатии F5 сразу запускалась 1С, нужно поместить в файл *.vscode/launch.json* примерно такой код: +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug 1С", + "program": "путь/к/файлу/1cv8c", + "args": [ + "/IBName", + "Test1" + ], + "cwd": "${workspaceFolder}", + "preLaunchTask": "rust: cargo build" + } + ] +} +``` +Для разработки на linux я использую виртуальную машину Hyper-V, VS Code подключается по ssh. Чтобы запуск 1С работал из ssh, нужно в конфигурацию запуска добавить: +```json +"env": {"DISPLAY": ":1"} +``` +Для разработки и тестирования также подходит [Учебная версия 1С](https://online.1c.ru/catalog/free/learning.php), но версия для windows только x32. + +MacOS не тестировал, думаю должно работать. + +Android/iOS/веб-клиент не реализовано и планов таких нет. diff --git a/conf1c/ConfigDumpInfo.xml b/conf1c/ConfigDumpInfo.xml new file mode 100644 index 0000000..9d17dd6 --- /dev/null +++ b/conf1c/ConfigDumpInfo.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conf1c/Configuration.xml b/conf1c/Configuration.xml new file mode 100644 index 0000000..ec71127 --- /dev/null +++ b/conf1c/Configuration.xml @@ -0,0 +1,221 @@ + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + 29e7f5a4-dc26-408a-bd6e-083b5bf1384c + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + f76706d7-a451-49f0-a3b8-8c92dc9eb401 + + + e3687481-0a87-462c-a166-9f34594f9bba + efad9454-c037-40e6-92f8-de6f995cb912 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + 5a509aa9-2e5a-47a8-977a-8fbe609d782c + + + 51f2d5d8-ea4d-4064-8892-82951750031e + 3982e56b-1417-490c-8ac4-7672c7bffa80 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + 862a5e5b-5378-4b7c-b567-0ab6725fc840 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + ed5cd24c-da69-4a84-a011-421e03c8dd71 + + + + Конфигурация + + + + Version8_3_22 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + Taxi + Version8_3_22 + + + + Русский + Обработка1 + + + \ No newline at end of file diff --git a/conf1c/DataProcessors/Обработка1.xml b/conf1c/DataProcessors/Обработка1.xml new file mode 100644 index 0000000..514d872 --- /dev/null +++ b/conf1c/DataProcessors/Обработка1.xml @@ -0,0 +1,29 @@ + + + + + + aa584d2a-1a1f-4807-b2e6-588aef5193b8 + 620a7e0f-3713-4585-842f-262d1dfa18c0 + + + e6189229-5433-466b-a435-ac6f2fa36046 + 8606e276-c11b-4eaa-af7b-424d33285d14 + + + + Обработка1 + + + true + DataProcessor.Обработка1.Form.Форма + + false + + + + +
Форма
+
+
+
\ No newline at end of file diff --git a/conf1c/DataProcessors/Обработка1/Forms/Форма.xml b/conf1c/DataProcessors/Обработка1/Forms/Форма.xml new file mode 100644 index 0000000..ba12bd1 --- /dev/null +++ b/conf1c/DataProcessors/Обработка1/Forms/Форма.xml @@ -0,0 +1,22 @@ + + +
+ + Форма + + + ru + Форма + + + + Managed + false + + PlatformApplication + MobilePlatformApplication + + + +
+
\ No newline at end of file diff --git a/conf1c/DataProcessors/Обработка1/Forms/Форма/Ext/Form.xml b/conf1c/DataProcessors/Обработка1/Forms/Форма/Ext/Form.xml new file mode 100644 index 0000000..121f7ba --- /dev/null +++ b/conf1c/DataProcessors/Обработка1/Forms/Форма/Ext/Form.xml @@ -0,0 +1,83 @@ + +
+ Use + + + + ИмяФайла + + + + ИмяФайлаНачалоВыбора + + + + + + + + + cfg:DataProcessorObject.Обработка1 + + true + + + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Имя файла</v8:content> + </v8:item> + + + xs:string + + 0 + Variable + + + + ИмяФайла + + + + + + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Тест2</v8:content> + </v8:item> + + + + ru + Тест2 + + + Тест2 + + + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Тест1</v8:content> + </v8:item> + + + + ru + Тест1 + + + Тест1 + + + \ No newline at end of file diff --git a/conf1c/DataProcessors/Обработка1/Forms/Форма/Ext/Form/Module.bsl b/conf1c/DataProcessors/Обработка1/Forms/Форма/Ext/Form/Module.bsl new file mode 100644 index 0000000..773023c --- /dev/null +++ b/conf1c/DataProcessors/Обработка1/Forms/Форма/Ext/Form/Module.bsl @@ -0,0 +1,85 @@ + +&НаКлиенте +Процедура Тест1(Команда) + Тест1НаСервере(ИмяФайла); +КонецПроцедуры + +&НаСервереБезКонтекста +Процедура Тест1НаСервере(ИмяФайла) + + Начало = ТекущаяУниверсальнаяДатаВМиллисекундах(); + + Если Не ПодключитьВнешнююКомпоненту(ИмяФайла, "Test", ТипВнешнейКомпоненты.Native, ТипПодключенияВнешнейКомпоненты.НеИзолированно) Тогда + Сообщить("Не удалось подключить"); + Возврат; + КонецЕсли; + + Сообщить("Подключена"); + ОбъектКомпоненты = Новый ("AddIn.Test.Class1"); + Test = ОбъектКомпоненты.Test; + Конец = ТекущаяУниверсальнаяДатаВМиллисекундах(); + Сообщить(СтрШаблон("Test: %1", Test)); + Сообщить(СтрШаблон("Длительность: %1", Конец - Начало)); + +КонецПроцедуры + + +&НаКлиенте +Процедура Тест2(Команда) + Тест2НаСервере(ИмяФайла); +КонецПроцедуры + +&НаСервереБезКонтекста +Процедура Тест2НаСервере(ИмяФайла) + + Начало = ТекущаяУниверсальнаяДатаВМиллисекундах(); + + Попытка + ОбъектКомпоненты = Новый ("AddIn.Test.Class1"); + Исключение + Если Не ПодключитьВнешнююКомпоненту(ИмяФайла, "Test", ТипВнешнейКомпоненты.Native, ТипПодключенияВнешнейКомпоненты.НеИзолированно) Тогда + ВызватьИсключение "Не удалось подключить"; + КонецЕсли; + ОбъектКомпоненты = Новый ("AddIn.Test.Class1"); + КонецПопытки; + + ОбъектКомпоненты.PropI32 = 123; + Если ОбъектКомпоненты.PropI32 <> 123 Тогда + ВызватьИсключение "Не удалось установить значение PropI32"; + КонецЕсли; + + ОбъектКомпоненты.PropF64 = 456.789; + Если ОбъектКомпоненты.PropF64 <> 456.789 Тогда + ВызватьИсключение "Не удалось установить значение PropF64"; + КонецЕсли; + + ОбъектКомпоненты.PropBool = Истина; + Если ОбъектКомпоненты.PropBool <> Истина Тогда + ВызватьИсключение "Не удалось установить значение PropBool"; + КонецЕсли; + + Date = ТекущаяДатаСеанса(); + ОбъектКомпоненты.PropDate = Date; + Если ОбъектКомпоненты.PropDate <> Date Тогда + ВызватьИсключение "Не удалось установить значение PropDate"; + КонецЕсли; + + ОбъектКомпоненты.PropStr = "Привет!"; + Если ОбъектКомпоненты.PropStr <> "Привет!" Тогда + ВызватьИсключение "Не удалось установить значение PropStr"; + КонецЕсли; + + Blob = ПолучитьДвоичныеДанныеИзСтроки("Привет!"); + ОбъектКомпоненты.PropBlob = Blob; + Если ОбъектКомпоненты.PropBlob <> Blob Тогда + ВызватьИсключение "Не удалось установить значение PropBlob"; + КонецЕсли; + + Если ОбъектКомпоненты.Method1("11", "22", "33") <> "112233" Тогда + ВызватьИсключение "Не удалось установить значение Method1"; + КонецЕсли; + + Конец = ТекущаяУниверсальнаяДатаВМиллисекундах(); + Сообщить(СтрШаблон("Длительность: %1", Конец - Начало)); + +КонецПроцедуры diff --git a/conf1c/Ext/HomePageWorkArea.xml b/conf1c/Ext/HomePageWorkArea.xml new file mode 100644 index 0000000..dc7c9e4 --- /dev/null +++ b/conf1c/Ext/HomePageWorkArea.xml @@ -0,0 +1,14 @@ + + + TwoColumnsEqualWidth + + +
DataProcessor.Обработка1.Form.Форма
+ 10 + + true + +
+
+ +
\ No newline at end of file diff --git a/conf1c/Languages/Русский.xml b/conf1c/Languages/Русский.xml new file mode 100644 index 0000000..a62fde0 --- /dev/null +++ b/conf1c/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/src/addin1.rs b/src/addin1.rs new file mode 100644 index 0000000..5a5ece1 --- /dev/null +++ b/src/addin1.rs @@ -0,0 +1,208 @@ +use crate::ffi::{Addin, Connection, ParamValue, ReturnValue, Tm}; +use utf16_lit::utf16_null; + +const PROPS: &[&[u16]] = &[ + &utf16_null!("Test"), + &utf16_null!("PropI32"), + &utf16_null!("PropF64"), + &utf16_null!("PropBool"), + &utf16_null!("PropDate"), + &utf16_null!("PropStr"), + &utf16_null!("PropBlob"), +]; + +const METHODS: &[&[u16]] = &[&utf16_null!("Method1")]; + +pub struct Addin1 { + prop_i32: i32, + prop_f64: f64, + prop_bool: bool, + prop_date: Tm, + prop_str: String, + prop_blob: Vec, +} + +impl Addin1 { + pub fn new() -> Addin1 { + Addin1 { + prop_i32: 0, + prop_f64: 0.0, + prop_bool: false, + prop_date: Tm::default(), + prop_str: String::new(), + prop_blob: Vec::new(), + } + } +} + +impl Drop for Addin1 { + fn drop(&mut self) {} +} + +impl Addin for Addin1 { + fn init(&mut self, _interface: &'static Connection) -> bool { + true + } + + fn get_info(&mut self) -> u16 { + 1000 + } + + fn done(&mut self) {} + + fn register_extension_as(&mut self) -> &'static [u16] { + &utf16_null!("Class1") + } + + fn get_n_props(&mut self) -> usize { + PROPS.len() + } + + fn find_prop(&mut self, name: &[u16]) -> Option { + PROPS.iter().position(|&x| x == name) + } + + fn get_prop_name(&mut self, num: usize, _alias: usize) -> Option<&'static [u16]> { + PROPS.get(num).map(|&x| x) + } + + fn get_prop_val(&mut self, num: usize, val: ReturnValue) -> bool { + match num { + 0 => val.set_i32(111226), + 1 => val.set_i32(self.prop_i32), + 2 => val.set_f64(self.prop_f64), + 3 => val.set_bool(self.prop_bool), + 4 => val.set_date(self.prop_date), + 5 => { + let s: Vec = self.prop_str.encode_utf16().collect(); + val.set_str(s.as_slice()); + } + 6 => { + val.set_blob(self.prop_blob.as_slice()); + } + _ => (), + }; + true + } + + fn set_prop_val(&mut self, num: usize, val: &ParamValue) -> bool { + match num { + 1 => match val { + ParamValue::I32(x) => { + self.prop_i32 = *x; + true + } + _ => false, + }, + 2 => match val { + ParamValue::F64(x) => { + self.prop_f64 = *x; + true + } + _ => false, + }, + 3 => match val { + ParamValue::Bool(x) => { + self.prop_bool = *x; + true + } + _ => false, + }, + 4 => match val { + ParamValue::Date(x) => { + self.prop_date = *x; + true + } + _ => false, + }, + 5 => match val { + ParamValue::Str(x) => { + self.prop_str = String::from_utf16(x).unwrap(); + true + } + _ => false, + }, + 6 => match val { + ParamValue::Blob(x) => { + self.prop_blob.clear(); + self.prop_blob.extend_from_slice(x); + true + } + _ => false, + }, + _ => false, + } + } + + fn is_prop_readable(&mut self, _num: usize) -> bool { + true + } + + fn is_prop_writable(&mut self, num: usize) -> bool { + match num { + 2 => true, + 3 => true, + _ => true, + } + } + + fn get_n_methods(&mut self) -> usize { + METHODS.len() + } + + fn find_method(&mut self, name: &[u16]) -> Option { + METHODS.iter().position(|&x| x == name) + } + + fn get_method_name(&mut self, num: usize, _alias: usize) -> Option<&'static [u16]> { + METHODS.get(num).map(|&x| x) + } + + fn get_n_params(&mut self, num: usize) -> usize { + match num { + 0 => 3, + _ => 0, + } + } + + fn get_param_def_value( + &mut self, + _method_num: usize, + _param_num: usize, + _value: ReturnValue, + ) -> bool { + true + } + + fn has_ret_val(&mut self, num: usize) -> bool { + match num { + 0 => true, + _ => false, + } + } + + fn call_as_proc(&mut self, _num: usize, _params: &[ParamValue]) -> bool { + false + } + + fn call_as_func(&mut self, num: usize, params: &[ParamValue], ret_value: ReturnValue) -> bool { + match num { + 0 => { + let mut buf = Vec::::new(); + for p in params { + match p { + ParamValue::Str(x) => buf.extend_from_slice(x), + _ => return false, + } + } + ret_value.set_str(buf.as_slice()); + true + } + _ => false, + } + } + + fn set_locale(&mut self, _loc: &[u16]) {} + + fn set_user_interface_language_code(&mut self, _lang: &[u16]) {} +} diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..3eff9b8 --- /dev/null +++ b/src/ffi.rs @@ -0,0 +1,753 @@ +use std::{ + ffi::{c_int, c_long, c_ulong, c_void}, + mem::size_of, + ptr, + slice::{from_raw_parts, from_raw_parts_mut}, +}; + +#[repr(C)] +#[derive(Debug)] +pub enum AttachType { + CanAttachNotIsolated = 1, + CanAttachIsolated, + CanAttachAny, +} + +#[repr(C)] +#[allow(dead_code)] +#[derive(Clone, Copy, Default)] +pub struct Tm { + pub sec: c_int, // seconds after the minute - [0, 60] including leap second + pub min: c_int, // minutes after the hour - [0, 59] + pub hour: c_int, // hours since midnight - [0, 23] + pub mday: c_int, // day of the month - [1, 31] + pub mon: c_int, // months since January - [0, 11] + pub year: c_int, // years since 1900 + pub wday: c_int, // days since Sunday - [0, 6] + pub yday: c_int, // days since January 1 - [0, 365] + pub isdst: c_int, // daylight savings time flag + + #[cfg(target_family = "unix")] + pub gmtoff: std::ffi::c_long, // seconds east of UTC + #[cfg(target_family = "unix")] + pub zone: std::ffi::c_char, // timezone abbreviation +} + +#[allow(dead_code)] +pub struct ReturnValue<'a> { + mem: &'a MemoryManager, + variant: &'a mut TVariant, +} + +#[allow(dead_code)] +impl<'a> ReturnValue<'a> { + pub fn set_empty(self) { + self.variant.vt = VariantType::EMPTY; + } + pub fn set_i32(self, val: i32) { + self.variant.vt = VariantType::I4; + self.variant.value.i32 = val; + } + + pub fn set_bool(self, val: bool) { + self.variant.vt = VariantType::BOOL; + self.variant.value.bool = val; + } + + pub fn set_f64(self, val: f64) { + self.variant.vt = VariantType::R8; + self.variant.value.f64 = val; + } + + pub fn set_date(self, val: Tm) { + self.variant.vt = VariantType::TM; + self.variant.value.tm = val; + } + + pub fn set_str(self, val: &[u16]) { + let Some(data) = self.mem.alloc_memory::(val.len()) else { + return; + }; + + data.copy_from_slice(val); + + self.variant.vt = VariantType::PWSTR; + self.variant.value.data_str.ptr = data.as_ptr() as *mut u16; + self.variant.value.data_str.len = data.len() as u32; + } + + pub fn set_blob(self, val: &[u8]) { + let Some(data) = self.mem.alloc_memory::(val.len()) else { + return; + }; + + data.copy_from_slice(val); + + self.variant.vt = VariantType::BLOB; + self.variant.value.data_blob.ptr = data.as_ptr() as *mut u8; + self.variant.value.data_blob.len = data.len() as u32; + } + + pub fn alloc_str(self, len: usize) -> Option<&'a mut [u16]> { + let Some(data) = self.mem.alloc_memory::(len) else { + return None; + }; + + self.variant.vt = VariantType::PWSTR; + self.variant.value.data_str.ptr = data.as_ptr() as *mut u16; + self.variant.value.data_str.len = data.len() as u32; + + Some(data) + } + + pub fn alloc_blob(self, len: usize) -> Option<&'a mut [u8]> { + let Some(data) = self.mem.alloc_memory::(len) else { + return None; + }; + + self.variant.vt = VariantType::BLOB; + self.variant.value.data_blob.ptr = data.as_ptr() as *mut u8; + self.variant.value.data_blob.len = data.len() as u32; + + Some(data) + } +} + +pub enum ParamValue<'a> { + Empty, + Bool(bool), + I32(i32), + F64(f64), + Date(Tm), + Str(&'a [u16]), + Blob(&'a [u8]), +} + +impl<'a> From<&'a TVariant> for ParamValue<'a> { + fn from(param: &'a TVariant) -> ParamValue { + unsafe { + match param.vt { + VariantType::EMPTY => Self::Empty, + VariantType::BOOL => Self::Bool(param.value.bool), + VariantType::I4 => Self::I32(param.value.i32), + VariantType::R8 => Self::F64(param.value.f64), + VariantType::TM => Self::Date(param.value.tm), + VariantType::PWSTR => Self::Str(from_raw_parts( + param.value.data_str.ptr, + param.value.data_str.len as usize, + )), + VariantType::BLOB => Self::Blob(from_raw_parts( + param.value.data_blob.ptr, + param.value.data_blob.len as usize, + )), + _ => Self::Empty, + } + } + } +} + +#[repr(u16)] +#[allow(dead_code)] +enum VariantType { + EMPTY = 0, + NULL, + I2, //int16_t + I4, //int32_t + R4, //float + R8, //double + DATE, //DATE (double) + TM, //struct tm + PSTR, //struct str string + INTERFACE, //struct iface + ERROR, //int32_t errCode + BOOL, //bool + VARIANT, //struct _tVariant * + I1, //int8_t + UI1, //uint8_t + UI2, //uint16_t + UI4, //uint32_t + I8, //int64_t + UI8, //uint64_t + INT, //int Depends on architecture + UINT, //unsigned int Depends on architecture + HRESULT, //long hRes + PWSTR, //struct wstr + BLOB, //means in struct str binary data contain + CLSID, //UUID + + UNDEFINED = 0xFFFF, +} + +#[repr(C)] +#[allow(dead_code)] +#[derive(Clone, Copy)] +struct DataStr { + pub ptr: *mut u16, + pub len: u32, +} + +#[repr(C)] +#[allow(dead_code)] +#[derive(Clone, Copy)] +struct DataBlob { + pub ptr: *mut u8, + pub len: u32, +} + +#[repr(C)] +union VariantValue { + pub bool: bool, + pub i32: i32, + pub f64: f64, + pub tm: Tm, + pub data_str: DataStr, + pub data_blob: DataBlob, +} + +#[repr(C)] +#[allow(dead_code)] +struct TVariant { + value: VariantValue, + elements: u32, //Dimension for an one-dimensional array in pvarVal + vt: VariantType, +} + +pub trait Addin { + fn init(&mut self, interface: &'static Connection) -> bool; + fn get_info(&mut self) -> u16; + fn done(&mut self); + fn register_extension_as(&mut self) -> &'static [u16]; + fn get_n_props(&mut self) -> usize; + fn find_prop(&mut self, name: &[u16]) -> Option; + fn get_prop_name(&mut self, num: usize, alias: usize) -> Option<&'static [u16]>; + fn get_prop_val(&mut self, num: usize, val: ReturnValue) -> bool; + fn set_prop_val(&mut self, num: usize, val: &ParamValue) -> bool; + fn is_prop_readable(&mut self, num: usize) -> bool; + fn is_prop_writable(&mut self, num: usize) -> bool; + fn get_n_methods(&mut self) -> usize; + fn find_method(&mut self, name: &[u16]) -> Option; + fn get_method_name(&mut self, num: usize, alias: usize) -> Option<&'static [u16]>; + fn get_n_params(&mut self, num: usize) -> usize; + fn get_param_def_value( + &mut self, + method_num: usize, + param_num: usize, + value: ReturnValue, + ) -> bool; + fn has_ret_val(&mut self, method_num: usize) -> bool; + fn call_as_proc(&mut self, method_num: usize, params: &[ParamValue]) -> bool; + fn call_as_func(&mut self, method_num: usize, params: &[ParamValue], val: ReturnValue) -> bool; + fn set_locale(&mut self, loc: &[u16]); + fn set_user_interface_language_code(&mut self, lang: &[u16]); +} + +#[repr(C)] +//#[allow(dead_code)] +struct InitDoneBaseVTable { + dtor: usize, + #[cfg(target_family = "unix")] + dtor2: usize, + init: unsafe extern "system" fn(&mut InitDoneBase, &'static Connection) -> bool, + set_mem_manager: + unsafe extern "system" fn(&mut InitDoneBase, &'static MemoryManager) -> bool, + get_info: unsafe extern "system" fn(&mut InitDoneBase) -> c_long, + done: unsafe extern "system" fn(&mut InitDoneBase), +} + +unsafe extern "system" fn init( + component: &mut InitDoneBase, + interface: &'static Connection, +) -> bool { + component.addin.init(interface) +} + +unsafe extern "system" fn set_mem_manager( + component: &mut InitDoneBase, + mem: &'static MemoryManager, +) -> bool { + component.memory = Some(mem); + true +} + +unsafe extern "system" fn get_info(component: &mut InitDoneBase) -> c_long { + component.addin.get_info() as c_long +} + +unsafe extern "system" fn done(component: &mut InitDoneBase) { + component.addin.done() +} + +#[repr(C)] +#[allow(dead_code)] +struct LanguageExtenderBaseVTable { + dtor: usize, + #[cfg(target_family = "unix")] + dtor2: usize, + register_extension_as: + unsafe extern "system" fn(&mut LanguageExtenderBase, *mut *mut u16) -> bool, + get_n_props: unsafe extern "system" fn(&mut LanguageExtenderBase) -> c_long, + find_prop: unsafe extern "system" fn(&mut LanguageExtenderBase, *const u16) -> c_long, + get_prop_name: + unsafe extern "system" fn(&mut LanguageExtenderBase, c_long, c_long) -> *const u16, + get_prop_val: for<'a> unsafe extern "system" fn( + &mut LanguageExtenderBase, + c_long, + &'a mut TVariant, + ) -> bool, + set_prop_val: + unsafe extern "system" fn(&mut LanguageExtenderBase, c_long, &TVariant) -> bool, + is_prop_readable: unsafe extern "system" fn(&mut LanguageExtenderBase, c_long) -> bool, + is_prop_writable: unsafe extern "system" fn(&mut LanguageExtenderBase, c_long) -> bool, + get_n_methods: unsafe extern "system" fn(&mut LanguageExtenderBase) -> c_long, + find_method: unsafe extern "system" fn(&mut LanguageExtenderBase, *const u16) -> c_long, + get_method_name: + unsafe extern "system" fn(&mut LanguageExtenderBase, c_long, c_long) -> *const u16, + get_n_params: unsafe extern "system" fn(&mut LanguageExtenderBase, c_long) -> c_long, + get_param_def_value: unsafe extern "system" fn( + &mut LanguageExtenderBase, + c_long, + c_long, + &mut TVariant, + ) -> bool, + has_ret_val: unsafe extern "system" fn(&mut LanguageExtenderBase, c_long) -> bool, + call_as_proc: unsafe extern "system" fn( + &mut LanguageExtenderBase, + c_long, + *const TVariant, + c_long, + ) -> bool, + call_as_func: for<'a> unsafe extern "system" fn( + &mut LanguageExtenderBase, + c_long, + &mut TVariant, + *const TVariant, + c_long, + ) -> bool, +} + +unsafe extern "system" fn register_extension_as( + component: &mut LanguageExtenderBase, + name: *mut *mut u16, +) -> bool { + let Some(allocator) = component.memory else { + return false; + }; + + let extension_name = component.addin.register_extension_as(); + + let Some(data) = allocator.alloc_memory::(extension_name.len()) else { + return false; + }; + data.copy_from_slice(extension_name); + unsafe { *name = data.as_mut_ptr() }; + + true +} + +unsafe extern "system" fn get_n_props(component: &mut LanguageExtenderBase) -> c_long { + component.addin.get_n_props() as c_long +} + +unsafe extern "system" fn find_prop( + component: &mut LanguageExtenderBase, + name: *const u16, +) -> c_long { + let len = strlen(name); + let name = from_raw_parts(name, len); + match component.addin.find_prop(name) { + Some(i) => i as c_long, + None => -1, + } +} + +unsafe extern "system" fn get_prop_name( + component: &mut LanguageExtenderBase, + num: c_long, + alias: c_long, +) -> *const u16 { + let Some(allocator) = component.memory else { + return ptr::null(); + }; + let Some(prop_name) = component.addin.get_prop_name(num as usize, alias as usize) else { + return ptr::null(); + }; + let Some(name) = allocator.alloc_memory::(prop_name.len()) else { + return ptr::null(); + }; + + name.copy_from_slice(prop_name); + name.as_ptr() +} + +unsafe extern "system" fn get_prop_val<'a, T: Addin>( + component: &mut LanguageExtenderBase, + num: c_long, + val: &'a mut TVariant, +) -> bool { + let Some(mem) = component.memory else { + return false; + }; + + let return_value = ReturnValue { mem, variant: val }; + component.addin.get_prop_val(num as usize, return_value) +} + +unsafe extern "system" fn set_prop_val( + component: &mut LanguageExtenderBase, + num: c_long, + val: &TVariant, +) -> bool { + let param = ParamValue::from(val); + component.addin.set_prop_val(num as usize, ¶m) +} + +unsafe extern "system" fn is_prop_readable( + component: &mut LanguageExtenderBase, + num: c_long, +) -> bool { + component.addin.is_prop_readable(num as usize) +} + +unsafe extern "system" fn is_prop_writable( + component: &mut LanguageExtenderBase, + num: c_long, +) -> bool { + component.addin.is_prop_writable(num as usize) +} + +unsafe extern "system" fn get_n_methods( + component: &mut LanguageExtenderBase, +) -> c_long { + component.addin.get_n_methods() as c_long +} + +unsafe extern "system" fn find_method( + component: &mut LanguageExtenderBase, + name: *const u16, +) -> c_long { + let len = strlen(name); + let name = from_raw_parts(name, len); + match component.addin.find_method(name) { + Some(i) => i as c_long, + None => -1, + } +} + +unsafe extern "system" fn get_method_name( + component: &mut LanguageExtenderBase, + num: c_long, + alias: c_long, +) -> *const u16 { + let Some(allocator) = component.memory else { + return ptr::null(); + }; + let Some(method_name) = component.addin.get_method_name(num as usize, alias as usize) else { + return ptr::null(); + }; + let Some(name) = allocator.alloc_memory::(method_name.len()) else { + return ptr::null(); + }; + + name.copy_from_slice(method_name); + name.as_ptr() +} + +unsafe extern "system" fn get_n_params( + component: &mut LanguageExtenderBase, + num: c_long, +) -> c_long { + component.addin.get_n_params(num as usize) as c_long +} + +unsafe extern "system" fn get_param_def_value( + component: &mut LanguageExtenderBase, + method_num: c_long, + param_num: c_long, + val: &mut TVariant, +) -> bool { + let Some(mem) = component.memory else { + return false; + }; + + let return_value = ReturnValue { mem, variant: val }; + + component + .addin + .get_param_def_value(method_num as usize, param_num as usize, return_value) +} + +unsafe extern "system" fn has_ret_val( + component: &mut LanguageExtenderBase, + method_num: c_long, +) -> bool { + component.addin.has_ret_val(method_num as usize) +} + +unsafe extern "system" fn call_as_proc( + component: &mut LanguageExtenderBase, + method_num: c_long, + params: *const TVariant, + size_array: c_long, +) -> bool { + let param_values = from_raw_parts(params, size_array as usize) + .iter() + .map(|x| ParamValue::from(x)) + .collect::>(); + + component + .addin + .call_as_proc(method_num as usize, param_values.as_slice()) +} + +unsafe extern "system" fn call_as_func<'a, T: Addin>( + component: &mut LanguageExtenderBase, + method_num: c_long, + ret_value: &'a mut TVariant, + params: *const TVariant, + size_array: c_long, +) -> bool { + let Some(mem) = component.memory else { + return false; + }; + + let return_value = ReturnValue { + mem, + variant: ret_value, + }; + + let param_values = from_raw_parts(params, size_array as usize) + .iter() + .map(|x| ParamValue::from(x)) + .collect::>(); + + component + .addin + .call_as_func(method_num as usize, param_values.as_slice(), return_value) +} + +#[repr(C)] +#[allow(dead_code)] +struct LocaleBaseVTable { + dtor: usize, + #[cfg(target_family = "unix")] + dtor2: usize, + set_locale: unsafe extern "system" fn(&mut LocaleBase, *const u16), +} + +unsafe extern "system" fn set_locale(component: &mut LocaleBase, loc: *const u16) { + let len = strlen(loc); + let loc = from_raw_parts(loc, len); + component.addin.set_locale(loc) +} + +#[repr(C)] +#[allow(dead_code)] +struct UserLanguageBaseVTable { + dtor: usize, + #[cfg(target_family = "unix")] + dtor2: usize, + set_user_interface_language_code: + unsafe extern "system" fn(&mut UserLanguageBase, *const u16), +} + +unsafe extern "system" fn set_user_interface_language_code( + component: &mut UserLanguageBase, + lang: *const u16, +) { + let len = strlen(lang); + let lang = from_raw_parts(lang, len); + component.addin.set_user_interface_language_code(lang) +} + +#[repr(C)] +#[allow(dead_code)] +struct ComponentBase { + vptr1: Box>, + vptr2: Box>, + vptr3: Box>, + vptr4: Box>, + destroy: unsafe extern "system" fn(*mut *mut ComponentBase), + memory: Option<&'static MemoryManager>, + addin: T, +} + +unsafe extern "system" fn destroy(component: *mut *mut ComponentBase) { + let component = unsafe { Box::from_raw(*component) }; + drop(component); +} + +#[repr(C)] +#[allow(dead_code)] +struct InitDoneBase { + _vptr1: Box>, + _vptr2: Box>, + _vptr3: Box>, + _vptr4: Box>, + destroy: unsafe extern "system" fn(*mut *mut ComponentBase), + memory: Option<&'static MemoryManager>, + addin: T, +} + +// type InitDoneBase = ComponentBase; + +#[repr(C)] +#[allow(dead_code)] +struct LanguageExtenderBase { + _vptr2: Box>, + _vptr3: Box>, + _vptr4: Box>, + destroy: unsafe extern "system" fn(*mut *mut ComponentBase), + memory: Option<&'static MemoryManager>, + addin: T, +} + +#[repr(C)] +#[allow(dead_code)] +struct LocaleBase { + _vptr3: Box>, + _vptr4: Box>, + destroy: unsafe extern "system" fn(*mut *mut ComponentBase), + memory: Option<&'static MemoryManager>, + addin: T, +} + +#[repr(C)] +#[allow(dead_code)] +struct UserLanguageBase { + _vptr4: Box>, + destroy: unsafe extern "system" fn(*mut *mut ComponentBase), + memory: Option<&'static MemoryManager>, + addin: T, +} + +pub fn create_component(addin: T) -> *mut c_void { + let vptr1 = Box::new(InitDoneBaseVTable { + dtor: 0, + #[cfg(target_family = "unix")] + dtor2: 0, + init, + set_mem_manager, + get_info, + done, + }); + + let vptr2 = Box::new(LanguageExtenderBaseVTable { + dtor: 0, + #[cfg(target_family = "unix")] + dtor2: 0, + register_extension_as, + get_n_props, + find_prop, + get_prop_name, + get_prop_val, + set_prop_val, + is_prop_readable, + is_prop_writable, + get_n_methods, + find_method, + get_method_name, + get_n_params, + get_param_def_value, + has_ret_val, + call_as_proc, + call_as_func, + }); + + let vptr3 = Box::new(LocaleBaseVTable { + dtor: 0, + #[cfg(target_family = "unix")] + dtor2: 0, + set_locale, + }); + + let vptr4 = Box::new(UserLanguageBaseVTable { + dtor: 0, + #[cfg(target_family = "unix")] + dtor2: 0, + set_user_interface_language_code, + }); + + let c = Box::new(ComponentBase { + vptr1, + vptr2, + vptr3, + vptr4, + destroy: destroy::, + memory: None, + addin, + }); + + let p = Box::leak(c); + p as *mut ComponentBase as *mut c_void +} + +pub fn destroy_component(component: *mut *mut c_void) { + #[repr(C)] + #[allow(dead_code)] + struct ComponentWrapper { + vptr1: Box, + vptr2: Box, + vptr3: Box, + vptr4: Box, + destroy: unsafe extern "system" fn(*mut *mut c_void), + } + + unsafe { + let wrapper = *component as *mut ComponentWrapper; + let wrapper = &mut *wrapper; + (wrapper.destroy)(component); + } +} + +#[repr(C)] +#[allow(dead_code)] +struct MemoryManagerVTable { + dtor: usize, + #[cfg(target_family = "unix")] + dtor2: usize, + alloc_memory: unsafe extern "system" fn(&MemoryManager, *mut *mut c_void, c_ulong) -> bool, + free_memory: unsafe extern "system" fn(&MemoryManager, *mut *mut c_void), +} + +#[repr(C)] +#[allow(dead_code)] +struct MemoryManager { + vptr1: &'static MemoryManagerVTable, +} + +impl MemoryManager { + pub fn alloc_memory<'a, T>(&self, size: usize) -> Option<&'a mut [T]> { + let mut data = ptr::null_mut::(); + unsafe { + if (self.vptr1.alloc_memory)(self, &mut data, (size * size_of::()) as c_ulong) { + let d = from_raw_parts_mut(data as *mut T, size); + Some(d) + } else { + None + } + } + } +} + +#[repr(C)] +#[allow(dead_code)] +struct ConnectionVTable { + dtor: usize, + #[cfg(target_family = "unix")] + dtor2: usize, +} + +#[repr(C)] +#[allow(dead_code)] +pub struct Connection { + vptr1: &'static ConnectionVTable, +} + +fn strlen(s: *const u16) -> usize { + let mut i = 0; + while unsafe { *s.add(i) } != 0 { + i += 1; + } + i += 1; + i +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b8c4d3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,53 @@ +mod addin1; +mod ffi; + +use std::{ + ffi::{c_long, c_void, c_int}, + sync::atomic::{AtomicI32, Ordering}, +}; + +use addin1::Addin1; +use ffi::{destroy_component, AttachType}; +use utf16_lit::utf16_null; + +use crate::ffi::create_component; + +pub static mut PLATFORM_CAPABILITIES: AtomicI32 = AtomicI32::new(-1); + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn GetClassObject(_name: *const u16, component: *mut *mut c_void) -> c_long { + let addin = Addin1::new(); + unsafe { + *component = create_component(addin); + } + 1 +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn DestroyObject(component: *mut *mut c_void) -> c_long { + destroy_component(component); + 0 +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn GetClassNames() -> *const u16 { + utf16_null!("Class1").as_ptr() +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn SetPlatformCapabilities(capabilities: c_int) -> c_long { + unsafe { + PLATFORM_CAPABILITIES.store(capabilities, Ordering::Relaxed); + } + 3 +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn GetAttachType() -> AttachType { + AttachType::CanAttachAny +}