English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Как открыть мир C# из MQL5 путем экспорта неуправляемого кода

Как открыть мир C# из MQL5 путем экспорта неуправляемого кода

MetaTrader 5Интеграция | 9 февраля 2011, 16:55
14 111 42
investeo
investeo

Введение

Долгое время я искал простое решение, которое позволило бы мне использовать в MQL5 управляемые (managed) DLL, написанные на C#. После чтения множества статей, когда я уже был готов реализовать обертку (wrapper) для управляемой DLL на С++ , я наткнулся на блестящее решение, сэкономившее мне много часов работы.

Предлагаемое решение является простым примером экспорта управляемого C#-кода для неуправляемого (unmanaged) приложения. В данной статье я рассмотрю основы работы управляемых DLL, объясню причины, по которым они не могут быть использованы напрямую в MetaTrader 5, и предложу найденные мной решения, которые позволят использовать управляемый код из MetaTrader 5.

Я приведу пример простейшего использования шаблонов экспорта неуправляемого кода (unmanaged exports) и рассмотрю моменты, которые я обнаружил. Это может послужить основой для всех, кто пытается использовать в MetaTrader 5 библиотеки DLL, написанные на C#.


1. Управляемый и неуправляемый код

Поскольку большинство читателей могут быть не знакомы с отличиями между управляемым и неуправляемым кодом, кратко опишем их. Для реализации торговли, индикаторов, советников и скриптов в MetaTrader 5 в основном используется язык MQL5. Кроме того, существует возможность использования (и динамической загрузки) готовых библиотек, написанных на других языках. Эти библиотеки также известны как DLL или динамически подключаемые библиотеки.

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

Возрастающий интерес к использованию DLL в MetaTrader 5 обусловлен возможностью сокрытия некоторых частей реализации индикаторов или советников. Основная причина использования библиотек - возможность повторного использования кода без необходимости его постоянного переписывания.

До появления платформы .NET все существующие DLL-библиотеки компилировались при помощи Visual Basic, Delphi, VC++, были реализованы в виде COM, Win32, при этом они могли исполняться непосредственно операционной системой. Далее такой код мы будем называть неуправляемым (unmanaged) или нативным (native) кодом.

Затем появилась платформа .NET с совершенно другим окружением. Код контролируется (или управляется) общеязыковой исполняющей средой (Common Language Runtime, CLR).  Из исходного кода, который может быть написан на нескольких различных языках, CLR компиляторы генерируют код и метаданные в промежуточном языке CIL (Common Intermediate Language).

CIL является машинно-независимым языком высокого уровня, а метаданные полностью описывают типы объектов, оперируемых CIL в формате общей спецификации типов (Common Type Specification, CTS). Поскольку CLR известно все о типах, он может предоставить нам управляемое окружение исполнения.  Управление можно рассматривать как сборку мусора - автоматическое управление памятью, удаление объектов и обеспечение безопасности от общих ошибок native-языков, которые могут быть обусловлены исполнением чужеродного кода с привилегиями администратора или простым перекрытием областей памяти.

Необходимо отметить, что код на CIL никогда не выполняется напрямую, он всегда транслируется в native-код в режиме компиляции "на лету" (JIT, Just-In-Time) или предварительной компиляцией CIL в native-код:

Рисунок 1. Общеязыковая управляющая среда (CLR, Common Language Runtime) 

Рисунок 1. Общеязыковая управляющая среда (CLR, Common Language Runtime)


2. Возможные варианты реализации доступа к управляемому коду из MQL5

В следующих параграфах я рассмотрю методы, позволяющие получить доступ к управляемому коду из неуправляемого кода.

Я полагаю, стоит упомянуть все известные методы, возможно кто-то предпочтет использовать другой способ, отличный от того, который использую я. Известные методы: COM Interop, Reverse P/Invoke, C++ IJW, C++/Cli class wrapper и Unmanaged Exports.


2.1. COM Interop (COM-взаимодействие)

Объектная модель компонентов (Component Object Model, COM) - это стандарт интерфейса, предложенный Microsoft в начале 90-х. Ключевая идея этой технологии заключается в возможности использования объектов, написанных на различных языках программирования любым другим COM-объектом без знания его внутренней реализации. Такие требования приводят к использованию строго заданного интерфейса COM, полностью отделенного от реализации.

Фактически COM был замен технологией .NET, а Microsoft подталкивает к использованию .NET вместо COM.  Для обеспечения обратной совместимости со старым кодом .NET может осуществлять  взаимодействие с COM в обоих направлениях: .NET может вызывать методы COM объекта, а COM объект может использовать управляемый код .NET.

Эта функциональность называется COM-взаимодействие (COM Interoperability) или COM Interop. Программный интерфейс (API) COM-взаимодействия находится в пространстве имен (namespace) System.Runtime.InteropServices.

Рисунок 2. Модель COM-взаимодействия (COM Interoperability)

Рисунок 2. Модель COM-взаимодействия (COM Interoperability)

Приведенный ниже код COM-взаимодействия вызывает единственную функцию raw_factorial.

Пожалуйста обратите внимание на функции CoInitialize(), CoCreateInstance() и CoUninitialize() и функцию вызова интерфейса:

#include "windows.h"
#include <stdio.h>
#import "CSDll.tlb" named_guids

int main(int argc, char* argv[])
{
    HRESULT hRes = S_OK;
    CoInitialize(NULL);
    CSDll::IMyManagedInterface *pManagedInterface = NULL;

    hRes = CoCreateInstance(CSDll::CLSID_Class1, NULL, CLSCTX_INPROC_SERVER, 
     CSDll::IID_IMyManagedInterface, reinterpret_cast<void**> (&pManagedInterface));

    if (S_OK == hRes)
    {
        long retVal =0;
        hRes = pManagedInterface->raw_factorial(4, &retVal);
        printf("The value returned by the dll is %ld\n",retVal);
        pManagedInterface->Release();
    }

    CoUninitialize();
    return 0;
}

Для дальнейшего чтения о COM-взаимодействии рекомендую почитать документацию в статье Introduction to COM Interop и пример использования How to call C++ code from Managed, and vice versa (Interop), найденный мной на блоге MSDN.


2.2. Reverse P/Invoke (Обратный вызов неуправляемого кода)

Вызов неуправляемого кода (Platform Invoke, P/Invoke) разрешает .NET вызывать любую функцию неуправляемого языка в соответствии с ее сигнатурой. Это осуществляется выполнением нативной функции из .NET. Использование хорошо объяснено в статье Platform Invoke Tutorial.

В основе лежит использование атрибута DllImport для маркировки импортируемой функции:

// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(string c);
    [DllImport("msvcrt.dll")]
    internal static extern int _flushall();

    public static void Main() 
    {
        puts("Test");
        _flushall();
    }
}

Обратное действие может быть осуществлено через управляемую callback-функцию в неуправляемый код.

Это называется обратным вызовом неуправляемого кода (Reverse P/Invoke) и достигается реализацией публичного делегата (delegate) из управляемой среды и импортом вызываемой функции, реализованной в нативной DLL.

#include <stdio.h>
#include <string.h>
typedef void (__stdcall *callback)(wchar_t * str);
extern "C" __declspec(dllexport) void __stdcall caller(wchar_t * input, int count, callback call)
{
      for(int i = 0; i < count; i++)
      {
            call(input);
      }
}

Пример управляемого кода:

using System.Runtime.InteropServices;
public class foo
{
    public delegate void callback(string str);
    public static void callee(string str)
    {
        System.Console.WriteLine("Managed: " +str);
    }
    public static int Main()
    {
        caller("Hello World!", 10, new callback(foo.callee));
        return 0;
    }
    [DllImport("nat.dll",CallingConvention=CallingConvention.StdCall)]
    public static extern void caller(string str, int count, callback call);
}

Основной особенностью данного решения является то, что оно требует, чтобы исполнение начал управляемый код.

Для дальнейшего изучения почитайте Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks) и PInvoke-Reverse PInvoke and stdcall - cdecl.


2.3. C++ IJW

С++ взаимодействие (C++ interop), также известный как IJW (It Just Works) является специальной возможностью Managed Extensions for C++:

#using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;

#include <stdio.h>

int main()
{
   String * pStr = S"Hello World!";
   char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer(); 
   
   puts(pChars);
   
   Marshal::FreeHGlobal(pChars);
} 

Это решение может быть полезно тем, кто хочет использовать свой управляемый C++ код в native-приложениях. Для дальнешего изучения посмотрите статьи Interoperability in Managed Extensions for C++ и Using IJW in Managed C++.


2.4. C++/Cli wrapper class (Класс обертки C++/Cli)

Реализация класса посредством обертки C++/Cli заключается во вложении (embedding) или "обертке" (wrapping) управляемого класса другим классом, написанным в режиме C++/Cli.

Первым шагом к написанию DLL-обертки является написание класса на C++, который обертывает методы исходного управляемого класса. Класс обертки должен содержать хэндл объекта .NET при помощи шаблона gcroot<> и должен делегировать все вызовы из оригинального класса. Класс обертки является управляемым, поскольку он компилируется в формате IL (промежуточного языка).

Следующим шагом является написание native-класса на С++ с директивой "#pragma unmanaged", которая обертывает класс в IL-коде и делегирует все вызовы посредством директивы  __declspec(dllexport). Эти шаги создадут native-DLL, которая может быть использована любым native-приложением.

Пожалуйста, посмотрите пример реализации. Первым шагом является реализация кода на C#.

Этот пример класса Calculator содержит два public-метода:

public class Calculator
{
    public int Add(int first, int second)
    {
        return first + second;
    }
    public string FormatAsString(float i)
    {
        return i.ToString();
    }
}

Следующий шаг - написание управляемой обертки (managed wrapper), которая будет делегировать все методы класса Calculator:

#pragma once
#pragma managed

#include <vcclr.h>

class ILBridge_CppCliWrapper_Calculator {
private:
    //Aggregating the managed class
    gcroot<CppCliWrapper::Calculator^> __Impl;
public:
    ILBridge_CppCliWrapper_Calculator() {
        __Impl = gcnew CppCliWrapper::Calculator;
    }
    int Add(int first, int second) {
        System::Int32 __Param_first = first;
        System::Int32 __Param_second = second;
        System::Int32 __ReturnVal = __Impl->Add(__Param_first, __Param_second);
        return __ReturnVal;
    }
    wchar_t* FormatAsString(float i) {
        System::Single __Param_i = i;
        System::String __ReturnVal = __Impl->FormatAsString(__Param_i);
        wchar_t* __MarshaledReturnVal = marshal_to<wchar_t*>(__ReturnVal);
        return __MarshaledReturnVal;
    }
};

Обратите внимание на то, что ссылки на исходный класс Calculator размещаются при помощи инструкции gcnew и хранятся как шаблон gcroot<>. Все обернутые методы могут иметь те же имена и параметры, что и оригинальные методы, и возвращать значения, предшествующие __Param и __ReturnVal соответственно.

Теперь нужно реализовать класс на C++ (неуправляемый код), который обертывает C++/Cli и экспортирует native-методы в DLL.

Файл заголовка должен содержать определение класса с директивой __declspec(dllexport) и хранить указатель на класс обертки.

#pragma once
#pragma unmanaged

#ifdef THISDLL_EXPORTS
#define THISDLL_API __declspec(dllexport)
#else
#define THISDLL_API __declspec(dllimport)
#endif

//Forward-определение моста
class ILBridge_CppCliWrapper_Calculator;

class THISDLL_API NativeExport_CppCliWrapper_Calculator {
private:
    //Связывающий мост
    ILBridge_CppCliWrapper_Calculator* __Impl;
public:
    NativeExport_CppCliWrapper_Calculator();
    ~NativeExport_CppCliWrapper_Calculator();
    int Add(int first, int second);
    wchar_t* FormatAsString(float i);
};

И его реализация:

#pragma managed
#include "ILBridge_CppCliWrapper_Calculator.h"
#pragma unmanaged
#include "NativeExport_CppCliWrapper_Calculator.h"

NativeExport_CppCliWrapper_Calculator::NativeExport_CppCliWrapper_Calculator() {
    __Impl = new ILBridge_CppCliWrapper_Calculator;
}
NativeExport_CppCliWrapper_Calculator::~NativeExport_CppCliWrapper_Calculator()
{
    delete __Impl;
}
int NativeExport_CppCliWrapper_Calculator::Add(int first, int second) {
    int __ReturnVal = __Impl->Add(first, second);
    return __ReturnVal;
}
wchar_t* NativeExport_CppCliWrapper_Calculator::FormatAsString(float i) {
    wchar_t* __ReturnVal = __Impl->FormatAsString(i);
    return __ReturnVal;
}

Пошаговое руководство для создания этого класса обертки рассмотрено в статье .NET to C++ Bridge.

Полное руководство для создания оберток изложено в статье Mixing .NET and native code.  С общей информацией о декларации хэндлов в native-типах можно ознакомиться в статье How to: Declare Handles in Native Types.


2.5. Unmanaged exports (Экспорт неуправляемого кода)

Эта техника полностью описана в книге Expert .NET 2.0 IL Assembler, которую я рекомендую всем, кто хотел бы почитать про детали работы компилятора .NET. Основная идея заключается в открытии управляемых методов как экспорт неуправляемого кода (unmanaged exports) в управляемой DLL путем декомпиляции (при помощи ILDasm) скомпилированного модуля в IL-код, изменения таблиц VTable и VTableFixup модуля и повторная компиляция DLL при помощи ILAsm.

Эта задача может казаться очень сложной, но результатом будет создание DLL, которая может быть использована из любого неуправляемого native-кода. Нужно помнить о том, что эта сборка все еще является управляемой (managed), поэтому должна быть установлена платформа .NET. Пошаговое руководство изложено в статье Export Managed Code as Unmanaged.

После декомпиляции DLL посредством ILDasm мы получим исходный код на промежуточном языке IL.  Посмотрите на простой пример кода на IL с экспортом неуправляемого кода:

assembly extern mscorlib {}
..assembly UnmExports {}
..module UnmExports.dll
..corflags 0x00000002
..vtfixup [1] int32 fromunmanaged at VT_01
..data VT_01 = int32(0)
..method public static void foo()
{
..vtentry 1:1
..export [1] as foo
ldstr "Hello from managed world"
call void [mscorlib]System.Console::WriteLine(string)
ret
}

Строки исходника на IL, отвечающие за реализацию неуправляемого экспорта:

..vtfixup [1] int32 fromunmanaged at VT_01
..data VT_01 = int32(0)

и

..vtentry 1:1
..export [1] as foo

Первая часть отвечает за добавление точки входа в функцию в таблице VTableFixup и установку в VT_01 виртуального адреса функции. Вторая часть указывает на то, какой именно VTEntry будет использоваться для этой функции и псевдоним экспортируемой функции (export alias).

В процессе реализации DLL у нас не было необходимости реализовывать никакой дополнительный код за исключением обычного управляемого кода C# в DLL. Как указано в книге Expert .NET 2.0 IL Assembler, этот метод полностью открывает мир управляемого кода со всей его безопасностью и библиотеками классов для программ, написанных на native-коде.

Недостатком является то, что работа с промежуточным IL-языком подходит не для всех.  Я был уверен в том, что мне придется писать обертку в виде класс на C++, пока я не нашел шаблон Unmanaged exports, созданный Robert Giesecke http://sites.google.com/site/robertgiesecke/, который позволяет использовать экспорт неуправляемого кода (unmanaged exports) без необходимости работы с IL-кодом.


3. Шаблон Unmanaged exports на C#

В шаблонах проектов по созданию экспорта неуправляемого кода от R.Giesecke используется MSBuild task, который автоматически добавляет создание упомянутых выше модификаций для VT, поэтому нет необходимости изменения кода на IL. Нужно только загрузить проект шаблона (в виде zip-файла) и скопировать его содержимое в папку ProjectTemplate среды Visual Studio.

После компиляции проекта полученную DLL можно без проблем использовать в MetaTrader 5, в следующих разделах я приведу примеры.


4. Примеры

Задача выяснения того, как передавать переменные, массивы и структуры между MetaTrader 5 и C# оказалась весьма непростой, поэтому я полагаю, что информация, изложенная здесь, позволит вам сэкономить много времени. Все примеры были скомпилированы в Windows Vista с .NET 4.0 и Visual C# Express 2010. К статье прилагается пример DLL и кода на MQL5, который вызывает функции из DLL, написанной на C#.


4.1. Пример 1. Сложение двух переменных типа integer, double или float в функции DLL и возврат результата в MetaTrader 5

using System;
using System.Text;
using RGiesecke.DllExport;
using System.Runtime.InteropServices;

namespace Testme
{
    class Test
    {

        [DllExport("Add", CallingConvention = CallingConvention.StdCall)]
        public static int Add(int left, int right)
        {
            return left + right;
        }

        [DllExport("Sub", CallingConvention = CallingConvention.StdCall)]
        public static int Sub(int left, int right)
        {
            return left - right;
        }

        [DllExport("AddDouble", CallingConvention = CallingConvention.StdCall)]
        public static double AddDouble(double left, double right)
        {
            return left + right;
        }

        [DllExport("AddFloat", CallingConvention = CallingConvention.StdCall)]
        public static float AddFloat(float left, float right)
        {
            return left + right;
        }

    }
}

Как вы, возможно, заметили, каждой экспортируемой функции предшествует директива DllExport. Первый параметр описывает псевдоним экспортируемой функции, а второй параметр задает способ вызова (calling convention), для MetaTrader 5 следует использовать CallingConvention.StdCall.

Код на MQL5, который импортирует и использует функции, экспортированные из DLL, является простым и не отличается от кода, написанного в C++. Сначала нужно объявить импортированные функции внутри блока #import для указания списка функций из DLL, которые в дальнейшем будут вызываться из кода на MQL5:

//+------------------------------------------------------------------+
//|                                  UnmanagedExportsDLLExample1.mq5 |
//|                                      Copyright 2010, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#import "Testme.dll"
   int Add(int left,int right);
   int Sub(int left,int right);
   float AddFloat(float left,float right);
   double AddDouble(double left,double right);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   for(int i=0; i<3; i++)
     {
      Print(Add(i,666));
      Print(Sub(666,i));
      Print(AddDouble(666.5,i));
      Print(AddFloat(666.5,-i));
     }
  }
//+------------------------------------------------------------------+

Результат:

2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 664.50000
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 668.5
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 664
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 668
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 665.50000
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 667.5
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 665
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 667
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 666.50000
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 666.5
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 666
2011.01.30 21:28:18     UnmanagedExportsDLLExample1 (EURUSD,M1) 666


4.2. Пример 2. Доступ к одномерному массиву

        [DllExport("Get1DInt", CallingConvention = CallingConvention.StdCall)]
        public static int Get1DInt([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]  int[] tab, int i, int idx)
        {
            return tab[idx];
        }

        [DllExport("Get1DFloat", CallingConvention = CallingConvention.StdCall)]
        public static float Get1DFloat([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]  float[] tab, int i, int idx)
        {
            return tab[idx];
        }

        [DllExport("Get1DDouble", CallingConvention = CallingConvention.StdCall)]
        public static double Get1DDouble([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]  double[] tab, int i, int idx)
        {
            return tab[idx];
        }

Для передачи одномерного массива в директиве MarshalAs в качестве первого параметра нужно указать UnmanagedType.LPArray, а в качестве второго - SizeParamIndex. Параметр SizeParamIndex указывает на то, какой параметр (начиная с 0) является параметром, содержащим размер массива.

В примерах, описанных выше массив имеет размер i, а в качестве значения, возвращаемого функцией, используется элемент массива с индексом idx.

Пример доступа к массиву на MQL5 приведен ниже:

//+------------------------------------------------------------------+
//|                                  UnmanagedExportsDLLExample2.mq5 |
//|                                      Copyright 2010, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#import "Testme.dll"  
   int Get1DInt(int &t[],int i,int idx);
   float Get1DFloat(float &t[],int i,int idx);
   double Get1DDouble(double &t[],int i,int idx);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   int tab[3];
   tab[0] = 11;
   tab[1] = 22;
   tab[2] = 33;

   float tfloat[3]={0.5,1.0,1.5};
   double tdouble[3]={0.5,1.0,1.5};

   for(int i=0; i<3; i++)
     {
      Print(tab[i]);
      Print(Get1DInt(tab,3,i));
      Print(Get1DFloat(tfloat,3,i));
      Print(Get1DDouble(tdouble,3,i));

     }
  }
//+------------------------------------------------------------------+

Результат:

2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 1.5
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 1.50000
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 33
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 33
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 1
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 1.00000
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 22
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 22
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 0.5
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 0.50000
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 11
2011.01.30 21:46:25     UnmanagedExportsDLLExample2 (EURUSD,M1) 11

 

4.3. Пример 3. Заполнение одномерного массива и возврат результата в MetaTrader 5

        [DllExport("SetFiboArray", CallingConvention = CallingConvention.StdCall)]
        public static int SetFiboArray([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]
        int[] tab, int len, [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] res)
        {
            res[0] = 0;
            res[1] = 1;
            
            if (len < 3) return -1;
            for (int i=2; i<len; i++)
                res[i] = res[i-1] + res[i-2];
            return 0;
        }

В этом примере используются два массива для иллюстрации обозначения входных параметров. Если требуется возврат элементов массива (переданных по ссылке) в MetaTrader 5, достаточно указать атрибуты [In, Out,]  перед атрибутом MarshalAs.

//+------------------------------------------------------------------+
//|                                  UnmanagedExportsDLLExample3.mq5 |
//|                                      Copyright 2011, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#import "Testme.dll"  
    int SetFiboArray(int& t[], int i, int& o[]);
#import


//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
int fibo[10];
static int o[10];

   for (int i=0; i<4; i++)
   { fibo[i]=i; o[i] = i; }
   
   SetFiboArray(fibo, 6, o);
   
   for (int i=0; i<6; i++)
      Print(IntegerToString(fibo[i])+":"+IntegerToString(o[i]));
      
  }
//+------------------------------------------------------------------+

Результат:

2011.01.30 22:01:39     UnmanagedExportsDLLExample3 (EURUSD,M1) 0:5
2011.01.30 22:01:39     UnmanagedExportsDLLExample3 (EURUSD,M1) 0:3
2011.01.30 22:01:39     UnmanagedExportsDLLExample3 (EURUSD,M1) 3:2
2011.01.30 22:01:39     UnmanagedExportsDLLExample3 (EURUSD,M1) 2:1
2011.01.30 22:01:39     UnmanagedExportsDLLExample3 (EURUSD,M1) 1:1
2011.01.30 22:01:39     UnmanagedExportsDLLExample3 (EURUSD,M1) 0:0


4.4. Пример 4. Доступ к двумерному массиву

        public static int idx(int a, int b) {int cols = 2; return a * cols + b; }
 
        [DllExport("Set2DArray", CallingConvention = CallingConvention.StdCall)]
        public static int Set2DArray([In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int len)
        {
            tab[idx(0, 0)] = 0;
            tab[idx(0, 1)] = 1;
            tab[idx(1, 0)] = 2;
            tab[idx(1, 1)] = 3;
            tab[idx(2, 0)] = 4;
            tab[idx(2, 1)] = 5;
            
            return 0;
        }

Маршаллинг двумерного массива не является столь простым, но я использовал трюк: передавал двумерный массив как одномерный и осуществлял  доступ к элементам массива при помощи дополнительной функции idx.

//+------------------------------------------------------------------+
//|                                  UnmanagedExportsDLLExample4.mq5 |
//|                                      Copyright 2011, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#import "Testme.dll"
   int Set2DArray(int &t[][2],int i);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   int t2[3][2];

   Set2DArray(t2,6);

   for(int row=0; row<3; row++)
      for(int col=0; col<2; col++)
         Print("t2["+IntegerToString(row)+"]["+IntegerToString(col)+"]="+IntegerToString(t2[row][col]));

  }
//+------------------------------------------------------------------+

Результат:

2011.01.30 22:13:01     UnmanagedExportsDLLExample4 (EURUSD,M1) t2[2][1]=5
2011.01.30 22:13:01     UnmanagedExportsDLLExample4 (EURUSD,M1) t2[2][0]=4
2011.01.30 22:13:01     UnmanagedExportsDLLExample4 (EURUSD,M1) t2[1][1]=3
2011.01.30 22:13:01     UnmanagedExportsDLLExample4 (EURUSD,M1) t2[1][0]=2
2011.01.30 22:13:01     UnmanagedExportsDLLExample4 (EURUSD,M1) t2[0][1]=1
2011.01.30 22:13:01     UnmanagedExportsDLLExample4 (EURUSD,M1) t2[0][0]=0


4.5. Пример 5. Замена содержимого строки

  	 [DllExport("ReplaceString", CallingConvention = CallingConvention.StdCall)]
        public static int ReplaceString([In, Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder str,
        [MarshalAs(UnmanagedType.LPWStr)]string a, [MarshalAs(UnmanagedType.LPWStr)]string b)
        {
            str.Replace(a, b);

            if (str.ToString().Contains(a)) return 1;
            else  return 0;
        }

Несмотря на то, что этот пример короткий, его реализация заняла достаточно много времени, поскольку попытки использования атрибутов [In,Out] или ключевых слов ref и out не увенчались успехом.

Решение заключается в использовании StringBuilder вместо переменной string.

//+------------------------------------------------------------------+
//|                                  UnmanagedExportsDLLExample5.mq5 |
//|                                      Copyright 2011, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#import "Testme.dll"   
   int ReplaceString(string &str,string a,string b);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   string str="A quick brown fox jumps over the lazy dog";
   string stra = "fox";
   string strb = "cat";


   Print(str);
   Print(ReplaceString(str,stra,strb));
   Print(str);

  }
//+------------------------------------------------------------------+

Результат:

2011.01.30 22:18:36     UnmanagedExportsDLLExample5 (EURUSD,M1) A quick brown cat jumps over the lazy dog
2011.01.30 22:18:36     UnmanagedExportsDLLExample5 (EURUSD,M1) 0
2011.01.30 22:18:36     UnmanagedExportsDLLExample5 (EURUSD,M1) A quick brown fox jumps over the lazy dog


4.6. Пример 6. Передача и изменение структуры типа MqlTick

	 private static List<MqlTick> list;

	 [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct MqlTick
        {
            public Int64 Time;
            public Double Bid;
            public Double Ask;
            public Double Last;
            public UInt64 Volume;
        }

        [DllExport("AddTick", CallingConvention = CallingConvention.StdCall)]
        public static int AddTick(ref MqlTick tick, ref double bidsum)
        {
            bidsum = 0.0;

            if (list == null) list = new List<MqlTick>();

            tick.Volume = 666;
            list.Add(tick);

            foreach (MqlTick t in list) bidsum += t.Ask;

            return list.Count;
        }

Передача по ссылке структуры типа MqlTick, маркируется при помощи ключевого слова ref. Описанию структуры MqlTick предшествует атрибут [StructLayout (LayoutKind.Sequential, Pack =1)].

Параметр Pack указывает на выравнивание данных в структуре, для деталей см. описание StructLayoutAttribute.Pack Field.

//+------------------------------------------------------------------+
//|                                  UnmanagedExportsDLLExample6.mq5 |
//|                                      Copyright 2011, Investeo.pl |
//|                                                http:/Investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, Investeo.pl"
#property link      "http:/Investeo.pl"
#property version   "1.00"

#import "Testme.dll"
   int AddTick(MqlTick &tick, double& bidsum);
#import
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& open[],
                const double& high[],
                const double& low[],
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
  {
//---
   MqlTick newTick;
   double bidsum;
   
   SymbolInfoTick(Symbol(), newTick);
   
   Print("before = " + IntegerToString(newTick.volume));
   
   Print(AddTick(newTick, bidsum));
   
   Print("after = " + IntegerToString(newTick.volume) + " : " + DoubleToString(bidsum));
   
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Результат:

2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) after = 666 : 8.167199999999999
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) 6
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) before = 0
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) after = 666 : 6.806
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) 5
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) before = 0
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) after = 666 : 5.4448
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) 4
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) before = 0
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) after = 666 : 4.0836
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) 3
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) before = 0
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) after = 666 : 2.7224
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) 2
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) before = 0
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) after = 666 : 1.3612
2011.01.30 23:59:05     TickDLLSend (EURUSD,M1) 1
2011.01.30 23:59:04     TickDLLSend (EURUSD,M1) before = 0

 

Выводы

В данной статье я представил различные методы взаимодействия между кодом, написанным на MQL5, и управляемым кодом на C#.

Также я подготовил несколько примеров маршалинга структур MQL5 для C# и примеров вызова экспортированных функций DLL в скриптах на MQL5. Я полагаю, приведенные примеры могут служить основой для дальнейших исследований аспектов написания DLL в управляемом коде.

Эта статья также открывает двери для использования в MetaTrader 5 множества библиотек, уже реализованных на C#. Для более глубокого ознакомления, пожалуйста, прочитайте статьи в разделе "Литература".


Для тестирования работы прилагаемых программ разместите файлы в следующих каталогах:

MQL5\Libraries\testme.dll
MQL5\Scripts\unmanagedexportsdllexample1.mq5
MQL5\Scripts\unmanagedexportsdllexample2.mq5
MQL5\Scripts\unmanagedexportsdllexample3.mq5
MQL5\Scripts\unmanagedexportsdllexample4.mq5
MQL5\Scripts\unmanagedexportsdllexample5.mq5
MQL5\Experts\unmanagedexportsdllexample6.mq5


Литература

  1. P/Invoke и 64-битная разработка
  2. Использование P/Invoke: прячем кнопку Пуск и панель задач в Windows
  3. .NET в unmanaged окружении: platform invoke или что такое LPTSTR
  4. Маршалинг данных при вызове неуправляемого кода
  5. Маршалинг взаимодействия
  6. Взаимодействие с неуправляемым кодом
  7. Разделы практических руководств, описывающие взаимодействие с неуправляемым кодом
  8. Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications
  9. Interoperating with Unmadged Coded
  10. Introduction to COM Interop
  11. Component Object Model (COM)
  12. Exporting from a DLL Using __declspec(dllexport)
  13. How to: Declare Handles in Native Types
  14. How to call C++ code from Managed, and vice versa (Interop)
  15. Reverse P/Invoke and exception 
  16. How to call a managed DLL from native Visual C++ code in Visual Studio.NET or in Visual Studio 2005
  17. Platform Invoke Tutorial
  18. PInvoke-Reverse PInvoke and __stdcall - __cdecl
  19. Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks)
  20. Mixing .NET and native code
  21. Export Managed Code as Unmanaged
  22. Understanding Classic COM Interoperability With .NET Applications
  23. Managed Extensions for C++ Programming
  24. Robert Giesecke's site
  25. MSBuild Tasks
  26. Common Language Runtime

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/249

Последние комментарии | Перейти к обсуждению на форуме трейдеров (42)
Trader Fortis
Trader Fortis | 28 февр. 2021 в 22:16
Работает ли такой подход для .NET 5 версии?
Igor Makanu
Igor Makanu | 1 мар. 2021 в 06:15
Trader Fortis:
Работает ли такой подход для .NET 5 версии?

не проверял, но сомневаюсь, что будет работать

МТ4 очень сложно взаимодействует с C# - постоянно какие подводные камни

проще на МТ5 перейти

но если принципиально МТ4 использовать, то как вариант - запустите по методике из статьи .dll на C# , а в ней в отдельном потоке запустите любой код C# и организуйте обмен , я так запускал 64-разрядные библиотеки C# 

Trader Fortis
Trader Fortis | 1 мар. 2021 в 18:04

Спасибо за ответ!

У меня вопрос как раз по MT5 - там заявлена нативная поддержка .NET библиотек, но у меня не получается запустить библиотеку на .NET5, только на .NET4.

Igor Makanu
Igor Makanu | 1 мар. 2021 в 20:23
Trader Fortis:

Спасибо за ответ!

У меня вопрос как раз по MT5 - там заявлена нативная поддержка .NET библиотек, но у меня не получается запустить библиотеку на .NET5, только на .NET4.

у меня тоже в прошлом году не работал .NET5 под МТ5, не разбирался

спросите у разработчиков будет ли поддержка и когда, в топике новых релизов МТ5, например тут https://www.mql5.com/ru/forum/363680

Moatle Thompson
Moatle Thompson | 19 апр. 2023 в 16:30
The wrapper function , how is it done.
Применение псевдошаблонов как альтернатива шаблонов С++ Применение псевдошаблонов как альтернатива шаблонов С++
В статье описывается прием программирования, позволяющий обойтись без механизма шаблонов, при этом сохранив стиль программирования, присущий им. Рассмотрены особенности реализации шаблонов пользовательскими методами, прилагается готовый к эксплуатации код скрипта, создающий код на основе указанных шаблонов.
Торговый эксперт по книге Б. Вильямса "Новые измерения в биржевой торговле" Торговый эксперт по книге Б. Вильямса "Новые измерения в биржевой торговле"
В данной статье я расскажу о создании торгового эксперта по книге Б. Вильямса "Новые измерения в биржевой торговле" для платформы MetaTrader 5 на языке MQL5. Сама стратегия хорошо известна и до сих пор вызывает споры среди трейдеров о ее работоспособности. В статье рассматриваются торговые сигналы системы Б. Вильямса, особенности их реализации и результаты тестирования на исторических данных.
Электронные таблицы на MQL5 Электронные таблицы на MQL5
В статье описан класс двумерного динамического массива, имеющий в своем составе разнотипные данные первого измерения. Хранение данных в виде таблиц удобно при решении большого класса задач по упорядочиванию, хранению и оперированию связными данными разных типов. К статье приложен код класса, реализующего функционал работы с таблицами.
Универсальная регрессионная модель для прогнозирования рыночной цены Универсальная регрессионная модель для прогнозирования рыночной цены
Рыночная цена складывается в результате устойчивого равновесия между спросом и предложением, а те, в свою очередь, зависят от множества экономических, политических и психологических факторов. Непосредственный учет всех составляющих осложнен как различием природы, так и причиной воздействия этих факторов. На основании разработанной регрессионной модели в статье сделана попытка прогнозирования рыночной цены.