Offenlegen von C#-Code in MQL5 mithilfe nicht gemanagter Exporte

investeo | 14 März, 2016

Einleitung

Lange Zeit suchte ich nach einer einfachen Lösung, die es mir ermöglichen würde, gemanagte C#-DLLs in MQL5 zu nutzen. Nach der Lektüre zahlreicher Beiträge war ich bereit, einen C++-Wrapper für gemanagte DLLs zu nutzen, als ich auf eine brillante Lösung stieß, die mir viele Stunden Arbeit ersparte.

Die Lösung bot ein einfaches Beispiel für den Export von gemanagtem C#-Code für die Nutzung in einer ungemanagten Anwendung. In diesem Beitrag werde ich einen Hintergrund zu gemanagten DLLs liefern, beschreiben, warum kein direkter Zugriff darauf aus MetaTrader möglich ist, und die Lösungen vorstellen, die ich gefunden habe, die die Nutzung von gemanagtem Code aus MetaTrader ermöglichen.

Ich werde ein Beispiel für den einfachen Gebrauch von Vorlagen ungemanagter Exporte bereitstellen und mit meinen Funden fortfahren. Dies sollte einen soliden Hintergrund für jeden liefern, der versucht, C#-DLL-Codes in MetaTrader 5 zu nutzen.


1. Gemanagter und ungemanagter Code

Da den meisten Lesern der Unterschied zwischen gemanagtem und ungemanagtem Code vermutlich nicht bekannt ist, beschreibe ich ihn in einigen wenigen Sätzen. Für die Umsetzung von Handelsregeln, Indikatoren, Expert Advisors und Scripts nutzt MetaTrader grundsätzlich die MQL-Sprache. Allerdings kann man bereits implementierte Bibliotheken in anderen Sprachen nutzen und sie während der Laufzeit dynamisch verbinden. Diese Bibliotheken werden auch als DLLs oder Dynamic Link Libraries bezeichnet.

Die Bibliotheken sind im Wesentlichen binäre Dateien, die kompilierte Quellcodes beinhalten, die durch eine Reihe externer Programme aufgerufen werden können, um bestimmte Operationen durchzuführen. Beispielsweise können neuronale Netzwerkbibliotheken Funktionen für Schulungen und Tests für neuronale Netzwerke exportieren, Derivatbibliotheken können Berechnungen verschiedener Derivate exportieren, Matrixbibliotheken können Operationen auf Matrizen exportieren. DLLs für MetaTrader werden immer beliebter, da sie es möglich machen, Teile der Implementierung von Indikatoren oder Expert Advisors auszublenden. Einer der Hauptgründe für die Nutzung von Bibliotheken ist die Wiederverwendung von bestehendem Code, ohne ihn immer wieder neu implementieren zu müssen.

Vor der Einführung von .NET konnten alle DLLs, die in Visual Basic, Delphi, VC++ kompiliert wurden, sei es COM, Win32 oder gewöhnliches C++, direkt durch das Betriebssystem ausgeführt werden. Diesen Code nennen wir ungemanagten oder nativen Code. Dann erschient .NET und lieferte eine völlig andere Art von Umgebung.

Der Code wird durch .NET Common Language Runtime (CLR) kontrolliert (oder gemanagt). CLR-Compiler erzeugen aus dem Quellcode Code, der in unterschiedlichen Sprachen geschrieben sein kann, und Metadaten in Common Intermediate Language (CIL).

CIL ist eine maschinenunabhängige übergeordnete Sprache und die Metadaten liefern eine vollständige Beschreibung von Objekttypen, die durch CIL gemäß der Common Type Specification (CTS) beschrieben werden. Da CLR alles über die Typen weiß, kann sie uns eine gemanagte Ausführungsumgebung bereitstellen. Das Management kann als Garbage Collection – automatische Speicherverwaltung, Objektlöschung und Gewährleistung von Sicherheit – betrachtet werden, die vor häufigen Fehlern in nativen Sprachen schützt, die zur Ausführung von Fremdcode mit Administratorrechten oder einfach zum Überschreiben des Speichers führen könnten.

Es muss erwähnt werden, dass CIL-Code niemals direkt ausgeführt wird. Er wird durch JIT-Kompilierung (Just-In-Time) oder eine Vorabkompilierung von CIL in nativen Maschinencode übersetzt. Für jemanden, der dies zum ersten Mal liest, kann die Vorstellung eines gemanagten Codes verwirrend erscheinen. Deshalb führe ich unten den allgemeinen Arbeitsfluss innerhalb von CLR auf:

 

 

Abbildung 1. Common Language Runtime 

2. Möglichkeiten der Umsetzung des Zugriffs auf gemanagten Code aus MQL5

Im folgenden Abschnitt beschreibe ich Methoden, die den Zugriff auf gemanagten Code aus ungemanagtem Code ermöglichen.

Ich denke, sie alle sollten erwähnt werden, weil andere Anwender möglicherweise eine andere Methode nutzen möchten als ich. Die verwendeten Methoden sind COM Interop, Reverse P/Invoke, C++ IJW, C++/Cli wrapper class und Unmanaged Exports.

2.1. COM Interop 

Component Object Model (COM) ist ein binärer Interface-Standard, der von Microsoft in den frühen Neunzigern eingeführt wurde. Die Grundidee dieser Technologie ist es, die Nutzung von Objekten, die in anderen Programmiersprachen erstellt wurden, durch jedes andere COM-Objekt zu ermöglichen, ohne seine interne Umsetzung zu kennen. Diese Anforderung erzwingt die Umsetzung eines strikt definierten Interfaces des COM, das vollständig von der Implementierung getrennt ist.

Tatsächlich wurde COM durch die .NET-Technologie überholt und Microsoft drängt auf die Nutzung von .NET anstelle von COM. Um die Abwärtskompatibilität mit älterem Code zu ermöglichen, kann .NET in beiden Richtungen mit COM kooperieren, das heißt, .NET kann COM-Methoden aufrufen und COM-Objekte können gemanagten .NET-Code nutzen.

Diese Funktion heißt COM-Interoperabilität oder COM Interop. Die COM-Interop-API befindet sich im gemanagten Namensraum System.Runtime.InteropServices.

 

Abbildung 2. Modell der COM-Interoperabilität

Abbildung 2. Modell der COM-Interoperabilität 

Der folgende COM-Interop-Code ruft die Einzelfunktion raw_factorial auf.

Bitte beachten Sie die Funktionen CoInitialize(), CoCreateInstance() und CoUninitialize() und die Aufruffunktion des Interfaces:

#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;
}

Bitte lesen Sie für weitere Informationen über COM Interop die detaillierte Dokumentation unter Introduction to COM Interop und das Anwendungsbeispiel, das ich im MSDN-Blog gefunden habe: How to call C++ code from Managed, and vice versa (Interop) (englisch).

2.2. Reverse P/Invoke

Platform Invoke, auch P/Invoke genannt, ermöglicht es .NET, jede beliebige Funktion in jeder ungemanagten Sprache aufzurufen, solange ihre Signatur neu deklariert wird. Dies wird durch die Ausführung eines nativen Funktions-Pointers aus .NET bewerkstelligt. Die Verwendung wird unter Lernprogramm für die Plattformaktivierung ausführlich beschrieben.

Die grundlegende Verwendung ist die Nutzung des Attributs DllImport zum Markieren der zu importierenden Funktion:

// 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();
    }
}

Der umgekehrte Vorgang kann als Bereitstellung einer gemanagten delegierten callback-Funktion für nicht gemanagten Code beschrieben werden.

Dies wird als Reverse P/Invoke bezeichnet und wird durch die Implementierung einer öffentlichen delegierten Funktion in einer gemanagten Umgebung und den Import der aufrufenden Funktion in der nativen DLL erreicht: 

#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);
      }
}

Das Beispiel des gemanagten Codes sieht so aus:

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);
}

Das wichtigste Merkmal dieser Lösung ist es, dass sie voraussetzt, dass die gemanagte Seite die Interaktion einleitet.

Bitte lesen Sie für weitere Einzelheiten Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks) und PInvoke-Reverse PInvoke and stdcall - cdecl (englisch).


2.3. C++ IJW

C++ Interop, auch als It Just Works (IJW) bekannt, ist eine C++-spezifische Funktion aus 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);
}

Diese Lösung kann nützlich sein, wenn Sie Ihr gemanagtes C++ in einer nicht gemanagten Anwendung verwenden möchten. Bitte lesen Sie die Details dazu unter Interoperability in Managed Extensions for C++ und Using IJW in Managed C++ (englisch).

2.4. C++/Cli wrapper class

Die Implementierung der C++/Cli-Wrapper-Klasse erhält ihren Namen durch das Einbetten oder Wrapping einer gemanagten Klasse durch eine andere im C++-/Cli-Modus geschriebene Klasse. Der erste Schritt zum Schreiben des DLL-Wrappers ist das Schreiben der C++-Klasse, die die Methoden der ursprünglichen gemanagten Klasse umschließt.

Die Wrapper-Klasse muss ein Handle des .NET-Objekts mit dem Template gcroot<> enthalten und alle Aufrufe aus der ursprünglichen Klasse delegieren. Die Wrapper-Klasse wird im IL-Format (Zwischensprache) kompiliert und gilt deshalb als gemanagt.

Der nächste Schritt ist das Schreiben der nativen C++-Klasse mit der ungemanagten Direktive #pragma, die die IL-Klasse umschließt und alle Aufrufe durch die Direktive __declspec(dllexport) delegiert. Mit diesen Schritten wird die native C++-DLL erstellt, die durch jede ungemanagte Anwendung genutzt werden kann.

Bitte sehen Sie sich das Umsetzungsbeispiel an. Der erste Schritt ist die Umsetzung des C#-Codes.

Die Beispielklasse Calculator enthält zwei public-Methoden: 

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

Der nächste Schritt ist das Schreiben eines gemanagten Wrappers, der alle Methoden aus der Calculator-Klasse delegiert:

#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;
    }
};

Bitte beachten Sie, dass der Verweis auf die ursprüngliche Calculator-Klasse mithilfe der Instruktion gcnew und als Template gcroot<> gespeichert wird. Alle eingeschlossenen Methoden können die gleichen Namen haben wie die Originale und den Parametern und Ausgabewerten wird __Param bzw. __ReturnVal vorangestellt.

Nun muss die ungemanagte C++-Klasse, die die C++/Cli umschließt und die native C++-DLL exportiert, implementiert werden.

Die Kopfzeilendatei muss die Klassendefinition mit der Direktive __declspec(dllexport) enthalten und den Pointer zur Wrapper-Klasse speichern.

#pragma once
#pragma unmanaged

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

//Forward declaration for the bridge
class ILBridge_CppCliWrapper_Calculator;

class THISDLL_API NativeExport_CppCliWrapper_Calculator {
private:
    //Aggregating the bridge
    ILBridge_CppCliWrapper_Calculator* __Impl;
public:
    NativeExport_CppCliWrapper_Calculator();
    ~NativeExport_CppCliWrapper_Calculator();
    int Add(int first, int second);
    wchar_t* FormatAsString(float i);
};

Und ihre Implementierung:

#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;
}

Eine ausführliche Anleitung zur Erstellung dieses Wrappers finden Sie im Beitrag .NET to C++ Bridge (englisch).

Eine vollständige Anleitung für die Erstellung von Wrappern finden sie unter Mixing .NET and native code (englisch). Allgemeine Informationen zum Deklarieren von Handles in nativen Typen finden Sie unter Gewusst wie: Deklarieren von Handles in systemeigenen Typen.

2.5. Unmanaged exports 

Diese Technik wird im Buch Expert .NET 2.0 IL Assembler, das ich jedem, der über Details des .NET-Compilers nachlesen möchte, ans Herz legen möchte, vollständig beschrieben. Die Grundidee ist die Offenlegung gemanagter Methoden als ungemanagte Exporte einer gemanagten DLL durch die Dekompilierung eines bereits kompilierten Moduls in IL-Code mithilfe von ILDasm, die Änderung der Tabellen VTable und VTableFixup des Moduls und die erneute Kompilierung der DLL mithilfe von ILAsm.

Diese Aufgabe mag abschreckend wirken, doch das Ergebnis ist eine DLL, die innerhalb jeder beliebigen ungemanagten Anwendung genutzt werden kann. Man muss daran denken, dass es sich immer noch um einen gemanagten Aufbau handelt, also muss .NET Framework installiert sein.  Ein Schritt-für-Schritt-Tutorial hierzu steht unter Export Managed Code as Unmanaged (englisch) zur Verfügung.

Nach der Dekompilierung der DLL durch ILDasm erhalten wir den Quellcode in der IL-Sprache. Bitte sehen Sie sich das unten abgebildete einfache Beispiel eines IL-Codes mit ungemanagtem Export an:

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
}

Die Zeilen des IL-Quellcodes, die für die Umsetzung ungemanagter Exporte verantwortlich sind, sind:

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

und

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

Im ersten Teil wird der Eingangspunkt in der Tabelle VTableFixup und die Einstellung der virtuellen Adresse VT_01 in der Funktion definiert. Der zweite Teil legt fest, welcher VTEntry für diese Funktion genutzt werden soll, und bestimmt den Export-Alias für die zu exportierende Funktion. 

Die Vorteile dieser Lösung sind, dass wir während der Implementierungsphase der DLL keinen zusätzlichen Code, abgesehen von der üblichen gemanagten C#-DLL, implementieren müssen und, wie es im Buch verdeutlich wird, dass diese Methode die gemanagte Welt mit all ihrer Sicherheit und ihren Klassenbibliotheken für ungemanagte Clients vollständig öffnet.

Der Nachteil ist, dass die Arbeit mit der .NET-Assemblersprache nicht für alle geeignet ist. Ich war überzeugt, dass ich stattdessen eine C++-Wrapper-Klasse schreiben würde, bis ich die Unmanaged-Exports-Vorlage von Robert Giesecke fand: http://sites.google.com/site/robertgiesecke/. Diese Vorlage ermöglicht die Nutzung von Unmanaged Exports ohne die Notwendigkeit, sich mit IL-Code auseinanderzusetzen.

3. Unmanaged-Exports-Vorlage in C#

Die Unmanaged-Exports-Vorlage für C#-Projekte von R. Giesecke nutzt MSBuild task, das automatisch die erforderlichen VT-Anpassungen nach dem Aufbau einfügt, sodass der IL-Code keine Anpassung mehr benötigt. Das Vorlagenpaket muss nur als Zip-Datei heruntergeladen und in den ProjectTemplates-Ordner von Visual Studio kopiert werden.

Nach der Kompilierung des Projekts kann die entstandene DLL-Datei problemlos von MetaTrader importiert werden. Beispiele dafür stelle ich in den nachfolgenden Abschnitten bereit.

4. Beispiele

Es war ziemlich schwierig, herauszufinden, wie Variablen, Arrays und Strukturen zwischen MetaTrader und C# mithilfe der korrekten Marshalling-Methode übergeben werden, und ich denke, die hier bereitgestellten Informationen werden Ihnen viel Zeit ersparen. Alle Beispiele wurden unter Windows Vista mit .NET 4.0 und Visual C# Express 2010 kompiliert. Ich hänge ebenfalls eine Beispiel-DLL mit MQL5-Code an, der Funktionen aus der C#-DLL aufruft.

4.1. Beispiel 1. Addieren von zwei Variablen der Typen integer, double oder float und Ausgeben des Ergebnisses in MetaTrader

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;
        }

    }
}

Ihnen dürfte aufgefallen sein, dass jeder exportierten Funktion die Direktive DllExport vorangestellt wird. Der erste Parameter beschreibt das Alias der exportierten Funktion und der zweite Parameter bestimmt die Aufrufkonvention. Für MetaTrader müssen wir CallingConvention.StdCall verwenden.

Der MQL5-Code, der die aus der DLL exportierten Funktionen importiert und nutzt, ist einfach und unterscheidet sich nicht von anderen in nativem C++ geschriebenen DLLs. Als Erstes müssen die importierten Funktionen innerhalb des Blocks #import deklariert und bestimmt werden, welche Funktionen aus der DLL später aus dem MQL5-Code genutzt werden können:

//+------------------------------------------------------------------+
//|                                  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));
     }
  }
//+------------------------------------------------------------------+

Ergebnis 

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. Beispiel 2. Zugriff auf eindimensionale Arrays

        [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];
        }

Für die Übergabe eines eindimensionalen Arrays muss die Direktive MarshalAs UnmanagedType.LPArray als ersten Parameter und SizeParamIndex als zweiten Parameter übergeben. SizeParamIndex bestimmt, welcher Parameter (ab 0) der Parameter ist, der die Array-Größe enthält.

In den oben aufgeführten Beispielen ist i die Array-Größe und idx der Index des auszugebenen Elements.

Hier sehen Sie einen Beispiel-MQL5-Code, der den Zugriff auf ein Array nutzt:

//+------------------------------------------------------------------+
//|                                  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));

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

Ergebnis

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. Beispiel 3. Befüllen eines eindimensionalen Arrays und Rückgabe an MetaTrader

        [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;
        }

Dieses Beispiel nutzt zwei Eingabe-Arrays zum Vergleich der Konvention der Eingabeparameter. Falls veränderte Elemente an MetaTrader zurückgegeben werden müssen (Übergabe nach Verweis), reicht es, die Attribute [In, Out,] dem Attribut MarshalAs voranzustellen.

//+------------------------------------------------------------------+
//|                                  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]));
      
  }
//+------------------------------------------------------------------+

 Ergebnis

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. Beispiel 4. Zugriff auf zweidimensionale Arrays

        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;
        }

Die Übergabe zweidimensionaler Arrays ist nicht so einfach, doch ich habe einen Trick genutzt, nämlich die Übergabe des 2D-Arrays als eindimensional und den Zugriff auf Array-Elemente durch die Hilfsfunktion 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]));

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

Ergebnis

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. Beispiel 5. Ersetzen von String-Inhalten

  	 [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;
        }

Dieses Beispiel ist ziemlich kurz, doch es hat lange gedauert, es umzusetzen, da ich ohne Erfolg versucht habe, den String-Parameter mithilfe der Attribute [In, Out] oder der Schlüsselbegriffe ref oder out zu verwenden.

Die Lösung ist die Verwendung von StringBuilder anstatt der Variable 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);

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

Ergebnis

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. Beispiel 6. Senden und Ändern der MqlTick-Struktur

	 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;
        }

Die MqlTick-Struktur wird per Verweis übergeben und durch den Schlüsselbegriff ref markiert. Der MqlTick-Struktur selbst muss das Attribut [StructLayout (LayoutKind.Sequential, Pack =1)] vorangestellt werden.

Der Parameter Pack beschreibt den Datenabgleich in der Struktur. Weitere Details finden Sie unter 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);
  }
//+------------------------------------------------------------------+

Ergebnis

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

 

Fazit

In diesem Beitrag stelle ich verschiedene Interaktionsmethoden zwischen MQL5-Code und gemanagtem C#-Code vor.

Ich habe auch mehrere Beispiele dafür bereitgestellt, wie MQL5-Strukturen gegen C# angeordnet werden können und wie sich exportierte DLL-Funktionen in MQL5-Scripts aufrufen lassen. Ich bin überzeugt, dass die hier bereitgestellten Beispiele als Basis für zukünftige Forschungen zum Schreiben von DLLs in gemanagtem Code dienen können.

Dieser Beitrag bereitet auch Wege für die Nutzung bereits in C# implementierter Bibliotheken in MetaTrader. Weitere Details finden Sie in den im Abschnitt Literatur verlinkten Beiträgen. Zu Beiträgen mit englischen Titeln existieren keine deutschen Übersetzungen. Zu den deutschen Versionen der verlinkten Beiträge gelangen Sie, indem Sie "en-us" in der URL durch "de-de" ersetzen.

Bitte legen Sie die Dateien zum Testen in die folgenden Ordner:

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

Literatur

  1. Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications
  2. Interoperating with Unmanaged Code
  3. Introduction to COM Interop
  4. Component Object Model (COM)
  5. Exportieren aus einer DLL mithilfe von "__declspec(dllexport)
  6. Gewusst wie: Deklarieren von Handles in systemeigenen Typen
  7. How to call C++ code from Managed, and vice versa (Interop)
  8. Reverse P/Invoke and exception 
  9. Wie Sie eine verwaltete DLL von systemeigenen Visual C++-Code in Visual Studio.NET oder Visual Studio 2005 aufrufen
  10. Lernprogramm für die Plattformaktivierung
  11. PInvoke-Reverse PInvoke and __stdcall - __cdecl
  12. Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks)
  13. Mixing .NET and native code
  14. Export Managed Code as Unmanaged
  15. Understanding Classic COM Interoperability With .NET Applications
  16. Managed Extensions for C++ Programming
  17. Robert Gieseckes Seite
  18. MSBuild Tasks
  19. Common Language Runtime