diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs index 052b81cc3..47477058a 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs @@ -22,6 +22,7 @@ class NativeApiComponent : BslObjectValue, IRuntimeContextInstance, IDisposable { private IntPtr _object; private TypeDescriptor _type; + private readonly NativeApiLibrary _library; private readonly NativeApiProxy.OnErrorDelegate _onError; private readonly NativeApiProxy.OnEventDelegate _onEvent; private readonly NativeApiProxy.OnStatusDelegate _onStatus; @@ -87,6 +88,8 @@ public NativeApiComponent( if (!NativeApiProxy.IsAvailable) throw new RuntimeException("Native API Proxy DLL is not loaded"); + _library = library; + _onError = (wcode, source, descr, scode) => OnComponentError?.Invoke(Status(wcode), scode, S(source), S(descr)); _onEvent = (source, message, data) => @@ -329,11 +332,7 @@ public void CallAsFunction(int methodNumber, IValue[] arguments, out IValue retV public void Dispose() { - if (_object == IntPtr.Zero) - return; - - NativeApiProxy.DestroyObject(_object); - _object = IntPtr.Zero; + _library.DestroyComponent(this, ref _object); } } } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs index 72b8155ad..16fc8bac9 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs @@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using OneScript.Contexts; using OneScript.Exceptions; @@ -33,16 +34,23 @@ class NativeApiFactory public static bool Register(string filepath, string identifier, ITypeManager typeManager) { - if (_libraries.ContainsKey(identifier)) - return false; - var library = new NativeApiLibrary(filepath, identifier, typeManager); - if (library.Loaded) - _libraries.Add(identifier, library); - return library.Loaded; + // Проверка и добавление под одной блокировкой: иначе два задания, + // подключающие одну метку, загрузят библиотеку дважды + lock (_librariesLock) + { + if (_libraries.ContainsKey(identifier)) + return false; + var library = new NativeApiLibrary(filepath, identifier, typeManager); + if (library.Loaded) + _libraries.TryAdd(identifier, library); + return library.Loaded; + } } - private static readonly Dictionary _libraries = - new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly object _librariesLock = new object(); + + private static readonly ConcurrentDictionary _libraries = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); internal static bool TryGetLibrary(string identifier, out NativeApiLibrary library) { @@ -53,14 +61,17 @@ internal static bool TryGetLibrary(string identifier, out NativeApiLibrary libra internal static void Shutdown() { - if (_shutdown) - return; + lock (_librariesLock) + { + if (_shutdown) + return; - _shutdown = true; + _shutdown = true; - foreach (var item in _libraries) - item.Value.Dispose(); - _libraries.Clear(); + foreach (var item in _libraries) + item.Value.Dispose(); + _libraries.Clear(); + } } [ScriptConstructor] diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs index cd6368ee3..9e15c76f9 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs @@ -25,7 +25,8 @@ class NativeApiLibrary : IDisposable { private delegate IntPtr GetClassNames(); - private readonly List _components = new List(); + private readonly HashSet _components = + new HashSet(ReferenceEqualityComparer.Instance); private readonly string _identifier; private readonly String _tempfile; @@ -37,6 +38,11 @@ class NativeApiLibrary : IDisposable private readonly List _knownExtensionNames = new List(); private bool _allKeysEnumerated; + // Компоненты одной библиотеки создают из разных фоновых заданий и запросов веб-сервера, + // а кэш имен и список созданных компонент у библиотеки общие + private readonly object _lock = new object(); + private bool _disposed; + public NativeApiLibrary(string filepath, string identifier, ITypeManager typeManager) { _identifier = identifier; @@ -110,48 +116,58 @@ public IValue CreateComponent(ITypeManager typeManager, object host, String type { var typeDef = typeManager.GetTypeByName(typeName); + lock (_lock) + { + // Библиотеку выгружают при остановке движка, а фоновые задания еще могут работать + if (_disposed) + throw new RuntimeException($"Библиотека внешних компонент `{_identifier}` уже выгружена"); + + return DoCreateComponent(host, typeDef, componentName); + } + } + + private IValue DoCreateComponent(object host, TypeDescriptor typeDef, String componentName) + { if (_extensionToClassName.TryGetValue(componentName, out var cachedClassName)) return TrackComponent(CreateComponentByClassName(host, typeDef, cachedClassName, componentName)); if (_allKeysEnumerated) throw CreateNotFoundException(componentName); - if (NativeApiFactory.AllowFactoryClassNames) - { - var resolvedName = ResolveClassName(componentName); - var component = TryCreateComponent(host, typeDef, resolvedName); - if (component != null) - return TrackComponent(component); - - component = TryCreateComponent(host, typeDef, componentName); - if (component != null) - return TrackComponent(component); - } + var component = TryCreateByFactoryClassName(host, typeDef, componentName) + ?? FindByExtensionName(host, typeDef, componentName); + if (component != null) + return TrackComponent(component); + throw CreateNotFoundException(componentName); + } + + private NativeApiComponent TryCreateByFactoryClassName(object host, TypeDescriptor typeDef, string componentName) + { + if (!NativeApiFactory.AllowFactoryClassNames) + return null; + + return TryCreateComponent(host, typeDef, ResolveClassName(componentName)) + ?? TryCreateComponent(host, typeDef, componentName); + } + + // Создает компоненты еще не проверенных классов и запоминает их имена расширений + private NativeApiComponent FindByExtensionName(object host, TypeDescriptor typeDef, string componentName) + { NativeApiComponent matched = null; foreach (var className in _classNames) { - if (_checkedKeys.Contains(className)) + if (!_checkedKeys.Add(className)) continue; - _checkedKeys.Add(className); - var candidate = TryCreateComponent(host, typeDef, className); if (candidate == null) continue; - var extensionName = candidate.GetExtensionName(); - if (!string.IsNullOrEmpty(extensionName)) + if (RememberExtensionName(candidate, className, componentName)) { - _extensionToClassName[extensionName] = className; - if (!_knownExtensionNames.Any(n => string.Equals(n, extensionName, StringComparison.OrdinalIgnoreCase))) - _knownExtensionNames.Add(extensionName); - - if (string.Equals(extensionName, componentName, StringComparison.OrdinalIgnoreCase)) - { - matched = candidate; - break; - } + matched = candidate; + break; } candidate.Dispose(); @@ -160,10 +176,21 @@ public IValue CreateComponent(ITypeManager typeManager, object host, String type if (_classNames != null && _checkedKeys.Count >= _classNames.Length) _allKeysEnumerated = true; - if (matched != null) - return TrackComponent(matched); + return matched; + } - throw CreateNotFoundException(componentName); + // true - имя расширения компоненты совпало с искомым + private bool RememberExtensionName(NativeApiComponent candidate, string className, string componentName) + { + var extensionName = candidate.GetExtensionName(); + if (string.IsNullOrEmpty(extensionName)) + return false; + + _extensionToClassName[extensionName] = className; + if (!_knownExtensionNames.Any(n => string.Equals(n, extensionName, StringComparison.OrdinalIgnoreCase))) + _knownExtensionNames.Add(extensionName); + + return string.Equals(extensionName, componentName, StringComparison.OrdinalIgnoreCase); } private NativeApiComponent CreateComponentByClassName( @@ -197,6 +224,24 @@ private IValue TrackComponent(NativeApiComponent component) return component; } + /// + /// Уничтожает объект компоненты и снимает ее с учета: при выгрузке библиотеки ее уничтожать уже не нужно. + /// Под блокировкой библиотеки ОсвободитьОбъект из другого потока и выгрузка библиотеки + /// не уничтожат объект дважды, а выгрузка не дойдет до FreeLibrary раньше, чем объект уничтожен. + /// + internal void DestroyComponent(NativeApiComponent component, ref IntPtr nativeObject) + { + lock (_lock) + { + if (nativeObject == IntPtr.Zero) + return; + + NativeApiProxy.DestroyObject(nativeObject); + nativeObject = IntPtr.Zero; + _components.Remove(component); + } + } + private RuntimeException CreateNotFoundException(string componentName) { var message = new StringBuilder(); @@ -214,11 +259,21 @@ private RuntimeException CreateNotFoundException(string componentName) public void Dispose() { - foreach (var component in _components) + NativeApiComponent[] components; + lock (_lock) + { + // Под той же блокировкой, что и создание: компонента, созданная после снимка, осталась бы жить + _disposed = true; + components = _components.ToArray(); + _components.Clear(); + } + + // Не под перебором списка: освобождаемая компонента сама снимает себя с учета. + // Если ее уже уничтожают из другого потока, Dispose дождется этого на блокировке + foreach (var component in components) { component.Dispose(); } - _components.Clear(); if (Loaded && NativeApiKernel.FreeLibrary(Module)) { diff --git a/tests/native-api.os b/tests/native-api.os index f360f4434..bfe4ff44c 100644 --- a/tests/native-api.os +++ b/tests/native-api.os @@ -29,6 +29,7 @@ ВсеТесты.Добавить("ТестДолжен_СоздатьSecondAddInНезависимоОтРегистра"); ВсеТесты.Добавить("ТестДолжен_ОтклонитьКлючФабрикиGetClassNames"); ВсеТесты.Добавить("ТестДолжен_БалансироватьСчётчикиЖизненногоЦикла"); + ВсеТесты.Добавить("ТестДолжен_СоздаватьКомпонентыИзНесколькихЗаданий"); #Если Windows Тогда ЭтоWindows = Истина; @@ -67,16 +68,12 @@ КонецПроцедуры -Функция ПодключитьКомпонентуDLL() - - Если DLLПодключенаУспешно <> Неопределено Тогда - Возврат DLLПодключенаУспешно; - КонецЕсли; +Функция ПутьКомпонентыDLL() СистемнаяИнформация = Новый СистемнаяИнформация(); КаталогСборок = ОбъединитьПути(ТекущийСценарий().Каталог, "native-api"); - + Если СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Windows_x86 Тогда ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "bin", "AddInNativeWin32.dll"); ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Windows_x86_64 Тогда @@ -84,14 +81,24 @@ ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Linux_x86 Тогда ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build32", "AddInNativeLin32.so"); ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Linux_x86_64 Тогда - ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build64", "AddInNativeLin64.so"); + ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build64", "AddInNativeLin64.so"); Иначе ВызватьИсключение "Неподдерживаемый тип платформы: " + СистемнаяИнформация.ТипПлатформы; КонецЕсли; - + + Возврат ПутьБиблиотеки; + +КонецФункции + +Функция ПодключитьКомпонентуDLL() + + Если DLLПодключенаУспешно <> Неопределено Тогда + Возврат DLLПодключенаУспешно; + КонецЕсли; + Идентификатор = "AddinNativeDLL"; - ФайлКомпоненты = Новый Файл(ПутьБиблиотеки); + ФайлКомпоненты = Новый Файл(ПутьКомпонентыDLL()); юТест.ПроверитьИстину(ФайлКомпоненты.Существует(), "Файл компоненты '" + ФайлКомпоненты.ПолноеИмя + "' должен существовать"); @@ -609,6 +616,42 @@ КонецПроцедуры +Процедура ТестДолжен_СоздаватьКомпонентыИзНесколькихЗаданий() Экспорт + + // Своя метка: кэш имен у этой библиотеки пуст, его заполняют сами задания + Идентификатор = "AddinNativeDLLЗадания"; + КомпонентаПодключена = ПодключитьВнешнююКомпоненту(ПутьКомпонентыDLL(), Идентификатор, ТипВнешнейКомпоненты.Native); + юТест.ПроверитьИстину(КомпонентаПодключена, "Компонента должна подключиться"); + + // Типы регистрируются заранее: проверяется библиотека компонент, а не менеджер типов + ТипПервой = Тип("AddIn." + Идентификатор + ".CAddInNative"); + ТипВторой = Тип("AddIn." + Идентификатор + ".SecondAddIn"); + + Задания = Новый Массив; + Для Номер = 1 По 8 Цикл + Параметры = Новый Массив; + Параметры.Добавить(Идентификатор); + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "СоздатьКомпонентыВЗадании", Параметры)); + КонецЦикла; + ФоновыеЗадания.ОжидатьВсе(Задания); + + Для Каждого Задание Из Задания Цикл + Если Задание.ИнформацияОбОшибке <> Неопределено Тогда + ВызватьИсключение Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки(); + КонецЕсли; + КонецЦикла; + +КонецПроцедуры + +Процедура СоздатьКомпонентыВЗадании(Идентификатор) Экспорт + + Для Номер = 1 По 50 Цикл + Компонента = Новый("AddIn." + Идентификатор + ".CAddInNative"); + Компонента = Новый("AddIn." + Идентификатор + ".SecondAddIn"); + КонецЦикла; + +КонецПроцедуры + Процедура ТестДолжен_ОтклонитьНеподключённуюМетку() Экспорт ИмяТипа = "AddIn.НетТакойМетки.Класс";