Обмен данными между DLL (C/C++) и приложением C#

Автор записи: Дмитрий (Admin)

Qlua-csharp-connector-dll
Для обмена данными между библиотекой 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» и растяните его по форме, чтобы получилось примерно следующее:
VS-форма-с-TextBox1Кликните по шапке формы, перейдите в окне «Свойства» на вкладку «События» и сделайте двойной клик в пустой строке напротив поля «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++":
Привет-из-C
Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!