
Для обмена данными между библиотекой DLL, написанной на языке C/C++ и приложением, написанном на языке C#, удобно и эффективно использовать Отображаемые в Памяти Файлы (MemoryMappedFile). По сути, это выделенный участок оперативной памяти компьютера (скорость!), который имеет свое уникальное имя и размер в байтах. Оба эти параметра задаются программистом. В дальнейшем можно, как читать из этой памяти, так и писать в нее, подключившись к ней в библиотеке DLL и в приложении C#.
Был проведен тест скорости обмена сообщениями, в тесте принимали участие следующие технологии: MemoryMappedFile, NamedPipes и Socket. Создавались по два отдельных приложения на C#, сервер и клиент, которые должны были обменяться друг с другом текстовыми сообщениями размером 120 символов 500 000 раз. На все это у них ушло следующее количество времени:
MemoryMappedFile: 1,5 секунды
NamedPipes: 12,5 секунд
Socket: 14 секунд
При этом, количество требуемого кода, так же, было меньше всего у MemoryMappedFile, по моему, выбор очевиден!
Пример создания и использования именованной памяти с именем "MyMemory" и размером 256 байт. В примере реализован следующий алгоритм:
QLua(Lua)
- Подключает библиотеку DLL
- Запускает функцию отправки сообщений в C#
- Останавливает функцию отправки сообщений в C#
Код скрипта QLua:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| -- Подключает библиотеку DLL
require("QluaCSharpConnector");
-- Флаг для поддержания работы функции main
IsRun = true;
function main()
-- Запускает функцию отправки сообщений в C#
QluaCSharpConnector.StartSendHi();
-- Обеспечивает работу скрипта и библиотеки до остановки скрипта пользователем
while IsRun do
sleep(1000);
end;
end;
function OnStop()
-- Останавливает функцию отправки сообщений в C#
QluaCSharpConnector.StopSendHi();
-- Останавливает цикл в функции main
IsRun = false;
end; |
C/C++
- Библиотека DLL создает/подключается к именованной памяти.
- Отправляет (записывает в память) текстовое сообщение: "Привет из C/C++".
- Читает память с периодичностью в 1 секунду, если память стала чиста, сообщение отправляется вновь.
Код библиотеки DLL (C/C++):
(Если Вы не знаете как создавать библиотеки DLL, которые можно использовать в скриптах QLua(Lua), ознакомьтесь, пожалуйста, с
данной статьей).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| #include <windows.h>
//=== Необходимые для Lua константы ============================================================================//
#define LUA_LIB
#define LUA_BUILD_AS_DLL
//=== Заголовочные файлы LUA ===================================================================================//
extern "C" {
#include "Lua\lauxlib.h"
#include "Lua\lua.h"
}
//=== Получает указатель на выделенную именованную память =====================================================//
// Имя для выделенной памяти
TCHAR Name[] = TEXT("MyMemory");
// Создаст, или подключится к уже созданной памяти с таким именем
HANDLE hFileMapMyMemory = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 256, Name);
//=== Стандартная точка входа для DLL ==========================================================================//
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
// Флаг необходимости отправлять сообщение
bool Run = true;
//=== Реализация функций, вызываемых из LUA ====================================================================//
static int forLua_StartSendHi(lua_State *L) // Отправляет сообщения для C#
{
// Если указатель на память получен
if (hFileMapMyMemory)
{
// Получает доступ (представление) непосредственно к чтению/записи байт
PBYTE pb = (PBYTE)(MapViewOfFile(hFileMapMyMemory, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 256));
// Если доступ получен
if (pb != NULL)
{
// Очищает память при первом обращении
for (int i = 0; i < 256; i++)pb[i] = '\0';
// Бесконечный цикл пока Run == true
while (Run)
{
// Если запись пустая (либо первая отправка, либо C# очистил память, подтвердив получение)
if (pb[0] == 0)
{
// Записывает текст сообщения в память
char *Str = "Привет из C/C++";
memcpy(pb, Str, strlen(Str));
}
// Пауза в 1 секунду
Sleep(1000);
}
// Закрывает представление
UnmapViewOfFile(pb);
// Закрывает указатель на память
CloseHandle(hFileMapMyMemory);
}
}
return(0);
}
static int forLua_StopSendHi(lua_State *L) // Прекращает отправку сообщений для C#
{
Run = false; // Выключает флаг
return(0);
}
//=== Регистрация реализованных в dll функций, чтобы они стали "видимы" для Lua ================================//
static struct luaL_reg ls_lib[] = {
{ "StartSendHi", forLua_StartSendHi }, // из скрипта Lua эту функцию можно будет вызывать так: QluaCSharpConnector.StartSendHi(); здесь можно указать любое другое название
{ "StopSendHi", forLua_StopSendHi }, // соответственно
{ NULL, NULL }
};
//=== Регистрация названия библиотеки, видимого в скрипте Lua ==================================================//
extern "C" LUALIB_API int luaopen_QluaCSharpConnector(lua_State *L) {
luaL_openlib(L, "QluaCSharpConnector", ls_lib, 0);
return 0;
} |
C#
- Приложение на C# создает/подключается к именованной памяти.
- Читает память с периодичностью в 1 секунду, если в памяти появилось текстовое сообщение: "Привет из C/C++", выводит его в текстовое поле и очищает память, сообщая тем самым DLL что сообщение получено.
Код C#:
Создайте проект
C# Windows Forms, если Вы не знаете как это сделать, посмотрите
здесь.
Разместите на форме один
TextBox (из вкладки
"Панель элементов"), установите его свойство
"Multiline" в
"True", а свойство
"ScrollBars" в
"Vertical" и растяните его по форме, чтобы получилось примерно следующее:

Кликните по шапке формы, перейдите в окне
"Свойства" на вкладку
"События" и сделайте двойной клик в пустой строке напротив поля
"FormClosing". Таким образом в файл
"Form1.cs" добавится новая функция
"Form1_FormClosing", которая будет вызываться после того, как Вы нажмете на кнопку закрытия формы запущенного приложения.
Снова кликните по шапке формы, перейдите в окне "Свойства" на вкладку "События" и сделайте двойной клик в пустой строке напротив поля "Shown". Таким образом в файл "Form1.cs" добавится новая функция "Form1_Shown", которая будет вызываться при первом показе формы.
Сейчас полностью замените код в файле "Form1.cs" на этот:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
| using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Test
{
public partial class Form1 : Form
{
// Выделенная именованная память
public MemoryMappedFile Memory;
// Объект для чтения из памяти
StreamReader SR_Memory;
// Объект для записи в память
StreamWriter SW_Memory;
// Флаг необходимости получать сообщения
bool Run = true;
public Form1()
{
InitializeComponent();
// Создаст, или подключится к уже созданной памяти с таким именем
Memory = MemoryMappedFile.CreateOrOpen("MyMemory", 256, MemoryMappedFileAccess.ReadWrite);
// Создает поток для чтения
SR_Memory = new StreamReader(Memory.CreateViewStream(), System.Text.Encoding.Default);
// Создает поток для записи
SW_Memory = new StreamWriter(Memory.CreateViewStream(), System.Text.Encoding.Default);
}
// Делегат нужен для того, чтобы безопасно обратиться к TextBox из другого потока
private delegate void TB(string Msg);
private void AppText(string Msg)
{
// Добавляет к сообщению символ перехода на новую строку
textBox1.AppendText(Msg + Environment.NewLine);
}
private void GetMessage()// Получает сообщения от DLL, выводит их в текстовое поле, очищает память
{
string Msg = "";
// Цикл работает пока Run == true
while(Run)
{
// Встает в начало потока для чтения
SR_Memory.BaseStream.Seek(0, SeekOrigin.Begin);
// Считывает данные из потока памяти, обрезая ненужные байты
Msg = SR_Memory.ReadToEnd().Trim('\0', '\r', '\n');
// Если в потоке нужное сообщение ("Привет из C/C++")
if (Msg == "Привет из C/C++")
{
// Потокобезопасно выводит сообщение в текстовое поле
BeginInvoke(new TB(AppText), Msg);
// Встает в начало потока для записи
SW_Memory.BaseStream.Seek(0, SeekOrigin.Begin);
// Очищает память, заполняя "нулевыми байтами"
for (int i = 0; i < 256; i++) SW_Memory.Write("\0");
// Очищает все буферы для SW_Memory и вызывает запись всех данных буфера в основной поток
SW_Memory.Flush();
}
//Пауза в 1 секунду
Thread.Sleep(1000);
}
// По завершению цикла, закрывает все потоки и освобождает именованную память
SR_Memory.Close();
SW_Memory.Close();
Memory.Dispose();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
Run = false;// Выключает флаг
}
private void Form1_Shown(object sender, EventArgs e)
{
// Запускает функцию чтения и вывода сообщений в отдельном потоке, чтобы форма отвечала на действия пользователя
new Thread(() =>
{
GetMessage();
}).Start();
}
}
} |
Скомпилируйте проект.
Теперь, после добавления библиотеки DLL в каталог терминала QUIK (туда, где файл "info.exe"), запуска скрипта QLua и запуска приложения C#, Вы увидите как на форме C#, с периодичностью в 1 секунду, появляются сообщения "Привет из C/C++":
Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!
После плясок с бубном вокруг ДЛЛ, пришел к выводу, что MMF, ну и еще Named Pipes, наверное действительно самые простые и достаточно эффективные решения взаимодействия приложения с Луа-окружением. Наверное уйду в одну из этих технологий.
Но здесь все просто пока передаешь один сигнал и нет дуплексного взаимодействия. Как только сигналов становится много (стакан, лента сделок, OHLSV, инфа о заявках, сделках и счетах) + запросы из приложения к Луа - начинаются проблемы. Т.е. нужно определяться интерфейсами и протоколами обмена, что, в общем, тоже нетривиальная задача.
Я вообще для каждого канала данных отдельную память просто выделяю и все, никаких конфликтов. Например, выделяю память "all_trades", в DLL зарегистрирована функция обратного вызова OnAllTrade, которая пишет новые сделки в стек FIFO, читает из этого стека отдельный фоновый поток и пишет их в память, если она ожидает записи, в C#, так же, висит отдельный поток
который постоянно читает память, когда она ожидает чтения и сохраняет сделки в соответствующий массив, в котором любой другой поток всегда может найти актуальную информацию.
А протокол простой 🙂 :
DLL
C#
Прочитал первые 4 байта и понятно что можно, что нельзя, для примера памяти "all_trade" вообще 2-х констант достаточно.
Здравствуйте. Спасибо Вам за этот раздел сайта. Книга Иерузалимского +Ваш сайт очень помогают в освоении. Книга - теория, а ваш сайт прикладные экземплы.
Вот только с MemoryMappedFile не могу смириться.)) Поэтому хочу уже в ДЛЛ уйти в managed код, и его цеплять к приложению С#. А в приложении С# уже делать обратные вызовы и получать события QLua.
В результате должны получить как бы C# API.
Первый вопрос - передача событий и данных ДЛЛ в поток managed класса С++. Пока пытаюсь разобраться с генерацией событий в С++. Не сильно получается, с С++ уже давно не работал.
Здравствуйте! Всегда пожалуйста. К сожалению, я не профессиональный программист, а самоучка, изучаю что-то по мере необходимости, потому только отдаленно понимаю что такое "поток managed класса С++". Вод здесь один хороший человек выложил пример как вообще без C++ обходиться, может быть это Вам поможет.
https://quikluacsharp.ru/stati-uchastnikov/lua-c-bez-s-dll/
Добрый день. Спасибо, я конечно читал эту статью, но мне хочется все-таки все взаимодействие с Луа сделать на С++. Я тоже не проф программист, но по работе всегда требовалось что-то сделать.)) Managed и unmanaged -это неуправляемый (чистый С++) и управляемый (NET) коды, и в С++ их можно совмещать в одном проекте, и даже в рамках одного срр файла. Уже пробовал, и это работает. Т.е., все можно делать в рамках VS, не используя сторонний компилятор (как в статье). Есть простенький экземпл, как это делается, могу прислать весь проект, если заинтересует.
Если Вы данную DLL пробовали подгружать в скрипт и все прошло хорошо, то пришлите пример, буду рад, просто, насколько я знаю, для использования Net в DLL нужно включать поддержку CLR, а с ней Lua не дружит, хотя, скорее всего, я что-то не понимаю 🙂
Пока это просто пример использования NET совместно с неуправляемым кодом с MSDN -иллюстрация возможности.
В вашей (нашей) ДЛЛ уже включил CLR и все прошло штатно, но NET-к од пока не писал. Как только что-то получится - пришлю.
Понял, хорошо, спасибо!
Здравствуйте! Дайте, пожалуйста, ссылку на эту статью с примером в MSDN.
https://msdn.microsoft.com/en-us/library/0adb9zxe.aspx
В общем, не получилось. Net-код нормально работает внутри Lua dll (при работоспособности коннекта с Квик) и вся функциональность Net видна снаружи. DLL цепляется к Net приложению и компилируется. Но... ошибка исполнения при цепляниии ДЛЛ к приложению.
Подумываю об организации callback из ДЛЛ.
Спасибо, я тут тоже про коллбеки читаю, как раз 🙂
В моем представлении, это должны быть, в конечном итоге, event's и делегаты. Потоки вполне могут быть разными.
Я, честно говоря, еще не могу одного понять, если бы DLL C++ подключалась к C#, то проблем бы не было, но она подключается к квику, и я не могу понять как запущенный экземпляр DLL увидит C# и как C# увидит этот экземпляр DLL.
Даже если бы мы к квику подключили DLL на C# написанную, то как из нее вызвать функцию приложения C# запущенного, это же два разных процесса, никак друг с другом не связанных.
Есть мысль, что нужно как-то создавать экземпляр какого-то класса в DLL и регистрировать его в глобальном пространстве, потом находить его из приложения C# и обращаться к его методам. Но это только мои догадки пока что 🙂
Хотя, вроде бы, экземпляры классов и так видны из вне, раньше что-то такое помню тестил, но даже примерно не помню уже как это все работает.
Отчего-то в Ваших постах отсутствует -"Ответить". Потому отвечаю как-бы сам себе.))
1. Объекты с# (или С++) в ДЛЛ запущенные из Луа и объекты ДЛЛ, запущенные из приложения - разные потоки. Т.е., внутри ДЛЛ Луа прекрасно общается с с#, но даже если мы его умудримся вывести наружу, то они все равно будет в потоке Луа.
Если вызываем ДЛЛ из приложения, то вызов и все колбэки уже будут в потоке приложения, если напрямую не работают со стеком Луа по указателю.
Т.е., взаимодействие (дуплекс) при вызовах м.б. только через события и делегаты.
Я такие ДЛЛки видел и работал с ними (там полный дуплекс), но как они устроены представления не имею.
PS Под с # имелось в виду C++ NET
По настройкам сайта допускается максимум 10 вложенных комментариев, так что, либо пишите новый комментарий как комментарий к самой статье, т.е. в новой ветке, либо как ответ, где есть кнопка ответить, я, в любом случае, их увижу.
Верно ли полагать, что данный способ быстрее, чем передача по DDE?
Даже не знаю, не сравнивал их скорость, цель данной технологии обеспечить двухстороннюю связь, в отличии от дде
Дмитрий, подскажите пожалуйста как бы выглядела передача из DLL CPP в C# передача инфо о сделке (с помощью выделенной памяти) не через строку символов, а напрямую через представление инфо байтами? Просто плохо знаю CPP
Николай
Здравствуйте, Николай. Я тоже плохо знаю CPP, к сожалению, но, в любом случае, строка это тоже набор байт и в именованной памяти хранятся именно байты, а не буквы, и отлаживать программу так легче.
Спасибо. Прочитаю 😉
Понятно))
Спасибо за ответ)))
Вот здесь, думаю, есть смысл поизучать:) https://msdn.microsoft.com/ru-ru/library/aa970268(v=vs.110).aspx
Да )) это не те компы)))) с которых начинал в 85 году, на ассемблере )))))) делали всякие контроллеры- сейчас программирование просто хобби
Тогда еще вопрос по созданию оконного приложения на С#, наверно если уж изучать и писать (сугубо для себя) то наверно окна на WPF. В интернете огромное кол-во примеров но как дилетанту трудно отличить качественные примеры от ....
Дмитрий, может у Вас есть пару сайтов на примете на эту тему, которые Вы могли бы по рекомендовать, для начального старта.
С уважением, Александр
К сожалению, Александр, в данном вопросе ничего толкового не могу Вам подсказать, потому что сам хочу WPF изучить, но времени не хватает, т.к. хочу сильно расширить функциональные возможности данного сайта, добавить много нового и нужного, для этого пишу его с нуля на C# ASP.NET MVC 5 и все время уходит на изучение данной технологии и создание элементов:) По-этому, по старинке, накидываю на форму стандартные элементы управления, навешиваю на них обработчики событий, а графики и т.п. отрисовываю в PictureBox из примитивов, делаю, например, функцию, которая рисует свечу в нужной координате, а потом вызываю ее в цикле с нужными координатами и получаю график и т.д. и т.п.
Безусловно - "Проще всего из чисел создавать строку с разделителями «;», типа: «1;2;3;4;5;6;7;8;9;10;11;12;13;14;15″ и ее ..." и это первое что приходит в голову.
Сомнение было в скорости выполнения. Каждый раз кодируешь в текст потом обратно.
Но если Вы таким образом передаете стакан и получаются малые временные в задержки.
Тогда сделаю также. Спасибо за совет.
Оставлю все как сейчас в текстовом формате.
Пожалуйста, обращайтесь! В современных компьютерах такие задачи выполняются мгновенно, тем более, если объем данных не велик.
Дмитрий, добрый день. Спасибо Вам за сайт -большая проделана работа.
Набирал примеры все сразу работало.
Решил используя Вашу идею обмена через CreateFileMapping в своем приводе но записать/прочитать структуру из 15 чисел не получилось. Хочу организовать обмен между С++ и C#
Помогите пожалуйста, если Вам это не трудно .
С уважением, Александр
Здравствуйте, Александр! Спасибо за добрые слова! Всегда рад помочь. Проще всего из чисел создавать строку с разделителями ";", типа: "1;2;3;4;5;6;7;8;9;10;11;12;13;14;15" и ее передавать из C/C++ в C# посредством FileMapping. А на стороне C# посредством Split(';') получать из строки массив элементов. О том, как передавать массив из QLua в C/C++ я добавлял недавно пример в конце статьи "ВЗАИМОДЕЙСТВИЕ LUA И БИБЛИОТЕКИ DLL, НАПИСАННОЙ НА C/C++". Но еще проще решить такую задачу по аналогии с примером, описанным в статье "ОТПРАВКА СТАКАНА ИЗ QUIK (QLUA) В ПРИЛОЖЕНИЕ C#". Что именно у Вас не получается?
А где такое может использоваться? Если только для передачи разовых сообщений между приложениями. Или я не прав?
Здравствуйте, Игорь! Благодарю за вопрос! Я использую этот механизм, как для получения данных из QUIK в C#, так и для отправки торговых приказов в QUIK. А вообще, спектр применения очень широк и зависит только от Вашей фантазии, ведь можно передать 1 байт информации, а можно и целый файл.
Если Вас заинтересовал данный вопрос, то рекомендую ознакомиться со следующими материалами: 1, 2, 3.
P.S. Еще, данная технология применяется когда нужно обрабатывать объемный файл и скорость обработки критична. Тогда файл размещается в оперативной памяти. Работа с ним ведется так же, как, если бы он был на диске, но скорость работы с оперативной памятью компьютера намного выше.
Спасибо за подробный ответ.
Скажите, а применим подобный метод передачи данных при передачи больших объемов? например если речь идет о Таблице всех сделок, стакана и некоторых других данных?
1) Я понимаю, что технически это реализуемо, но какова скорость такого взаимодействия?
2) Для передачи разных данных нужно будет создавать свою область данных? Например одна для Таблицы всех сделок, другая для стакана, третья для чего-либо еще ... Не очень удобно.
3) Если нужно также передавать команды в QUIK, то как я понимаю примерно так:
function main()
-- Запускает функцию отправки сообщений в C#
QluaCSharpConnector.StartSendHi();
-- Запускает функцию приема команд из C#
cmd = QluaCSharpConnector.RecieveCmd();
.....
-- Обеспечивает работу скрипта и библиотеки до остановки скрипта пользователем
while IsRun do
sleep(1000);
end;
end;
При этом отправка команд и получения будут обрабатываться последовательно, пока не отработает функция отправки команды, не запуститься функция получения команды.
4) Почему вы выбрали такой способ взаимодействия, а не, к примеру взаимодействие через COM интерфейс на LUA?
Старался излагать доходчиво вопросы, извиняюсь если где-то не получилось. Мои знания пока что по большей теоретические.
MemoryMappedFile технология специально создана для работы с большими объемами данных, тем более, таблица всех сделок, стакан и т.п. не таких большого объемов, как может показаться на первый взгляд.
1) Вы правы, технически это довольно легко реализуемо, скорость работы с оперативной памятью зависит от конфигурации Вашего компьютера. В любом случае, сам QUIK тоже работает из оперативной памяти и обмениваться данными на скорости большей, чем позволяет оперативная память, не сможет.
2) Теоретически, можно использовать и один экземпляр MemoryMappedFile, но вряд ли это будет удобно. Мне, лично, конструкция, когда для каждого потока данных выделен свой канал памяти показалась очень удобной. Тем более, создав один канал и поняв как он работает, не сложно будет размножить его простым копированием кода с изменением имен каналов и функций обращения.
3) Не совсем так. Вот, специально для Вас, небольшую статью с примерами написал (надеюсь и другим пригодится).
4) Мне этот способ показался более простым и понятным. У меня не очень приятные ощущения остались от использования ранее COM, честно говоря. Еще для использования COM в Lua-скрипте нужно больше строк кода писать, а во всей этой конструкции: «QUIK-Lua-C#», QUIK является самым слабым звеном, как по скорости, так и по надежности, а соответственно и Lua. По этой причине, я для себя принял решение максимально абстрагироваться от терминала и использовать его только как инструмент подключения к бирже, а всю логику и обработку/хранение данных перенести в C#, как, на мой взгляд, наилучшее на сегодня программное решение, как для понимания, так и по функциональности.
P.S. Рад был ответить на Ваши вопросы, надеюсь, что немного помог разобраться!
Большое спасибо!
Позвольте задам еще один вопрос 🙂
В случае обмена данными через MemoryMappedFile возникнет такая проблема проблему:
допустим нужно передать какие-то часто меняющиеся данные из Lua в С#, пускай это будет стакан. В Lua мы реализуем метод OnQuote, в котором получаем данные стакана и передаем их в функцию из DLL, назовем ее QluaCSharpConnector.TakeSS(). В ней, в цикле, полученные из LUA данные заносятся в файл памяти, если там не ноль. На C# мы реализуем цикл, который проверяет наличие данных в памяти - если там не 0, значит данные есть, забираем их и записываем 0.
Но на самом может случится так (и случится), что функция из DLL QluaCSharpConnector.TakeSS() получит из Lua новую порцию данных, а программа на C# еще не успела забрать/обработать предыдущие данные в файле памяти. В этом случае новые данные будут либо утеряны, либо DLL функция QluaCSharpConnector.TakeSS() будет ждать когда файл памяти очистится и будет готов записи новой порции данных. А что если в момент ожидания в LUA снова сработает OnQuote?
Игорь, благодарю за Ваш вопрос! Всегда рад помочь!
Ответил на Ваш вопрос в новой статье.