Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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);
}
}
}
39 changes: 25 additions & 14 deletions src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, NativeApiLibrary> _libraries =
new Dictionary<string, NativeApiLibrary>(StringComparer.OrdinalIgnoreCase);
private static readonly object _librariesLock = new object();

private static readonly ConcurrentDictionary<string, NativeApiLibrary> _libraries =
new ConcurrentDictionary<string, NativeApiLibrary>(StringComparer.OrdinalIgnoreCase);

internal static bool TryGetLibrary(string identifier, out NativeApiLibrary library)
{
Expand All @@ -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]
Expand Down
117 changes: 86 additions & 31 deletions src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class NativeApiLibrary : IDisposable
{
private delegate IntPtr GetClassNames();

private readonly List<NativeApiComponent> _components = new List<NativeApiComponent>();
private readonly HashSet<NativeApiComponent> _components =
new HashSet<NativeApiComponent>(ReferenceEqualityComparer.Instance);

private readonly string _identifier;
private readonly String _tempfile;
Expand All @@ -37,6 +38,11 @@ class NativeApiLibrary : IDisposable
private readonly List<string> _knownExtensionNames = new List<string>();
private bool _allKeysEnumerated;

// Компоненты одной библиотеки создают из разных фоновых заданий и запросов веб-сервера,
// а кэш имен и список созданных компонент у библиотеки общие
private readonly object _lock = new object();
private bool _disposed;

public NativeApiLibrary(string filepath, string identifier, ITypeManager typeManager)
{
_identifier = identifier;
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -197,6 +224,24 @@ private IValue TrackComponent(NativeApiComponent component)
return component;
}

/// <summary>
/// Уничтожает объект компоненты и снимает ее с учета: при выгрузке библиотеки ее уничтожать уже не нужно.
/// Под блокировкой библиотеки ОсвободитьОбъект из другого потока и выгрузка библиотеки
/// не уничтожат объект дважды, а выгрузка не дойдет до FreeLibrary раньше, чем объект уничтожен.
/// </summary>
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();
Expand All @@ -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();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Не под перебором списка: освобождаемая компонента сама снимает себя с учета.
// Если ее уже уничтожают из другого потока, Dispose дождется этого на блокировке
foreach (var component in components)
{
component.Dispose();
}
_components.Clear();

if (Loaded && NativeApiKernel.FreeLibrary(Module))
{
Expand Down
61 changes: 52 additions & 9 deletions tests/native-api.os
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ВсеТесты.Добавить("ТестДолжен_СоздатьSecondAddInНезависимоОтРегистра");
ВсеТесты.Добавить("ТестДолжен_ОтклонитьКлючФабрикиGetClassNames");
ВсеТесты.Добавить("ТестДолжен_БалансироватьСчётчикиЖизненногоЦикла");
ВсеТесты.Добавить("ТестДолжен_СоздаватьКомпонентыИзНесколькихЗаданий");

#Если Windows Тогда
ЭтоWindows = Истина;
Expand Down Expand Up @@ -67,31 +68,37 @@

КонецПроцедуры

Функция ПодключитьКомпонентуDLL()

Если DLLПодключенаУспешно <> Неопределено Тогда
Возврат DLLПодключенаУспешно;
КонецЕсли;
Функция ПутьКомпонентыDLL()

СистемнаяИнформация = Новый СистемнаяИнформация();

КаталогСборок = ОбъединитьПути(ТекущийСценарий().Каталог, "native-api");

Если СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Windows_x86 Тогда
ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "bin", "AddInNativeWin32.dll");
ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Windows_x86_64 Тогда
ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "bin64", "AddInNativeWin64.dll");
ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Linux_x86 Тогда
ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build32", "AddInNativeLin32.so");
ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Linux_x86_64 Тогда
ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build64", "AddInNativeLin64.so");
ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build64", "AddInNativeLin64.so");
Иначе
ВызватьИсключение "Неподдерживаемый тип платформы: " + СистемнаяИнформация.ТипПлатформы;
КонецЕсли;


Возврат ПутьБиблиотеки;

КонецФункции

Функция ПодключитьКомпонентуDLL()

Если DLLПодключенаУспешно <> Неопределено Тогда
Возврат DLLПодключенаУспешно;
КонецЕсли;

Идентификатор = "AddinNativeDLL";

ФайлКомпоненты = Новый Файл(ПутьБиблиотеки);
ФайлКомпоненты = Новый Файл(ПутьКомпонентыDLL());
юТест.ПроверитьИстину(ФайлКомпоненты.Существует(),
"Файл компоненты '" + ФайлКомпоненты.ПолноеИмя + "' должен существовать");

Expand Down Expand Up @@ -609,6 +616,42 @@

КонецПроцедуры

Процедура ТестДолжен_СоздаватьКомпонентыИзНесколькихЗаданий() Экспорт

// Своя метка: кэш имен у этой библиотеки пуст, его заполняют сами задания
Идентификатор = "AddinNativeDLLЗадания";
КомпонентаПодключена = ПодключитьВнешнююКомпоненту(ПутьКомпонентыDLL(), Идентификатор, ТипВнешнейКомпоненты.Native);
юТест.ПроверитьИстину(КомпонентаПодключена, "Компонента должна подключиться");

// Типы регистрируются заранее: проверяется библиотека компонент, а не менеджер типов
ТипПервой = Тип("AddIn." + Идентификатор + ".CAddInNative");
ТипВторой = Тип("AddIn." + Идентификатор + ".SecondAddIn");

Задания = Новый Массив;
Для Номер = 1 По 8 Цикл
Параметры = Новый Массив;
Параметры.Добавить(Идентификатор);
Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "СоздатьКомпонентыВЗадании", Параметры));
КонецЦикла;
ФоновыеЗадания.ОжидатьВсе(Задания);

Для Каждого Задание Из Задания Цикл
Если Задание.ИнформацияОбОшибке <> Неопределено Тогда
ВызватьИсключение Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки();
КонецЕсли;
КонецЦикла;

КонецПроцедуры

Процедура СоздатьКомпонентыВЗадании(Идентификатор) Экспорт

Для Номер = 1 По 50 Цикл
Компонента = Новый("AddIn." + Идентификатор + ".CAddInNative");
Компонента = Новый("AddIn." + Идентификатор + ".SecondAddIn");
КонецЦикла;

КонецПроцедуры

Процедура ТестДолжен_ОтклонитьНеподключённуюМетку() Экспорт

ИмяТипа = "AddIn.НетТакойМетки.Класс";
Expand Down
Loading