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

Автор записи: Дмитрий (Admin)
1 звезда2 звезды3 звезды4 звезды5 звезд (Голосов 8, среднее: 5,00 из 5)
Загрузка...

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:
C/C++

  • Библиотека DLL создает/подключается к именованной памяти.
  • Отправляет (записывает в память) текстовое сообщение: "Привет из C/C++".
  • Читает память с периодичностью в 1 секунду, если память стала чиста, сообщение отправляется вновь.
Код библиотеки DLL (C/C++):
C#

  • Приложение на C# создает/подключается к именованной памяти.
  • Читает память с периодичностью в 1 секунду, если в памяти появилось текстовое сообщение: "Привет из C/C++", выводит его в текстовое поле и очищает память, сообщая тем самым DLL что сообщение получено.
Код C#:
Теперь, после добавления библиотеки DLL в каталог терминала QUIK (туда, где файл "info.exe"), запуска скрипта QLua и запуска приложения C#, Вы увидите как на форме C#, с периодичностью в 1 секунду, появляются сообщения "Привет из C/C++":
Привет-из-C
Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!

Добавить комментарий

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

  1. Здравствуйте.
    Постараюсь коротко изложить проблему, но думаю стоит описать с самого начала.
    Решил создавать Именованную память для каждой бумаги "class_sec", например "SPBFUT_SiM3".
    Чтобы постоянно не подключаться к памяти, а только 1 раз, решил хранить HANDLE, выбрал std::map где ключ "class_sec", а значение HANDLE. Когда вызываю функцию записи в Именованную память передаю "class_sec", беру HANDLE из map, а если нету, тогда создаю. И все работает, но я обнаружил что открывается очень много HANDLE для одной и той же бумаги. Я стал выяснять причину и обнаружил что map "разрушается", то есть ключ "class_sec" превращается через время в мусор типа "x_{+1=.. " и соответственно создается новый HANDLE и помещается снова в map. При чем разрушаются только ключи, значения HANDLE остаются целые. При остановке Lua скрипта я вызываю другую функцию, которая перебирает весь map и закрывает HANDLE. Все HANDLE, в том числе и с поврежденными ключами, закрываются. Откртых HANDLE не остается.
    Для выяснения причины удалил все из функции и оказалось что разрушается std::map, std::unordered_map и обычные массивы со значениями const char* и просто char* тоже разрушаются. Поэтому я приведу упрощенный код, только с map где ключ "class_sec" а значение INT случайное число весто HANDLE. И так map тоже разрушается.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    std::map HANDLES;
    static int forLua_TEST(lua_State* L) {
      char message[256] = { 0 };
      OutputDebugStringA("Start TEST");
      for (auto& it : HANDLES) {
        sprintf_s(message, "HANDLE %s %d", it.first, it.second); OutputDebugStringA(message);
      }
      const char* name = lua_tostring(L, 1);
      lua_settop(L, 0);
      int H = HANDLES[name];
      if (H == NULL) {
        H = rand();
        HANDLES[name] = H;
      }
      return 0;
    }

    Смотрю в DebugView и через время вижу что ключи испорчены
    Сначала все хорошо:
    HANDLE SPBFUT_SiM3 41
    HANDLE SPBFUT_GZM3 18467
    ...
    но потом становится так:
    HANDLE < .1[23.Vs 41 HANDLE SPBFUT_GZM3 18467 HANDLE SPBFUT_SiM3 6334 то есть SPBFUT_SiM3 ключ испортился и был создан новый. Что касается Lua, возможно это происходит только если вызываю функцию из OnAllTrade, так как я пробовал вызывать просто из main функцию в цикле с миллионом итераций и случайно передавал разные "class_sec" и разрушения не заметил, с map все хорошо.

    1
    2
    3
    4
    
    function OnAllTrade(deal)
      local name = deal.class_code .. "_" .. deal.sec_code;
      myDLL.TEST(name);
    end

    Так же я пробовал проверять наличие в map, но это не решило проблему.

    1
    2
    3
    4
    5
    6
    
    if (HANDLES.find(name) == HANDLES.end()) {
      H = rand();
      HANDLES[name] = H;
    } else {
      H = handles[name];
    }

    Мои небольшие знания в СИ закончились, больше не знаю в каком направлении искать причину и почему такое происходит, возможно кто-то что-то подскажет...
    Вообще на работу моего алгоритма это ни как не влияет, я даже не знал что открываются тысячи HANDLE, случайно заглянул в Process Explorer и увидел. Можно конечно игнорировать, но я предпочитаю избавиться от этой утечки, если ее так можно назвать. Даже думал создать еще один алгоритм, который будет закрывать HANDLE с испорченными ключали в map и удалять эти записи в map. Но это вообще бред... Хочу понять причину и устранить.

    1. Здравствуйте.
      1. std::map HANDLES; - не заданы тип ключа и тип поля. Как выполняется ваша программа?
      ----
      2. const char* name это указатель на константную строку. По смыслу в качестве ключа и поля map надо использовать типы-значения (которые нельзя изменить "сбоку"), например, std::string.
      Попробуйте проверить следующий вариант:
      1) Вместо std::map HANDLES; -> std::map HANDLES;
      2) Вместо const char* name = lua_tostring(L, 1); -> std::string name = lua_tostring(L, 1);

          1. Благодарю за поясниения.
            1. Да. Я не досмотрел, когда писал на сайте код, конечно в программе у меня указаны типы, в тестовой я писал несколько вариантов:

            1
            2
            
            std::map handles;
            std::map handles;

            А в полной, для чего собственно и задумаывается, было прописано

            1
            
            std::map handles;

            2. Да, потом, когда уже написал сюда вопрос, я побробовал использовать тип string, сразу как то не пришло в голову, но хотя я особо и не верил что это изменит ситуацию. Как оказалось это действительно полностью решило проблему. map не разрушается, лишние HANDLE не создаются, все хорошо.
            - "По смыслу в качестве ключа и поля map надо использовать типы-значения (которые нельзя изменить "сбоку")" - Вот об этом мне ничего небыло известно, так как я не изучал основы языка СИ, а лишь пишу код используя найденные примеры в Интернете и описания функций. Когда испольовал тип string, что решило проблему, я стал догадываться что у типов char* есть какие то особенности, которые мне не известны, но какие именно не понятно, все же нужно изучать основы.
            Еще раз благодарю за помощь.

              1. Интересно, почему вы стали использовать C++?
                Таблица в Lua это map, но ключами и полями могут быть данные любого Lua-типа. При этом таблицы реализованы в Lua на C и сделано это на хорошем уровне, скорее всего, сравнимом с реализацией map в C++.

                1. Мне нужно создать Именованную память для каждой бумаги и записывать в нее данные после получения каждой сделки. Если передать из LUA даные в СИ, открыть доступ к памяти, получить указатель, записать данные и закрыть, на это уходит много времени. Поэтому я решил что на первой сделке открываю доступ, сохраняю HANDLE в map и так же с указателем, его тоже сохраняю в map, записываю данные в память и ничего не закрываю. Следующая сделка, теперь беру данные из map и пишу данные. Если сделка по другой бумаге, открывается или создается новая память и так же не закрывается. А при остановке скрипта всё закрываю.
                  То есть мне нужно было где то хранить HANDLE, если в lua таблицах, то я не знаю как, поэтому решил хранить в СИ, первое что нашел, где можно использовать текстовые ключи, это map.

  2. Доброго времени суток.
    Такая проблема, при переходе через ночь или остановке скрипта, квик наглухо повисает и крашится. Далее листинг скрипта. Помогите пожалуйста разобраться в чем может быть проблема...

    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
    98
    99
    100
    101
    102
    103
    
    require("getCandles");
     
    local Str = "";
    triggerForUpdate = 0;
    local CommandStr;
    lastIndexDs = 1;
    oldSize = 1;
    local ds;
    local callBackTrigget = false;
    IsRun = true;
    local t = "";
    local d = "";
    local m = "";
    local ClassName = "SPBFUT";
    local SecName = "RIU7";
    local f = "";
     
    function main()
    	while IsRun do
    		if tostring(getCandles.GetCommand()) == "run" then --Ловим из приложения команду на старт получения свечек с графика и дальнейшую отправку
    			callBackTrigget = true; --Переводим триггер в положение True
    			ds = CreateDataSource(ClassName, SecName, INTERVAL_M1); --Подписываеся на получение свечек
    			getCandles.ClearCommand(); --Используем функцию для очиски командной памяти, ибо бывает теряется команда, были прецеденты
    		elseif tostring(getCandles.GetCommand()) == "stop" then --Ловим из приложения команду на стоп получения свечек с графика и дальнейшую отправку
    			ds:Close(); --Отписывается от получения свечек с графика
    			getCandles.ClearCommand(); --Используем функцию для очиски командной памяти
    			callBackTrigget = false; --Переводим триггер в положение False
    			lastIndexDs = 1; --Меняем значение переменной последней отправленной свечки
    			Str = ""; --Очищаем строку отправки
    		end;
     
    		if callBackTrigget == true then --Проверяем значение триггера, можно ли начинать отправлять свечки или нет 
    			if ds:Size() ~= nil and ds:Size() > lastIndexDs then --Проверяем чтобы набор данных не пустовал, а так же проверяем наличие новых свечек
    				triggerForUpdate = 0; --Сбрасываем флаг для обновления графика в приложении
    				oldSize = ds:Size(); --Запоминаем количество свечек на момент попадания в условие
    				for i = lastIndexDs, ds:Size() - 1 do --Цикл для сбора свечек
    					if tostring(getCandles.GetCommand()) == "stop" then --Отлов команды из приложения для остановки сбора свечек
    						ds:Close(); --Отписывается от получения свечек с графика
    						getCandles.ClearCommand() --Используем функцию для очиски командной памяти
    						callBackTrigget = false; --Переводим триггер в положение False
    						lastIndexDs = 1; --Меняем значение переменной последней отправленной свечки
    						oldSize = 1; --Сбрасываем количество свечек на момент попадания в условие
    						Str = ""; --Очищаем строку отправки
    						break; --Останавливаем цикл
    					end;
     
    					if i == oldSize - 1 then triggerForUpdate = 1; end; --Проверяем не является ли текущая свечка последней на момент попадния в условие, если да, то переводим флаг обновления графика в приложени
     
    					if tonumber(ds:T(i).min) < 10 then t = "0"..tostring(ds:T(i).min); --Проверяем не потерялся ли 0 у минут
    						else t = tostring(ds:T(i).min); end;
     
    					if tonumber(ds:T(i).day) < 10 then d = "0"..tostring(ds:T(i).day); --Проверяем не потерялся ли 0 у дня
    						else d = tostring(ds:T(i).day); end;
     
    					if tonumber(ds:T(i).month) < 10 then m = "0"..tostring(ds:T(i).month); --Проверяем не потерялся ли 0 у месяца
    						else m = tostring(ds:T(i).month); end;
     
    					--Собираем свечки в одну строку, разделяя из знаком #
    					Str = Str.."T="..d.."."..m.."."..tostring(ds:T(i).year).." "..tostring(ds:T(i).hour)..":"..t..":".."00"..";"..
    						"O="..tostring(ds:O(i))..";".."H="..tostring(ds:H(i))..";".."L="..tostring(ds:L(i))..";".."C="..tostring(ds:C(i))..";"..
    						"V="..tostring(ds:V(i))..";".."U="..tostring(triggerForUpdate).."#";
    					sleep(1); --Делаем небольшую паузу, чтобы квик не повесился
    				end;
     
    				f = io.open(getScriptPath().."\\candles.txt","r+"); --Проверяем существитение файла
    				if f == nil then --Если файла не существует
    					if callBackTrigget == true then --И триггер для отпарвки свечек в положении True
    						lastIndexDs = oldSize; --Передаем старое количества свечек на момент попадания в уловие отправки свечек
    						f = io.open(getScriptPath().."\\candles.txt","w"); --Создаем файл
    						f:write(Str); --Записываем в него полученные свечки
    						f:flush(); --Применяем изменения в файле
    						f:close(); --Закрываем файл, тем самым освобождая его для приложения
    						Str = ""; --Очищаем строку
    						sleep(2000); --Задержка нужна чтобы файл успел забраться приложением, ибо если новых свечей нет, то произойдет конфликт доступа
    					end;
    				else --Если файл существует
    					f:close(); -- Освобождаем его
    					os.remove(getScriptPath().."\\candles.txt") --Удаляем
    					f = io.open(getScriptPath().."\\candles.txt","w"); --Создаем по новой
    					f:write(Str); --Записываем полученные свечки
    					f:flush(); --Применяем изменения в файле
    					f:close(); --Освобождаем файл
    					Str = ""; --Очищаем строку
    					sleep(2000); --Задержка нужна чтобы файл успел забраться приложением, ибо если новых свечей нет, то произойдет конфликт доступа
    				end;
    			end;
     
    			if callBackTrigget == true then --Проверяем состояние триггера на сбор и отправку свечей
    				--Проверяем есть ли подключение к серверу
    				if isConnected() == 1 then ds = CreateDataSource(ClassName, SecName, INTERVAL_M1); end; --Если есть, то переподписываемся на получение свечек
    				if isConnected() == 0 then ds:Close(); end; --Если нет, то отписываемся от получения свечек
     
    				--Условия выше предназначались для перехода через ночь и предотвращения зависания квика во время выключения скрипта вручную,
    				--но что то не сильно помогает.....
    			end;
    		end;
    		sleep(1);
    	end;
    end;
     
    function OnStop()
    	IsRun = false;
    end;
      1. Здравствуйте, попробуйте поразрывать и повосстанавливать соединение терминала с сервером просто днем, не дожидаясь ночи, возможно эта ситуация вызывает те проблемы, о которых Вы пишите, чтобы найти ошибку, которая Вас интересует нужно и код dll смотреть и еще что у Вас там работает, а на это много времени нужно, которого нет в свободном доступе.

            1. Разобрался, в том куске кода где я сомневался и правда ошибка, там постоянно исполнялось какое то действие при подключении или при отсутствии подключения к серверу, а этого по хорошему надо избегать, чтобы избежать всякого рода зависаний и дальнейших крашей.

              То есть было :

              1
              2
              
              if isConnected() == 1 then ds = CreateDataSource(ClassName, SecName, INTERVAL_M1); end;
              if isConnected() == 0 then ds:Close(); end;

              Стало:

              1
              2
              
              if isConnected() == 1 and ds:Size() == nil then ds = CreateDataSource(ClassName, SecName, INTERVAL_M1); end; 
              if isConnected() == 0 and ds:Size() ~= nil then ds:Close(); end;
  3. "там можно более хорошей скорости достичь." На самом в деле, в ДЛЛ тожеминимум и только подготовка данных. Там сложная математика привязана, которая выполняется вообще в сторонних программах, таких как - http://www.scilab.org/. Это свободно распространяемый аналог МатЛаба.
    Буду рад, если заинтересует. У него тоже С-шное API.

  4. "Дак а что мешает из луа с C# напрямую общаться через сокеты, без всяких DLL ?"
    Замысел архитектора.))
    Есть мысль сделать из ДЛЛ полноценное приложение. Почти полноценное. Получается достточно просто, но надо проверить. Однако все сразу невозможно. Спязь первична.)
    Если мысль заинтересовала и есть время на попробовать, то приватно могу изложить. Хотя, может оказаться, что вы и сами уже так делаете.

    1. Я пробовал в длл создавать окно с кнопочкой при помощи WinAPI, все просто оказалось, но дальше этого не пошел, т.к. не было необходимости. На C# пользовательский интерфейс как-то интереснее разрабатывать, по моему мнению 🙂 А, вообще, я последнее время пришел к выводу, что для робота вполне достаточно чистого QLua, а во всех этих коннекторах есть смысл только если нужно сделать какое-то самостоятельное приложение типа скальперского стакана, а робот, в традиционном понимании, должен сам торговать без участия пользователя, а если требуется участие, то это уже не робот 🙂

      1. Добрый день. Я немного не о том, и не говорю, что ДЛЛ перестанет быть ДЛЛ. Хотя, разумеется, можно и окно с кнопочкой, если сильно хочется.)) ДЛЛ будет выполнять, в общем, те же самые функции, т.е. конечная цель останется без изменения, но все сильно упрощается и возможности и быстродействиие ДЛЛ серьезно увеличиваются. При минимальных затратах.
        Что касается - "все делать в КЛуа", то это не для моих стратегий. Полсекунды - это уже вечность.))
        По мне, по возможности, как можно меньше Луа и самые примитивные операции, а лучше и вообще без Луа.)) Я до этого работал с терминалом с полным API, но брокер вывел его из эксплуатации, заменив на другой. Надеюсь Квик не исчезнет в одночасье, хотя мне он не особо нравится, но других вариантов - раз, и все.

      2. "Я пробовал в длл создавать окно с кнопочкой при помощи WinAPI, все просто оказалось, но дальше этого не пошел, т.к. не было необходимости."

        Ох, Дмитрий, какую хорошую вещь вы пробовали, а здесь ее не выложили!
        Есть люди, которые очень хотели бы ее увидеть т.к. сами они ее полгода изобретать будут, а ваш этот вышупомянутый эксперимент их бы спас!

        1. Просто как-то нестабильно все работало, не захотелось допиливать. Хорошо, посмотрю что я там навоял тогда, если найду. Если решу, что это можно выложить, то опубликую, хотя, я, обычно, не выкладываю то, что не работает нормально.

          1. Что-то я сегодня начитался много на тему прямого использования WinAPI - все, как один, говорят что это очень плохая идея, использовать в 21 веке WinAPI ))) Так что, наверное, уже не нужно. У меня своего мнения нет на этот счет, могу только полагаться на мнение сообщества. Наверное, действительно, не нужно))

      1. Конечно, но наверное лучше в другом месте. Все таки не наша тема. Пусть Хозяин (Дмитрий) скажет где лучше.
        Кстати, уже реализовал. Осталось причесать и в ДЛЛ-ку впихнуть.

        1. Спасибо, оперативно. Я думаю Дмитрий занят, спросил как с вами связаться и даже донатсов на развите проекта забросил, пока мне ничего не ответили. Моя почта yotra-global@yandex.ru Напишите, если не трудно на нее: КВИК-Луа-Сокет

      2. Провел тестирования клиент-сервера на сокетах. И клиент и сервер реализованы на С# как консольные приложения. Клиент и сервер обменивались строками длиной 120 символов, 500000 циклов. Продолжительность теста составила 35 с, что составляет 14285 циклов/с.

          1. Здравствуйте, Дмитрий. Большая статья с экземплами -https://professorweb.ru/my/csharp/web/level3/3_1.php Экземплы рабочие, но для непосредственного применения непригодны. Но можно поэкспериментировать и взять за основу.

          1. В принципе, могу. Но относительно кода в статье там, в общем, ничего нового. Применение NET- сокетов везде одинаковое. Я только вынес сами сокеты в отдельные классы и консоли клиента и сервера работают только с их методами.

  5. Сейчас много читаю по взаимодействию между процессами. Получается, что Pipes и MemoryMappedFiles - это прошлый век (Ни в коем случае не утверждаю, что их не надо применять.)). Они пошли от старых версий Unix. Сегодня основной технологией являются сокеты (Sockets) - уже и Windows. В общем, сокеты пришли тоже из Unix, а в Linux все взаимодействие между процессами уже изначально строилось на сокетах. Когда-то курсы по Linux(Unix) заканчивал, так там 2 недели только о сокетах и говорили.))
    Кстати, не потому-ли в стандартной поставке Framework Sockets есть, а pipes & MMF отсутствуют, и грузятся по необходимости.
    Не говорю, что буду на них делать, но тема интересная и в инете много инфы.

      1. Смотрю сокеты на С++. Быстродействие, говорите.) Простенький клиенти сервер, тест - > 2000 запросов/с. Это 0.5 мс/запрос. На Шарпах наверно побольше. Только надо Unix- сокеты, они локальные, по определению. Еще и могут размножаться как тараканы и идентифицировать клиента.
        Не агитирую.)) Но, чем больше читаю, тем больше нравится.

  6. Для тех, кто соберется реализовывать на NET обмен через Pipes или MMF.
    В заводской поставке VS 2015 Framework, System.IO.Pipes и System.IO.MemoryMappedFiles отсутствуют.
    Ищите на NuGet. Инсталируется в конкретный открытый проект.
    Нашел только для Framework 4.6. для более ранних версий - не знаю, хотя д.б. уже с версии 4.0.

    1. Странно, я ничего не подгружал специально, VS обычно сама выделяет ошибку, навожу на нее мышкой, появляется желтый значек с выпадающим списком, раскрываю его, выбираю первый пункт и в uses добавляется нужный компонент.

  7. Что-то в этом роде и имелось в виду под протоколами обмена. С MMF знаком только в принципе, применять не приходилось. В одном из приложений делал обмен обычными файлами, с записью на небольшой RAM-Disk. Чтобы избежать совместного доступа запись-чтение, создавал дополнительно пустой файл, как флаг окончания записи, который затирался приемником, что использовалось как флаг разрешения следующей записи. В общем, структура обмена практически аналогичная.
    Сейчас буду разбираться, как в MMF организована (и есть ли вообще) организация доступа.
    Я так понял, что вы обмениваетесь текст файлами в формате, типа CSV?

    1. Я, вообще, строку просто формирую и отправляю, разделитель выбираю в зависимости от данных, универсальных форматов не ищу, т.к. сделки одним способом удобнее отправлять, команды другим, стакан третьим и т.д. Посмотрите раздел меню "QLua C/C++ C#", там много примеров, хотя, сейчас, когда я реализовываю подобные задачи, я немного по другому делаю некоторые вещи, чем там представлено, но, по большому счету, суть та же осталась. А проблему совместного использования я тоже просто решил: DLL, например записала нужные данные в память, после чего установила в первые 4 байта константу _CR_, которая означает, что память ожидает чтения C#, после этого DLL берет новые данные для отправки и читает постоянно первые 4 байта памяти, как видит, что в памяти установлена константа _QW_, означающая, что память готова к записи QLua, пишет туда эти данные и устанавливает константу _CR_. На стороне C# происходит все то же самое, только, вместо записи - чтение. C#, видя константу _CR_, читает то, что прислала DLL, потом затирает память нулевыми байтами и устанавливает константу _QW_, после этого продолжает постоянно читать первые 4 байта, как видит в них _CR_, все повторяется. И никаким конфликтам там просто нет места.