
Для обмена данными между библиотекой 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#
[su_spoiler title="Код скрипта QLua:" style="fancy"]
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; |
[/su_spoiler]
C/C++
- Библиотека DLL создает/подключается к именованной памяти.
- Отправляет (записывает в память) текстовое сообщение: "Привет из C/C++".
- Читает память с периодичностью в 1 секунду, если память стала чиста, сообщение отправляется вновь.
[su_spoiler title="Код библиотеки DLL (C/C++):" style="fancy"]
(Если Вы не знаете как создавать библиотеки 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; } |
[/su_spoiler]
C#
- Приложение на C# создает/подключается к именованной памяти.
- Читает память с периодичностью в 1 секунду, если в памяти появилось текстовое сообщение: "Привет из C/C++", выводит его в текстовое поле и очищает память, сообщая тем самым DLL что сообщение получено.
[su_spoiler title="Код C#:" style="fancy"]
Создайте проект 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(); } } } |
Скомпилируйте проект.
[/su_spoiler]
Теперь, после добавления библиотеки DLL в каталог терминала QUIK (туда, где файл "info.exe"), запуска скрипта QLua и запуска приложения C#, Вы увидите как на форме C#, с периодичностью в 1 секунду, появляются сообщения "Привет из C/C++":

Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!